mutable shipNumberStatus = "loading"
mutable tripDateStatus = "loading"
shipNumber = {
  const pageURL = new URL(window.location.href)
  const shipNum = pageURL.searchParams.get("shipNumber")
  if (shipNum == null) {
    console.log("Ship num missing")
    mutable shipNumberStatus = "error-shipnum-missing"
    return null
  }
  
  // imo and mmsi ship numbers are 7 and 9 digits respectively
  const isIMONumber = /^[0-9]{7}$/
  const isMMSINumber = /^[0-9]{9}$/
  if (!(isIMONumber.test(shipNum) || isMMSINumber.test(shipNum))) {
    mutable shipNumberStatus = "error-shipnum-invalid"
    return null
  }
  mutable shipNumberStatus = "ok"
  return shipNum
}
tripDate = {
  const pageURL = new URL(window.location.href)
  const tripDate = pageURL.searchParams.get("date")
  if (tripDate == null) {
    console.log("Date missing")
    mutable tripDateStatus = "error-date-missing"
    return null
  }
  const isDate = /^[0-9]{8}$/
  if (!(isDate.test(tripDate))) {
    mutable tripDateStatus = "error-date-invalid"
    return null
  }
  mutable tripDateStatus = "ok"
  return tripDate
}
// unblock page if ship number and date are both available
unblockPage = {
  if (shipNumberStatus == "ok" && tripDateStatus == "ok") {
    document.title = `Ship ${shipNumber} on ${tripDate} - CounterCurrent`
    document
      .querySelectorAll(".results-required")
      .forEach(el => el.classList.add("results-found"))
  }
}greatCircle = [response.gc_start, response.gc_end].map(d => ({
    ...d,
    dateTimeString: prettifyDateTime(d.time)
  }))
