signalk-tides
Version:
Tidal predictions for the vessel's position from various online sources.
106 lines (105 loc) • 4.34 kB
JavaScript
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);
}
}