UNPKG

signalk-tides

Version:

Tidal predictions for the vessel's position from various online sources.

106 lines (105 loc) 4.34 kB
import path from "path"; import { unlink } from "fs/promises"; import { getDistance } from "geolib"; import moment from "moment"; import FileCache from "../cache.js"; const stationsUrl = `https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations.json?type=tidepredictions`; const dataGetterUrl = "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter"; const datum = "MLLW"; export default function (app) { return { id: "noaa", title: "NOAA (US only)", async start() { const cache = new FileCache(path.join(app.getDataDirPath(), "noaa")); const stations = await StationList.load(cache, app); // Remove old cache file // @ts-expect-error: configPath exists, just not part of the types unlink(path.join(app.config.configPath, "noaastations.json")).catch(() => { /* ignore */ }); return async (params) => { const { position, date = moment().subtract(1, "days") } = params; const station = stations.closestTo(position); const endpoint = new URL(dataGetterUrl); endpoint.search = new URLSearchParams({ product: "predictions", application: "signalk.org/node-server", begin_date: moment(date).format("YYYYMMDD"), end_date: moment(date).add(7, "days").format("YYYYMMDD"), datum, station: station.id, time_zone: "gmt", units: "metric", interval: "hilo", format: "json", }).toString(); app.debug(`Fetching tides from NOAA: ${endpoint}`); try { const res = await fetch(endpoint.toString()); if (!res.ok) throw new Error("Failed to fetch NOAA tides: " + res.statusText); const body = (await res.json()); app.debug("NOAA response: \n" + JSON.stringify(body, null, 2)); if (body.error) throw new Error(body.error.message); return { station: { name: station.name, position: { latitude: station.lat, longitude: station.lng, }, }, extremes: body.predictions.map(({ t, v, type }) => ({ type: type === "H" ? "High" : "Low", value: Number(v), time: new Date(`${t}Z`).toISOString(), })), }; } catch (err) { app.setPluginError(`Failed to fetch NOAA tides: ${err}`); // @ts-expect-error: app.error should accept more than just a string app.error(err); throw err; } }; }, }; } class StationList extends Map { static async load(cache, app) { let data = (await cache.get("stations")); if (data) { app.debug("NOAA: Loaded cached tide stations"); } else { app.debug("NOAA: Downloading tide stations"); const res = await fetch(stationsUrl); if (!res.ok) throw new Error(`Failed to download stations: ${res.statusText}`); data = (await res.json()); await cache.set("stations", data); } return new this(data.stations); } constructor(data) { super(data.map((station) => [station.id, station])); } closestTo(position) { return this.near(position, 1)[0]; } near(position, limit = 10) { const stationsWithDistances = Array.from(this.values()).map((station) => ({ ...station, distance: getDistance(position, { latitude: station.lat, longitude: station.lng, }), })); return stationsWithDistances .sort((a, b) => a.distance - b.distance) .slice(0, limit); } }