gcPoints = makePointCollection(greatCircle)
gcLine = makeLineString(greatCircle)
gcBounds = makeBounds(greatCircle)
optimisedLine = makeLineString(response.routes.optimised.waypoints.map(d => ({
  "lon": d.lon,
  "lat": d.lat
})))mapboxgl = {
  const gl = await require("mapbox-gl@3.12")
  if (!gl.accessToken) {
    gl.accessToken = mapboxKey
    const href = await require.resolve("mapbox-gl@3.12/dist/mapbox-gl.css")
    document.head.appendChild(html`<link href=${href} rel=stylesheet>`)
  }
  return gl
}
// load plot
Plot = require("@observablehq/plot@0.6.17")
// instantiate map
viewof map = {
  let container = html`<div />`
  yield container
  let map = new mapboxgl.Map({
    container,
    center: [0, 0],
    zoom: 10,
    style: `mapbox://styles/${mapboxStyle}`,
    scrollZoom: false
  })
  map.on("load", () => {
    container.value = map
    container.dispatchEvent(new CustomEvent("input"))
  })
}mapLoadTime = {
  map.addSource("gc-line-source", {
    type: "geojson",
    data: gcLine,
    lineMetrics: true
  })
  
  map.addLayer({
    id: "gc-line-layer",
    type: "line",
    source: "gc-line-source",
    layout: {
      "line-cap": "square",
      "line-join": "round",
    },
    paint: {
      "line-color": "rgba(0, 0, 0, 0)",  // overridden by line-gradient
      "line-width": 5,
      "line-dasharray": [1, 2]
    }
  })
  map.addSource("optimised-line-source", {
    type: "geojson",
    data: optimisedLine,
    lineMetrics: true
  })
  map.addLayer({
    id: "optimised-line-layer",
    type: "line",
    source: "optimised-line-source",
    layout: {
      "line-cap": "square",
      "line-join": "round",
    },
    paint: {
      "line-color": "rgba(0, 0, 0, 0)",  // overridden by line-gradient
      "line-width": 5,
      // "line-dasharray": [1, 2]
    }
  })
  // add points for start and finish
  map.addSource("gc-point-source", {
    type: "geojson",
    data: gcPoints
  })
  map.addLayer({
    id: "gc-point-layer",
    type: "circle",
    source: "gc-point-source",
    paint: {
      "circle-color": "orange",
      "circle-radius": 8,
    }
  })
  map.addLayer({
    id: "gc-label-layer",
    type: "symbol",
    source: "gc-point-source",
    layout: {
      "text-field": ["get", "dateTimeString"],
      "text-font": ["IBM Plex Sans Condensed Bold"],
      "text-size": 12,
      "text-justify": "left",
      "text-anchor": "left",
    },
    paint: {
      "text-color": "orange",
      "text-halo-color": "#092030",
      "text-halo-width": 2,
      "text-translate": [15, 5]
    }
  })
  // finally, add text labels on points
  return Date.now()
}
// fit map to routes whenevr window is resized
adjustBounds = {
  map.fitBounds(gcBounds, {
    animate: false,
    padding: {
      left: 40,
      right: width > 767.98 ? width * 0.45 : 40,
      top: 100,
      bottom: width > 767.98 ? 250 : 450
    }
  })
}function makePointCollection(arr, xAcc = d => d.lon, yAcc = d => d.lat, props = {}) {
  const coordArray = arr.map(d => ({
    "type": "Feature",
    "geometry": {
      "type": "Point",
      "coordinates": [xAcc(d), yAcc(d)]
    },
    "properties": d
  }))
  return ({
    "type": "FeatureCollection",
    "features": coordArray
  })
}
function makeLineString(arr, xAcc = d => d.lon, yAcc = d => d.lat) {
  const coordArray = arr.map(d => [xAcc(d), yAcc(d)])
  
  return ({
    "type": "FeatureCollection",
    "features": [
      {
        "type": "Feature",
        "geometry": {
          "type": "LineString",
          "coordinates": coordArray
        }
      }
    ]
  })
}
// TODO - reexamine this logic for anti-meridian crossing!
function makeBounds(arr, xAcc = d => d.lon, yAcc = d => d.lat) {
  return [
    [
      Math.min(...arr.map(d => xAcc(d))),
      Math.min(...arr.map(d => yAcc(d)))
    ],
    [
      Math.max(...arr.map(d => xAcc(d))),
      Math.max(...arr.map(d => yAcc(d)))
    ],
  ]
}function animateLineGradient({
  layer,
  colour,
  startTime,
  duration,
  delay
} = {}) {
  // TODO - use d3.scaleLinear()
  const aniProgress = Math.min((now - (startTime + delay)) / duration, 1)
  map.setPaintProperty(
    layer,
    "line-gradient",
    [
      "step", ["line-progress"],
      colour,
      aniProgress,
      "rgba(0, 0, 0, 0)",
    ]
  )
  return aniProgress
}function getMiddleCurrent(arr) {
  if (!Array.isArray(arr)) {
    console.error("getMiddleCurrent expects an array, not: ", arr)
    return null
  }
  const middle = Math.floor(arr.length / 2)
  return arr[middle] < -90 ? NaN : arr[middle]
}
function getCurrentAtDistance(arr, dist, cellWidth = 9260) {
  if (!Array.isArray(arr)) {
    console.error("getCurrentAtDistance expects an array, not: ", arr)
    return null
  }
 const indexFromStart = Math.floor((arr.length / 2) - (dist / cellWidth))
 return arr[indexFromStart] < -90 ? NaN : arr[indexFromStart]
}function accumulate(arr, i, accessor = d => d) {
  return arr.slice(0, i + 1).reduce((acc, curr) => acc + accessor(curr), 0)
}
// prettifyDate: return formatted date
function prettifyDate(dt) {
  const dateObj = new Date(dt)
  
  const thisYear = new Date(Date.now()).getFullYear()
  const wasThisYear = dateObj.getFullYear() == thisYear
  // format with date and month (and year, if the date isn't this year)
  const formatter = wasThisYear ?
    new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric" }) :
    new Intl.DateTimeFormat("en-US", {
      month: "short",
      day: "numeric",
      year: "numeric"
    })
  return formatter.format(dateObj)
}
function prettifyDateTime(dt) {
  const dateObj = new Date(dt)
  // format with date and month (and year, if the date isn't this year)
  const dateFormatter = new Intl.DateTimeFormat("en-US", {
    weekday: "short",
    month: "short",
    day: "numeric",
  })
  const timeFormatter = new Intl.DateTimeFormat("en-US", {
    hour: "numeric",
    minute: "numeric",
    timeZoneName: "short"
  })
  return dateFormatter.format(dateObj).toUpperCase() + "\n" +
    timeFormatter.format(dateObj).toUpperCase()
}land = FileAttachment("/assets/ne_110m_land.json").json()
Plot.plot({
  projection: { 
    type: "orthographic",
    rotate: [-response.gc_start.lon, 0]
  },
  marks: [
    Plot.geo(land, {
      fill: "#ffffff44"
    }),
    Plot.geo(gcLine, {
      stroke: "orange",
      strokeWidth: 1
    }),
    Plot.geo(gcPoints, {
      fill: "orange",
      r: 2
    }),
    Plot.sphere()
  ],
  width: 100,
  marginBottom: 10,
  style: {
    fontFamily: "Inter"
  }
})headlineFuelBurn = gcMaxFuel ?
  `This ship burned <span class="key-stat">${d3.format(".3s")(gcMaxFuel)} tonnes</span> of fuel on ${prettifyDate(response.gc_start.time)}.` :
  ``
