@sailingnaturali/signalk-currents 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/cache.d.ts +2 -2
- package/dist/calculations.js +2 -0
- package/dist/fetch.d.ts +6 -3
- package/dist/fetch.js +15 -12
- package/dist/index.js +9 -6
- package/dist/routes.d.ts +2 -0
- package/dist/routes.js +3 -1
- package/dist/sources/noaa.d.ts +5 -2
- package/dist/sources/noaa.js +12 -3
- package/dist/types.d.ts +7 -2
- package/dist/types.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,8 +47,8 @@ Each entry in `stations`:
|
|
|
47
47
|
| `noaaBin` | number | NOAA current-station bin (NOAA only). |
|
|
48
48
|
| `label` | string | Human-readable name. |
|
|
49
49
|
| `lat` / `lon` | number | Station position (used for nearest-station selection). |
|
|
50
|
-
| `floodDir` | number | Set direction (°true) while flooding. |
|
|
51
|
-
| `ebbDir` | number | Set direction (°true) while ebbing. |
|
|
50
|
+
| `floodDir` | number | Set direction (°true) while flooding. **CHS: required** (from the Canadian Tide and Current Tables / Sailing Directions). **NOAA: optional** — the API's measured `meanFloodDir` overrides whatever is configured. |
|
|
51
|
+
| `ebbDir` | number | Set direction (°true) while ebbing. Same sourcing rules as `floodDir` (NOAA `meanEbbDir`). |
|
|
52
52
|
|
|
53
53
|
### Example station config
|
|
54
54
|
|
|
@@ -99,7 +99,8 @@ ebb — fill them from a current atlas or pilot book for your stations.
|
|
|
99
99
|
|
|
100
100
|
### `/currents` resource
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
Served at `/signalk/v2/api/resources/currents` (anonymously readable under
|
|
103
|
+
`allow_readonly`):
|
|
103
104
|
|
|
104
105
|
```json
|
|
105
106
|
{
|
|
@@ -109,6 +110,8 @@ Mounted at `/plugins/signalk-currents/currents`:
|
|
|
109
110
|
"label": "Gillard Passage",
|
|
110
111
|
"lat": 50.3933,
|
|
111
112
|
"lon": -125.1567,
|
|
113
|
+
"floodDir": 160,
|
|
114
|
+
"ebbDir": 340,
|
|
112
115
|
"events": [
|
|
113
116
|
{ "utc": "2026-06-06T04:14:00.000Z", "kind": "slack", "speedKn": 0 },
|
|
114
117
|
{ "utc": "2026-06-06T05:40:00.000Z", "kind": "flood", "speedKn": 4.1 }
|
|
@@ -119,6 +122,8 @@ Mounted at `/plugins/signalk-currents/currents`:
|
|
|
119
122
|
```
|
|
120
123
|
|
|
121
124
|
`kind` is `slack` | `flood` | `ebb`; `speedKn` is the event speed magnitude in knots.
|
|
125
|
+
`floodDir` / `ebbDir` are the station's set directions in °true, straight from the
|
|
126
|
+
station config — so consumers can say which way the water flows, not just when it turns.
|
|
122
127
|
|
|
123
128
|
## Development
|
|
124
129
|
|
package/dist/cache.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export type DayCache = Map<string,
|
|
1
|
+
import { DayData } from './fetch';
|
|
2
|
+
export type DayCache = Map<string, DayData>;
|
|
3
3
|
export declare function createCache(): DayCache;
|
package/dist/calculations.js
CHANGED
|
@@ -40,5 +40,7 @@ function interpolateCurrent(now, events, station) {
|
|
|
40
40
|
extremum = e0;
|
|
41
41
|
} // rare flood↔ebb, linear
|
|
42
42
|
const dir = extremum.kind === 'ebb' ? station.ebbDir : station.floodDir;
|
|
43
|
+
if (dir === undefined)
|
|
44
|
+
return undefined; // no honest setTrue without a set direction
|
|
43
45
|
return { drift: speedKn * KN_TO_MS, setTrue: (dir * Math.PI) / 180 };
|
|
44
46
|
}
|
package/dist/fetch.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { CurrentEvent, StationConfig } from './types';
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { CurrentEvent, StationConfig, StationDirs } from './types';
|
|
2
|
+
export interface DayData extends StationDirs {
|
|
3
|
+
events: CurrentEvent[];
|
|
4
|
+
}
|
|
5
|
+
type DayFetcher = (s: StationConfig, dayStart: Date, dayEnd: Date) => Promise<DayData>;
|
|
6
|
+
export declare function stationData(station: StationConfig, start: Date, horizonDays: number, cache: Map<string, DayData>, fetcher?: DayFetcher): Promise<DayData>;
|
|
4
7
|
export {};
|
package/dist/fetch.js
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.stationData = stationData;
|
|
4
4
|
const chs_1 = require("./sources/chs");
|
|
5
5
|
const noaa_1 = require("./sources/noaa");
|
|
6
|
-
const defaultFetcher = (s, a, b) => s.provider === 'chs'
|
|
7
|
-
? (0, chs_1.fetchChsEvents)(s.stationId, a, b)
|
|
6
|
+
const defaultFetcher = async (s, a, b) => s.provider === 'chs'
|
|
7
|
+
? { events: await (0, chs_1.fetchChsEvents)(s.stationId, a, b) }
|
|
8
8
|
: (0, noaa_1.fetchNoaaEvents)(s.stationId, s.noaaBin ?? 0, a, b);
|
|
9
9
|
function utcDays(start, n) {
|
|
10
10
|
const base = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate()));
|
|
11
11
|
return Array.from({ length: n }, (_, i) => new Date(base.getTime() + i * 86400000).toISOString().slice(0, 10));
|
|
12
12
|
}
|
|
13
|
-
async function
|
|
14
|
-
const
|
|
13
|
+
async function stationData(station, start, horizonDays, cache, fetcher = defaultFetcher) {
|
|
14
|
+
const events = [];
|
|
15
|
+
let floodDir, ebbDir;
|
|
15
16
|
for (const day of utcDays(start, horizonDays)) {
|
|
16
17
|
const key = `${station.provider}:${station.stationId}:${day}`;
|
|
17
|
-
let
|
|
18
|
-
if (!
|
|
18
|
+
let data = cache.get(key);
|
|
19
|
+
if (!data) {
|
|
19
20
|
const dayStart = new Date(`${day}T00:00:00Z`);
|
|
20
21
|
const dayEnd = new Date(dayStart.getTime() + 86400000);
|
|
21
|
-
|
|
22
|
-
cache.set(key,
|
|
22
|
+
data = await fetcher(station, dayStart, dayEnd);
|
|
23
|
+
cache.set(key, data);
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
events.push(...data.events);
|
|
26
|
+
floodDir = floodDir ?? data.floodDir;
|
|
27
|
+
ebbDir = ebbDir ?? data.ebbDir;
|
|
25
28
|
}
|
|
26
|
-
|
|
27
|
-
return
|
|
29
|
+
events.sort((a, b) => a.utc.localeCompare(b.utc));
|
|
30
|
+
return { events, floodDir, ebbDir };
|
|
28
31
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
const types_1 = require("./types");
|
|
2
3
|
const cache_1 = require("./cache");
|
|
3
4
|
const fetch_1 = require("./fetch");
|
|
4
5
|
const calculations_1 = require("./calculations");
|
|
@@ -19,13 +20,13 @@ module.exports = function (app) {
|
|
|
19
20
|
properties: {
|
|
20
21
|
stations: {
|
|
21
22
|
type: 'array', title: 'Current stations',
|
|
22
|
-
items: { type: 'object', required: ['provider', 'stationId', 'label', 'lat', 'lon'
|
|
23
|
+
items: { type: 'object', required: ['provider', 'stationId', 'label', 'lat', 'lon'],
|
|
23
24
|
properties: {
|
|
24
25
|
provider: { type: 'string', enum: ['chs', 'noaa'] },
|
|
25
26
|
stationId: { type: 'string' }, noaaBin: { type: 'number' },
|
|
26
27
|
label: { type: 'string' }, lat: { type: 'number' }, lon: { type: 'number' },
|
|
27
|
-
floodDir: { type: 'number', title: 'Flood set (°true)' },
|
|
28
|
-
ebbDir: { type: 'number', title: 'Ebb set (°true)' },
|
|
28
|
+
floodDir: { type: 'number', title: 'Flood set (°true) — required for CHS; NOAA stations use the API\'s measured meanFloodDir' },
|
|
29
|
+
ebbDir: { type: 'number', title: 'Ebb set (°true) — required for CHS; NOAA stations use the API\'s measured meanEbbDir' },
|
|
29
30
|
} },
|
|
30
31
|
},
|
|
31
32
|
horizonDays: { type: 'number', default: 3 },
|
|
@@ -59,8 +60,9 @@ module.exports = function (app) {
|
|
|
59
60
|
// others (or skip the environment.current publish) for the whole cycle.
|
|
60
61
|
for (const station of stations) {
|
|
61
62
|
try {
|
|
62
|
-
const
|
|
63
|
-
|
|
63
|
+
const data = await (0, fetch_1.stationData)(station, now, horizonDays, cache);
|
|
64
|
+
// Provider-measured set directions (NOAA) beat the config values.
|
|
65
|
+
series.set(station.stationId, { station: (0, types_1.resolveStation)(station, data), events: data.events });
|
|
64
66
|
}
|
|
65
67
|
catch (e) {
|
|
66
68
|
app.error(`station ${station.label} fetch failed: ${e.message}`);
|
|
@@ -80,7 +82,8 @@ module.exports = function (app) {
|
|
|
80
82
|
app.setPluginStatus('No station near vessel position');
|
|
81
83
|
return;
|
|
82
84
|
}
|
|
83
|
-
|
|
85
|
+
// entry.station carries the resolved dirs; `station` is raw config.
|
|
86
|
+
const current = (0, calculations_1.interpolateCurrent)(now, entry.events, entry.station);
|
|
84
87
|
if (!current) {
|
|
85
88
|
app.setPluginStatus(`No current data bracketing now for ${station.label}`);
|
|
86
89
|
return;
|
package/dist/routes.d.ts
CHANGED
package/dist/routes.js
CHANGED
|
@@ -7,7 +7,9 @@ function currentsPayload(series) {
|
|
|
7
7
|
return {
|
|
8
8
|
stations: [...series.values()].map(s => ({
|
|
9
9
|
stationId: s.station.stationId, label: s.station.label,
|
|
10
|
-
lat: s.station.lat, lon: s.station.lon,
|
|
10
|
+
lat: s.station.lat, lon: s.station.lon,
|
|
11
|
+
floodDir: s.station.floodDir, ebbDir: s.station.ebbDir,
|
|
12
|
+
events: s.events,
|
|
11
13
|
})),
|
|
12
14
|
};
|
|
13
15
|
}
|
package/dist/sources/noaa.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
import { CurrentEvent } from '../types';
|
|
2
|
-
export
|
|
1
|
+
import { CurrentEvent, StationDirs } from '../types';
|
|
2
|
+
export interface NoaaDayData extends StationDirs {
|
|
3
|
+
events: CurrentEvent[];
|
|
4
|
+
}
|
|
5
|
+
export declare function fetchNoaaEvents(stationId: string, bin: number, start: Date, end: Date, fetchFn?: typeof fetch): Promise<NoaaDayData>;
|
package/dist/sources/noaa.js
CHANGED
|
@@ -18,12 +18,21 @@ async function fetchNoaaEvents(stationId, bin, start, end, fetchFn = fetch) {
|
|
|
18
18
|
if (!resp.ok)
|
|
19
19
|
throw new Error(`NOAA ${resp.status}`);
|
|
20
20
|
const cp = (await resp.json())?.current_predictions?.cp ?? [];
|
|
21
|
-
const
|
|
21
|
+
const events = [];
|
|
22
|
+
let floodDir, ebbDir;
|
|
22
23
|
for (const row of cp) {
|
|
24
|
+
// Every row repeats the station/bin's measured principal directions; take
|
|
25
|
+
// the first finite pair — this is the authority config can't match.
|
|
26
|
+
if (floodDir === undefined && Number.isFinite(Number(row.meanFloodDir))) {
|
|
27
|
+
floodDir = Number(row.meanFloodDir);
|
|
28
|
+
}
|
|
29
|
+
if (ebbDir === undefined && Number.isFinite(Number(row.meanEbbDir))) {
|
|
30
|
+
ebbDir = Number(row.meanEbbDir);
|
|
31
|
+
}
|
|
23
32
|
const kind = String(row.Type ?? '').toLowerCase();
|
|
24
33
|
if (kind !== 'slack' && kind !== 'flood' && kind !== 'ebb')
|
|
25
34
|
continue;
|
|
26
|
-
|
|
35
|
+
events.push((0, types_1.eventFromParts)(parseNoaaTime(row.Time), kind, parseFloat(row.Velocity_Major)));
|
|
27
36
|
}
|
|
28
|
-
return
|
|
37
|
+
return { events, floodDir, ebbDir };
|
|
29
38
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -11,7 +11,12 @@ export interface StationConfig {
|
|
|
11
11
|
label: string;
|
|
12
12
|
lat: number;
|
|
13
13
|
lon: number;
|
|
14
|
-
floodDir
|
|
15
|
-
ebbDir
|
|
14
|
+
floodDir?: number;
|
|
15
|
+
ebbDir?: number;
|
|
16
16
|
}
|
|
17
|
+
export interface StationDirs {
|
|
18
|
+
floodDir?: number;
|
|
19
|
+
ebbDir?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function resolveStation(station: StationConfig, fetched: StationDirs): StationConfig;
|
|
17
22
|
export declare function eventFromParts(utc: string, kind: CurrentKind, speed: number): CurrentEvent;
|
package/dist/types.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveStation = resolveStation;
|
|
3
4
|
exports.eventFromParts = eventFromParts;
|
|
5
|
+
// Measured dirs from the provider (NOAA meanFloodDir/meanEbbDir) beat whatever
|
|
6
|
+
// was typed into config; config is the fallback (and the only source for CHS).
|
|
7
|
+
function resolveStation(station, fetched) {
|
|
8
|
+
return {
|
|
9
|
+
...station,
|
|
10
|
+
floodDir: fetched.floodDir ?? station.floodDir,
|
|
11
|
+
ebbDir: fetched.ebbDir ?? station.ebbDir,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
4
14
|
function eventFromParts(utc, kind, speed) {
|
|
5
15
|
return { utc: new Date(utc).toISOString(), kind, speedKn: Math.abs(speed) };
|
|
6
16
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sailingnaturali/signalk-currents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Publish CHS/NOAA tidal-current predictions to SignalK — environment.current + a /currents resource, for a configured station list.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|