headlineFuelSaving = fuelSavingPct && fuelSavingPct > 0 ?
  ` Save <span class="key-stat">${d3.format(".3")(gcMaxFuel - optMaxFuel)} tonnes</span> with our forecasts.` :
  ``
html`<h1> ${headlineFuelBurn}${headlineFuelSaving}</h1>`CounterCurrent uses hyperlocal ocean forecasts to find the most fuel efficient route — with no change in arrival time.
optimalRoute = response.routes.optimised.waypoints
  .map((d, i) => ({
    ...d,
    index: i,
    alongDistNauticalMiles: i * 5,
    type: "Optimal",
    progressFrac: (i / response.routes.optimised.waypoints.length),
    alongCurrentColumn:
      response.oceancurrents.copernicus.along_track.map(d => d[i]),
    crossCurrentColumn:
      response.oceancurrents.copernicus.cross_track.map(d => d[i])
  }))
  .map((d, i, arr) => ({
    ...d,
    cumTime:      accumulate(arr, i, d => d.delta_time),
    cumDeviation: accumulate(arr, i, d => d.delta_crossdistance),
  }))
  .map((d, i) => ({
    ...d,
    alongCurrent: getCurrentAtDistance(d.alongCurrentColumn, d.cumDeviation),
    crossCurrent: getCurrentAtDistance(d.crossCurrentColumn, d.cumDeviation),
  }))
gcRoute = response.routes.great_circle.waypoints
  .map((d, i) => ({
    ...d,
    index: i,
    type: "Great Circle",
    progressFrac: (i / response.routes.great_circle.waypoints.length),
    alongDistNauticalMiles: i * 5,
    alongCurrentColumn:
      response.oceancurrents.copernicus.along_track.map(d => d[i]),
    crossCurrentColumn:
      response.oceancurrents.copernicus.cross_track.map(d => d[i])
  }))
  .map((d, i, arr) => ({
    ...d,
    cumTime:      accumulate(arr, i, d => d.delta_time),
    cumDeviation: accumulate(arr, i, d => d.delta_crossdistance),
  }))
  .map((d, i) => ({
    ...d,
    alongCurrent: getMiddleCurrent(d.alongCurrentColumn),
    crossCurrent: getMiddleCurrent(d.crossCurrentColumn),
  }))
// pivot gc and optimal routes wider for visualisation
routeComparison = optimalRoute.map((d, i) => ({
  ...d,
  index: d.index,
  progressFrac:      d.progressFrac,
  optCumDeviation:   d.cumDeviation,
  fuelSaving:        gcRoute[i].fuel_t - d.fuel_t,
  emissionsSaving:   gcRoute[i].emissions_t - d.emissions_t,
  costSaving:        gcRoute[i].cost_usd - d.cost_usd,
  optAlongCurrent:   d.alongCurrent,
  optCrossCurrent:   d.crossCurrent,
  gcAlongCurrent:    gcRoute[i].alongCurrent,
  gcCrossCurrent:    gcRoute[i].crossCurrent,
  alongCurrentDiff:  gcRoute[i].alongCurrent - d.alongCurrent,
  crossCurrentDiff:  gcRoute[i].crossCurrent - d.crossCurrent,
})).map((d, i) => ({
  ...d,
  currentDiffLength: vectorLength(d.alongCurrentDiff, d.crossCurrentDiff),
  currentDiffAngle:  vectorAngle(d.alongCurrentDiff, d.crossCurrentDiff),
  optHeading:        vectorAngle(9260, -d.delta_crossdistance)
})).map((d, i) => ({
  ...d,
  optHeadingVsCurrentDiff: d.currentDiffAngle - d.optHeading
})).map((d, i, arr) => ({
  ...d,
  fuelSavingCum:      accumulate(arr, i, d => d.fuelSaving),
  emissionsSavingCum: accumulate(arr, i, d => d.emissionsSaving),
  costSavingCum:      accumulate(arr, i, d => d.costSaving),
}))
// combine routes and calculate fuel savings for hero stats
gcMaxFuel  = gcRoute.map(d => d.fuel_t).reduce((sum, a) => sum + a, 0)
optMaxFuel = optimalRoute.map(d => d.fuel_t).reduce((sum, a) => sum + a, 0)
emissionsSaving = routeComparison.slice(-1).map(d => d.emissionsSavingCum)
costSaving = routeComparison.slice(-1).map(d => d.costSavingCum)
  
fuelSavingPct = 1 - (optMaxFuel / gcMaxFuel)
furthestWaypoint =
  optimalRoute
    .toSorted((a, b) => Math.abs(b.cumDeviation) - Math.abs(a.cumDeviation))
    .slice(0, 1)
tripTime = (new Date(response.gc_end.time) - new Date(response.gc_start.time)) / (60 * 60 * 1000)
greatCircleDistance = response.routes.great_circle.waypoints.length * 5deviationFormat = d3.format(".2s")
viewof hudPrimaryPlot = Plot.plot({
  marks: [
    Plot.ruleY([0], {
      stroke: "#333333",
      strokeWidth: 1.5,
      strokeDasharray: "2",
    }),
    Plot.areaY(routeComparison, {
      x: "alongDistNauticalMiles",
      y: "optCumDeviation",
      fill: "#1a65b0",
      fillOpacity: 0.25,
    }),
    Plot.line(routeComparison, {
      x: "alongDistNauticalMiles",
      y: "optCumDeviation",
      stroke: "#1a65b0",
    }),
    /* currents */
    width > 600 ?
      Plot.vector(routeComparison, {
        x: "alongDistNauticalMiles",
        y: d => d.optCumDeviation * 1.5,
        length: d => vectorLength(d.alongCurrentDiff, d.crossCurrentDiff),
        rotate: d => vectorAngle(d.alongCurrentDiff, d.crossCurrentDiff),
        stroke: d => d.fuelSaving > 0 ? "green" : "red"
      }) :
      null,
    // Plot.text(routeComparison, Plot.pointerX({
    //   x: "alongDistNauticalMiles",
    //   y: d => d.optCumDeviation * 1.98,
    //   text: d => d3.format(".2s")(vectorLength(d.alongCurrentDiff, d.crossCurrentDiff)) + "m/s",
    //   stroke: d => d.fuelSaving > 0 ? "green" : "red",
    //   fontSize: 16,
    //   fontWeight: "normal"
    // })),
    /* text annotation */
    Plot.text(furthestWaypoint, {
      x: "alongDistNauticalMiles",
      y: d => d.cumDeviation * -0.7,
      text: d => `We recommend that this ship deviate up to ${deviationFormat(Math.abs(d.cumDeviation) / 1852) + "nm"} in order to benefit from ocean currents.`,
      lineWidth: 18,
    }),
    Plot.ruleX(furthestWaypoint, {
      x: "alongDistNauticalMiles",
      y1: d => d.cumDeviation * 0.9,
      y2: d => d.cumDeviation * -0.2,
      strokeDasharray: "2 2"
    }),
    /* tips */
    Plot.ruleX(optimalRoute, Plot.pointerX({
      x: "alongDistNauticalMiles",
      y1: d => 0,
      y2: "cumDeviation",
      stroke: "#1a65b0",
    })),
    Plot.dot(optimalRoute, Plot.pointerX({
      x: "alongDistNauticalMiles",
      y: "cumDeviation",
      stroke: "#333333",
      fill: "white"
    })),
    Plot.dot(optimalRoute, Plot.pointerX({
      x: "alongDistNauticalMiles",
      y: "cumDeviation",
      stroke: "#1a65b0",
      fill: "white"
    })),
    Plot.text(optimalRoute, Plot.pointerX({
      x: "alongDistNauticalMiles",
      y: "cumDeviation",
      stroke: "#1a65b0",
      fill: "white",
      text: d => deviationFormat(Math.abs(d.cumDeviation) / 1852) + "nm",
      dy: 20
    })),
    /* labels */
    Plot.text([`DIRECT ROUTE: ${greatCircleDistance} NM`], {
      fontWeight: "bold",
      fill: "#1a65b0",
      x: greatCircleDistance / 2,
      y: 0,
      textAnchor: "center",
      // lineAnchor: "bottom",
      dy: -10
    }),
    Plot.text(gcRoute, Plot.selectFirst({
      x: "alongDistNauticalMiles",
      y: 0,
      text: d => "START",
      textAnchor: "start",
      dy: -10,
      fontWeight: "bold",
      fill: "black",
      // TODO - check first/last values before anchoring text against axis
    })),
    Plot.text(gcRoute, Plot.selectLast({
      x: "alongDistNauticalMiles",
      y: 0,
      text: d => "END",
      textAnchor: "end",
      dx: -5,
      dy: -10,
      fontWeight: "bold", 
      fill: "black",
      // TODO - check first/last values before anchoring text against axis
    })),
  ],
  x: {
    label: null,
    tickFormat: d => d3.format(".0f")(d) + "nm"
  },
  y: {
    reverse: true,
    label: null,
    axis: null
  },
  length: {
    range: [0, 15],
  },
  rotate: {
    range: [0, 360]
  },
  width: width,
  height: 275,
  insetBottom: 40,
  insetRight: 20,
  marginLeft: 100,
  marginTop: 50,
  style: {
    fontSize: 16,
    fontFamily: "Inter"
  }
})secondaryPlotColBase = ({
  "fuel": "fuelSaving",
  "emissions": "emissionsSaving",
  "cost": "costSaving"
})[hudSecondarySelection]
secondaryPlotFormat = ({
  "fuel": d => d3.format(".2s")(d) + "T",
  "fuel-pct": ".1%",
  "emissions": d => d3.format(".2s")(d) + "T CO2e",
  "cost": d => "US$" + d3.format(".2s")(d)
})[hudSecondarySelection]
secondaryPlotTitle = ({
  "fuel": "Tonnes of fuel saved",
  "fuel-pct": "Percentage of fuel saved",
  "emissions": "Greenhouse gas emissions saved",
  "cost": "Fuel cost saved"
})[hudSecondarySelection]
secondaryPlotCol = accumulateSavings ?
  secondaryPlotColBase + "Cum" :
  secondaryPlotColBase
secondaryPlotSubtitle = accumulateSavings ?
  `Accumulated over ${d3.format(".0f")(tripTime)} hour trip` :
  "Saved on each leg"
Plot.plot({
  marks: [
    Plot.ruleY([0], {
      stroke: "#333333",
      strokeWidth: 1.5,
      strokeDasharray: "2",
    }),
    Plot.differenceY(routeComparison, {
      x: "alongDistNauticalMiles",
      y: secondaryPlotCol,
      positiveFill: "green",
      negativeFill: "red",
      fillOpacity: 0.3,
      strokeOpacity: 0.9,
      // tip: true
    }),
    /* labels */
    Plot.text(routeComparison, Plot.selectFirst({
      x: "alongDistNauticalMiles",
      y: 0,
      text: d => "START",
      textAnchor: "start",
      dy: -10,
      fontWeight: "bold",
      fill: "black",
      // TODO - check first/last values before anchoring text against axis
    })),
    Plot.text(routeComparison, Plot.selectLast({
      x: "alongDistNauticalMiles",
      y: 0,
      text: d => "END",
      textAnchor: "end",
      dx: -5,
      dy: -10,
      fontWeight: "bold", 
      fill: "black",
      // TODO - check first/last values before anchoring text against axis
    })),
    /* layers brushed from primary hud plot */
    hudPrimaryPlot ?
      Plot.dot(routeComparison.filter(d => d.index == hudPrimaryPlot.index), {
        x: "alongDistNauticalMiles",
        y: secondaryPlotCol,
        // stroke: "type",
        fill: "#1a65b0",
        r: 4
      }) :
      null
  ],
  x: {
    label: null,
    tickFormat: d => d3.format(".0f")(d) + "nm"
  },
  y: {
    label: null,
    tickFormat: secondaryPlotFormat
  },
  color: {
    legend: false
  },
  marginLeft: 100,
  insetRight: 20,
  height: 300,
  width: width,
  title: secondaryPlotTitle,
  subtitle: secondaryPlotSubtitle,
  style: {
    background: "transparent",
    fontSize: 16,
    fontFamily: "Inter"
  }
})