signalk-tides 2.0.0 → 2.1.1
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/.github/workflows/release.yml +41 -0
- package/dist/middleware.js +13 -2
- package/package.json +7 -7
- package/public/assets/{index-D7RIUA1w.js → index-BGlA8qI5.js} +2 -2
- package/public/assets/{index-W6DTmxq6.css → index-BmxnqaCv.css} +1 -1
- package/public/assets/maplibre-gl-C8kBFIRu.js +748 -0
- package/public/index.html +2 -2
- package/.playwright-mcp/console-2026-06-06T09-48-18-232Z.log +0 -1
- package/.playwright-mcp/console-2026-06-12T13-01-03-706Z.log +0 -1
- package/.playwright-mcp/console-2026-06-12T13-15-30-205Z.log +0 -44
- package/.playwright-mcp/console-2026-06-12T13-16-00-389Z.log +0 -1
- package/.playwright-mcp/console-2026-06-12T14-41-49-989Z.log +0 -60
- package/.playwright-mcp/console-2026-06-12T14-51-31-609Z.log +0 -5
- package/dist/routes.js +0 -176
- package/dist/sources/index.js +0 -7
- package/dist/sources/neaps.js +0 -47
- package/dist/sources/noaa.js +0 -266
- package/dist/sources/stormglass.js +0 -199
- package/dist/sources/worldtides.js +0 -225
- package/dist/types.js +0 -1
- package/public/assets/maplibre-gl-c4hHMhPw.js +0 -748
package/public/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="./icon.svg" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
8
|
<title>Tides</title>
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-BGlA8qI5.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BmxnqaCv.css">
|
|
11
11
|
</head>
|
|
12
12
|
|
|
13
13
|
<body>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[ 2058ms] [ERROR] Failed to load resource: net::ERR_NETWORK_IO_SUSPENDED @ https://docs.stormglass.io/_media/logo.svg:0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[ 239ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3000/signalk-tides/assets/icon.svg:0
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
[ 260ms] [LOG] [ConnectionStateMachine] Service initialized @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
2
|
-
[ 265ms] [LOG] [AppInit Network Service] Connection Configuration not found. Creating configuration using Auto-Discovery URL: http://localhost:3000 @ http://localhost:3000/@mxtommy/kip/main-UBENKC2D.js:0
|
|
3
|
-
[ 268ms] [LOG] [ConnectionStateMachine] Starting HTTP discovery: Connecting to http://localhost:3000 @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
4
|
-
[ 268ms] [LOG] [Connection Service] Connecting to: http://localhost:3000 @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
5
|
-
[ 279ms] [LOG] [ConnectionStateMachine] HTTP discovery successful @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
6
|
-
[ 279ms] [LOG] [AppSettings Service] Service startup... @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:3
|
|
7
|
-
[ 279ms] [LOG] [AppSettings Service] LocalStorage enabled @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
|
|
8
|
-
[ 279ms] [LOG] [AppSettings Service] Error loading appConfig config. Force loading appConfig defaults @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
|
|
9
|
-
[ 279ms] [LOG] [AppSettings Service] Error loading dashboardsConfig config. Force loading dashboardsConfig defaults @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
|
|
10
|
-
[ 279ms] [LOG] [AppSettings Service] Error loading themeConfig config. Force loading themeConfig defaults @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
|
|
11
|
-
[ 280ms] [LOG] [AppSettings Service] Saving Application config to LocalStorage @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
|
|
12
|
-
[ 280ms] [LOG] [DatasetStreamService] Auto Starting 0 Datasets @ http://localhost:3000/@mxtommy/kip/chunk-5SMHTIVS.js:0
|
|
13
|
-
[ 280ms] [LOG] [AppInit Network Service] Initialization completed @ http://localhost:3000/@mxtommy/kip/main-UBENKC2D.js:0
|
|
14
|
-
[ 280ms] [LOG] [ConnectionStateMachine] WebSocket mode enabled @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
15
|
-
[ 280ms] [LOG] [AppInit Network Service] Starting WebSocket connection after initialization @ http://localhost:3000/@mxtommy/kip/main-UBENKC2D.js:0
|
|
16
|
-
[ 280ms] [LOG] [ConnectionStateMachine] Starting WebSocket connection @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
17
|
-
[ 280ms] [LOG] [Delta Service] ConnectionStateMachine request: WebSocket opening... @ http://localhost:3000/@mxtommy/kip/chunk-IXUKD73N.js:0
|
|
18
|
-
[ 287ms] [WARNING] [Dashboard Service] No dashboards found in settings, creating blank dashboard @ http://localhost:3000/@mxtommy/kip/chunk-5SMHTIVS.js:0
|
|
19
|
-
[ 289ms] [LOG] [Remote Dashboards] Cleaning paths on server @ http://localhost:3000/@mxtommy/kip/main-UBENKC2D.js:0
|
|
20
|
-
[ 290ms] [LOG] [UI Event Service] PWA mode: false @ http://localhost:3000/@mxtommy/kip/chunk-5SMHTIVS.js:49
|
|
21
|
-
[ 300ms] [LOG] *********** KIP Version Information *********** @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
|
|
22
|
-
[ 300ms] [LOG] ** App Version: 4.8.0 @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
|
|
23
|
-
[ 300ms] [LOG] ** Browser Version: Chrome 149 @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
|
|
24
|
-
[ 300ms] [LOG] ** OS Version: macOS @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
|
|
25
|
-
[ 300ms] [LOG] *********************************************** @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
|
|
26
|
-
[ 305ms] [LOG] [AppSettings Service] Saving Dashboard config to LocalStorage @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
|
|
27
|
-
[ 323ms] [LOG] [Delta Service] WebSocket connected without Authorization Token @ http://localhost:3000/@mxtommy/kip/chunk-IXUKD73N.js:0
|
|
28
|
-
[ 324ms] [LOG] [ConnectionStateMachine] WebSocket connected @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
29
|
-
[ 324ms] [LOG] [Connection Service] Server Name: signalk-server, Version: 2.23.0-beta.1, Roles: ["master","main"] @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
30
|
-
[ 437ms] [WARNING] [Request Service] Status Code: 403 - DENIED: Authorization with R/W or Admin permission level is required to send commands. Configure Sign In credential. @ http://localhost:3000/@mxtommy/kip/chunk-W6MCE3GH.js:0
|
|
31
|
-
[ 437ms] [WARNING] [Request Service] Status Code: 403 - DENIED: Authorization with R/W or Admin permission level is required to send commands. Configure Sign In credential. @ http://localhost:3000/@mxtommy/kip/chunk-W6MCE3GH.js:0
|
|
32
|
-
[ 437ms] [WARNING] [Request Service] Status Code: 403 - DENIED: Authorization with R/W or Admin permission level is required to send commands. Configure Sign In credential. @ http://localhost:3000/@mxtommy/kip/chunk-W6MCE3GH.js:0
|
|
33
|
-
[ 450ms] [WARNING] An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. @ http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:3
|
|
34
|
-
[ 515ms] [WARNING] An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. @ http://localhost:3000/@signalk/freeboard-sk/:0
|
|
35
|
-
[ 1311ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/freeboard-sk:0
|
|
36
|
-
[ 1387ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/kip:0
|
|
37
|
-
[ 1387ms] [LOG] [KipSeriesApiClientService] POST http://localhost:3000/plugins/kip/series/reconcile (0 series) @ http://localhost:3000/@mxtommy/kip/chunk-6ZR3M3IQ.js:2
|
|
38
|
-
[ 1403ms] [INFO] Internet connection detected. @ http://localhost:3000/@signalk/freeboard-sk/main-YHMZXR5G.js:249
|
|
39
|
-
[ 1414ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/kip/series/reconcile:0
|
|
40
|
-
[ 1414ms] [ERROR] [KipSeriesApiClientService] Series reconcile request failed: Ar @ http://localhost:3000/@mxtommy/kip/chunk-6ZR3M3IQ.js:2
|
|
41
|
-
[ 1517ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3000/signalk/v1/api/vessels/self/navigation/anchor:0
|
|
42
|
-
[ 1784ms] [WARNING] Blocked aria-hidden on an element because its descendant retained focus. The focus must not be hidden from assistive technology users. Avoid using aria-hidden on a focused element or its ancestor. Consider using the inert attribute instead, which will also prevent focus. For more details, see the aria-hidden section of the WAI-ARIA specification at https://w3c.github.io/aria/#aria-hidden.
|
|
43
|
-
Element with focus: <div>
|
|
44
|
-
Ancestor with aria-hidden: <app-root> @ http://localhost:3000/@signalk/freeboard-sk/:0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[ 68ms] [LOG] [Delta Service] App suspended @ http://localhost:3000/@mxtommy/kip/chunk-IXUKD73N.js:0
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
[ 188ms] [LOG] [ConnectionStateMachine] Service initialized @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
2
|
-
[ 191ms] [LOG] [ConnectionStateMachine] Starting HTTP discovery: Connecting to http://localhost:3000 @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
3
|
-
[ 192ms] [LOG] [Connection Service] Connecting to: http://localhost:3000 @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
4
|
-
[ 213ms] [LOG] [ConnectionStateMachine] HTTP discovery successful @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
5
|
-
[ 214ms] [LOG] [AppSettings Service] Service startup... @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:3
|
|
6
|
-
[ 214ms] [LOG] [AppSettings Service] LocalStorage enabled @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
|
|
7
|
-
[ 214ms] [LOG] [DatasetStreamService] Auto Starting 0 Datasets @ http://localhost:3000/@mxtommy/kip/chunk-5SMHTIVS.js:0
|
|
8
|
-
[ 214ms] [LOG] [AppInit Network Service] Initialization completed @ http://localhost:3000/@mxtommy/kip/main-UBENKC2D.js:0
|
|
9
|
-
[ 214ms] [LOG] [ConnectionStateMachine] WebSocket mode enabled @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
10
|
-
[ 214ms] [LOG] [AppInit Network Service] Starting WebSocket connection after initialization @ http://localhost:3000/@mxtommy/kip/main-UBENKC2D.js:0
|
|
11
|
-
[ 214ms] [LOG] [ConnectionStateMachine] Starting WebSocket connection @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
12
|
-
[ 215ms] [LOG] [Delta Service] ConnectionStateMachine request: WebSocket opening... @ http://localhost:3000/@mxtommy/kip/chunk-IXUKD73N.js:0
|
|
13
|
-
[ 220ms] [LOG] [Remote Dashboards] Cleaning paths on server @ http://localhost:3000/@mxtommy/kip/main-UBENKC2D.js:0
|
|
14
|
-
[ 221ms] [LOG] [UI Event Service] PWA mode: false @ http://localhost:3000/@mxtommy/kip/chunk-5SMHTIVS.js:49
|
|
15
|
-
[ 226ms] [LOG] *********** KIP Version Information *********** @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
|
|
16
|
-
[ 226ms] [LOG] ** App Version: 4.8.0 @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
|
|
17
|
-
[ 226ms] [LOG] ** Browser Version: Chrome 149 @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
|
|
18
|
-
[ 226ms] [LOG] ** OS Version: macOS @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
|
|
19
|
-
[ 226ms] [LOG] *********************************************** @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
|
|
20
|
-
[ 229ms] [LOG] [AppSettings Service] Saving Dashboard config to LocalStorage @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
|
|
21
|
-
[ 248ms] [LOG] [Delta Service] WebSocket connected without Authorization Token @ http://localhost:3000/@mxtommy/kip/chunk-IXUKD73N.js:0
|
|
22
|
-
[ 248ms] [LOG] [ConnectionStateMachine] WebSocket connected @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
23
|
-
[ 249ms] [LOG] [Connection Service] Server Name: signalk-server, Version: 2.23.0-beta.1, Roles: ["master","main"] @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
|
|
24
|
-
[ 370ms] [WARNING] [Request Service] Status Code: 403 - DENIED: Authorization with R/W or Admin permission level is required to send commands. Configure Sign In credential. @ http://localhost:3000/@mxtommy/kip/chunk-W6MCE3GH.js:0
|
|
25
|
-
[ 370ms] [WARNING] [Request Service] Status Code: 403 - DENIED: Authorization with R/W or Admin permission level is required to send commands. Configure Sign In credential. @ http://localhost:3000/@mxtommy/kip/chunk-W6MCE3GH.js:0
|
|
26
|
-
[ 370ms] [WARNING] [Request Service] Status Code: 403 - DENIED: Authorization with R/W or Admin permission level is required to send commands. Configure Sign In credential. @ http://localhost:3000/@mxtommy/kip/chunk-W6MCE3GH.js:0
|
|
27
|
-
[ 380ms] [WARNING] An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. @ http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:3
|
|
28
|
-
[ 476ms] [WARNING] An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. @ http://localhost:3000/@signalk/freeboard-sk/:0
|
|
29
|
-
[ 934ms] [INFO] Internet connection detected. @ http://localhost:3000/@signalk/freeboard-sk/main-YHMZXR5G.js:249
|
|
30
|
-
[ 940ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/freeboard-sk:0
|
|
31
|
-
[ 1000ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/kip:0
|
|
32
|
-
[ 1000ms] [LOG] [KipSeriesApiClientService] POST http://localhost:3000/plugins/kip/series/reconcile (0 series) @ http://localhost:3000/@mxtommy/kip/chunk-6ZR3M3IQ.js:2
|
|
33
|
-
[ 1007ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/kip/series/reconcile:0
|
|
34
|
-
[ 1007ms] [ERROR] [KipSeriesApiClientService] Series reconcile request failed: Ar @ http://localhost:3000/@mxtommy/kip/chunk-6ZR3M3IQ.js:2
|
|
35
|
-
[ 1073ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3000/signalk/v1/api/vessels/self/navigation/anchor:0
|
|
36
|
-
[ 87530ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/freeboard-sk:0
|
|
37
|
-
[ 87531ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/anchoralarm:0
|
|
38
|
-
[ 87532ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/tracks:0
|
|
39
|
-
[ 87532ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/signalk-polar-performance-plugin:0
|
|
40
|
-
[ 87532ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/resources-provider:0
|
|
41
|
-
[ 87536ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/signalk-racer:0
|
|
42
|
-
[ 87572ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/course-provider:0
|
|
43
|
-
[ 87573ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/autopilot:0
|
|
44
|
-
[ 87573ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/pypilot-autopilot-provider:0
|
|
45
|
-
[ 112153ms] [ERROR] ERROR _: NG0951
|
|
46
|
-
at Object.computation (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:4:94168)
|
|
47
|
-
at Object.producerRecomputeValue (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:3:33213)
|
|
48
|
-
at Qn (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:3:30918)
|
|
49
|
-
at n.r [as iframe] (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:3:32825)
|
|
50
|
-
at n.ngAfterViewInit (http://localhost:3000/@mxtommy/kip/chunk-5SMHTIVS.js:44:361583)
|
|
51
|
-
at ng (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:4:27312)
|
|
52
|
-
at rI (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:4:27474)
|
|
53
|
-
at Gg (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:4:27201)
|
|
54
|
-
at wa (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:4:26929)
|
|
55
|
-
at bS (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:4:67813) @ http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:3
|
|
56
|
-
[ 263184ms] [LOG] [AppSettings Service] Saving Dashboard config to LocalStorage @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
|
|
57
|
-
[ 263185ms] [LOG] [AppSettings Service] Saving Application config to LocalStorage @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
|
|
58
|
-
[ 399636ms] [LOG] [AppSettings Service] Saving Dashboard config to LocalStorage @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
|
|
59
|
-
[ 399636ms] [LOG] [AppSettings Service] Saving Application config to LocalStorage @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
|
|
60
|
-
[ 4767580ms] [LOG] [Delta Service] App suspended @ http://localhost:3000/@mxtommy/kip/chunk-IXUKD73N.js:0
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
[ 2353ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/skServer/plugins:0
|
|
2
|
-
[ 2353ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/skServer/appstore/available:0
|
|
3
|
-
[ 2353ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/skServer/security/access/requests:0
|
|
4
|
-
[ 2427ms] [LOG] connected @ http://localhost:3000/admin/assets/bootstrap-BbNbkfZh.js:69
|
|
5
|
-
[ 3712ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3000/signalk-usage/assets/icons/signalk-usage-icon_72x72.png:0
|
package/dist/routes.js
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { Router } from "express";
|
|
2
|
-
import { openapi } from "@neaps/api";
|
|
3
|
-
/**
|
|
4
|
-
* Create Express routes that implement a @neaps/api-compatible API
|
|
5
|
-
* backed by the given tide provider.
|
|
6
|
-
*/
|
|
7
|
-
export function createAdapterRoutes(provider) {
|
|
8
|
-
const router = Router();
|
|
9
|
-
router.get("/", (req, res) => {
|
|
10
|
-
res.json({
|
|
11
|
-
name: "Tides API",
|
|
12
|
-
version: openapi.info.version,
|
|
13
|
-
docs: `${req.baseUrl}/openapi.json`,
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
router.get("/openapi.json", (req, res) => {
|
|
17
|
-
res.json({ ...openapi, servers: [{ url: req.baseUrl || "/" }] });
|
|
18
|
-
});
|
|
19
|
-
router.get("/extremes", async (req, res) => {
|
|
20
|
-
const { latitude, longitude } = req.query;
|
|
21
|
-
if (!latitude || !longitude) {
|
|
22
|
-
return res
|
|
23
|
-
.status(400)
|
|
24
|
-
.json({ message: "latitude and longitude are required" });
|
|
25
|
-
}
|
|
26
|
-
try {
|
|
27
|
-
const result = await provider.getExtremesPrediction(parsePredictionOptions(req.query));
|
|
28
|
-
res.json({
|
|
29
|
-
...result,
|
|
30
|
-
extremes: result.extremes.map(serializeExtreme),
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
res.status(500).json({ message: error.message });
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
router.get("/timeline", async (req, res) => {
|
|
38
|
-
const { latitude, longitude } = req.query;
|
|
39
|
-
if (!latitude || !longitude) {
|
|
40
|
-
return res
|
|
41
|
-
.status(400)
|
|
42
|
-
.json({ message: "latitude and longitude are required" });
|
|
43
|
-
}
|
|
44
|
-
try {
|
|
45
|
-
const result = await provider.getTimelinePrediction(parsePredictionOptions(req.query));
|
|
46
|
-
res.json({
|
|
47
|
-
...result,
|
|
48
|
-
timeline: result.timeline.map(serializeTimelineEntry),
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
res.status(500).json({ message: error.message });
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
// Station endpoints — delegate to provider if supported
|
|
56
|
-
router.get("/stations", async (req, res) => {
|
|
57
|
-
const { query, latitude, longitude, maxResults, maxDistance, bbox } = req.query;
|
|
58
|
-
try {
|
|
59
|
-
if (query && provider.searchStations) {
|
|
60
|
-
const results = await provider.searchStations(String(query), {
|
|
61
|
-
maxResults: maxResults ? Number(maxResults) : undefined,
|
|
62
|
-
});
|
|
63
|
-
return res.json(results);
|
|
64
|
-
}
|
|
65
|
-
if (bbox && provider.stationsWithin) {
|
|
66
|
-
const parts = String(bbox)
|
|
67
|
-
.split(",")
|
|
68
|
-
.map(Number);
|
|
69
|
-
return res.json(await provider.stationsWithin(parts));
|
|
70
|
-
}
|
|
71
|
-
if (latitude && longitude && provider.stationsNear) {
|
|
72
|
-
const results = await provider.stationsNear({
|
|
73
|
-
latitude: Number(latitude),
|
|
74
|
-
longitude: Number(longitude),
|
|
75
|
-
maxResults: maxResults ? Number(maxResults) : undefined,
|
|
76
|
-
maxDistance: maxDistance ? Number(maxDistance) : undefined,
|
|
77
|
-
});
|
|
78
|
-
return res.json(results);
|
|
79
|
-
}
|
|
80
|
-
if (provider.stations) {
|
|
81
|
-
return res.json(await provider.stations());
|
|
82
|
-
}
|
|
83
|
-
res.status(501).json({
|
|
84
|
-
message: "Station discovery is not supported by this data source",
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
res.status(500).json({ message: error.message });
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
router.get("/stations/:source/:id", async (req, res) => {
|
|
92
|
-
if (!provider.findStation) {
|
|
93
|
-
return res.status(501).json({
|
|
94
|
-
message: "Station lookup is not supported by this data source",
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
const station = await provider.findStation(`${req.params.source}/${req.params.id}`);
|
|
99
|
-
res.json(station);
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
res.status(404).json({ message: error.message });
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
router.get("/stations/:source/:id/extremes", async (req, res) => {
|
|
106
|
-
if (!provider.getStationExtremes) {
|
|
107
|
-
return res.status(501).json({
|
|
108
|
-
message: "Station predictions are not supported by this data source",
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
|
-
const result = await provider.getStationExtremes(`${req.params.source}/${req.params.id}`, parseTimeOptions(req.query));
|
|
113
|
-
res.json({
|
|
114
|
-
...result,
|
|
115
|
-
extremes: result.extremes.map(serializeExtreme),
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
res.status(404).json({ message: error.message });
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
router.get("/stations/:source/:id/timeline", async (req, res) => {
|
|
123
|
-
if (!provider.getStationTimeline) {
|
|
124
|
-
return res.status(501).json({
|
|
125
|
-
message: "Station predictions are not supported by this data source",
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
try {
|
|
129
|
-
const result = await provider.getStationTimeline(`${req.params.source}/${req.params.id}`, parseTimeOptions(req.query));
|
|
130
|
-
res.json({
|
|
131
|
-
...result,
|
|
132
|
-
timeline: result.timeline.map(serializeTimelineEntry),
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
res.status(404).json({ message: error.message });
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
return router;
|
|
140
|
-
}
|
|
141
|
-
function parseTimeOptions(query) {
|
|
142
|
-
const { start, end, datum, units } = query;
|
|
143
|
-
return {
|
|
144
|
-
start: start ? new Date(String(start)) : new Date(),
|
|
145
|
-
end: end
|
|
146
|
-
? new Date(String(end))
|
|
147
|
-
: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
148
|
-
...(datum ? { datum: String(datum) } : {}),
|
|
149
|
-
...(units ? { units: String(units) } : {}),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
function parsePredictionOptions(query) {
|
|
153
|
-
const { latitude, longitude } = query;
|
|
154
|
-
return {
|
|
155
|
-
latitude: Number(latitude),
|
|
156
|
-
longitude: Number(longitude),
|
|
157
|
-
...parseTimeOptions(query),
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
/** Serialize an Extreme's Date to ISO string for JSON response */
|
|
161
|
-
function serializeExtreme(e) {
|
|
162
|
-
return {
|
|
163
|
-
time: e.time.toISOString(),
|
|
164
|
-
level: e.level,
|
|
165
|
-
high: e.high,
|
|
166
|
-
low: e.low,
|
|
167
|
-
label: e.label,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
/** Serialize a TimelineEntry's Date to ISO string for JSON response */
|
|
171
|
-
function serializeTimelineEntry(e) {
|
|
172
|
-
return {
|
|
173
|
-
time: e.time.toISOString(),
|
|
174
|
-
level: e.level,
|
|
175
|
-
};
|
|
176
|
-
}
|
package/dist/sources/index.js
DELETED
package/dist/sources/neaps.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { getExtremesPrediction, getTimelinePrediction, findStation, stationsNear, } from "neaps";
|
|
2
|
-
import { stations, search, bbox } from "@neaps/tide-database";
|
|
3
|
-
function stripStationDetails(station) {
|
|
4
|
-
const { id, name, region, country, continent, latitude, longitude, timezone, type } = station;
|
|
5
|
-
return { id, name, region, country, continent, latitude, longitude, timezone, type };
|
|
6
|
-
}
|
|
7
|
-
export default function (app) {
|
|
8
|
-
return {
|
|
9
|
-
id: "neaps",
|
|
10
|
-
title: "Neaps",
|
|
11
|
-
properties: {},
|
|
12
|
-
start() {
|
|
13
|
-
app.debug("Using Neaps");
|
|
14
|
-
const provider = {
|
|
15
|
-
getExtremesPrediction(options) {
|
|
16
|
-
return getExtremesPrediction(options);
|
|
17
|
-
},
|
|
18
|
-
getTimelinePrediction(options) {
|
|
19
|
-
return getTimelinePrediction(options);
|
|
20
|
-
},
|
|
21
|
-
findStation(id) {
|
|
22
|
-
return findStation(id);
|
|
23
|
-
},
|
|
24
|
-
searchStations(query, options) {
|
|
25
|
-
const limit = options?.maxResults ?? 10;
|
|
26
|
-
return search(query).slice(0, limit).map(stripStationDetails);
|
|
27
|
-
},
|
|
28
|
-
stationsNear(options) {
|
|
29
|
-
return stationsNear(options);
|
|
30
|
-
},
|
|
31
|
-
stationsWithin(box) {
|
|
32
|
-
return bbox(box).map(stripStationDetails);
|
|
33
|
-
},
|
|
34
|
-
stations() {
|
|
35
|
-
return stations.map(stripStationDetails);
|
|
36
|
-
},
|
|
37
|
-
getStationExtremes(id, options) {
|
|
38
|
-
return findStation(id).getExtremesPrediction(options);
|
|
39
|
-
},
|
|
40
|
-
getStationTimeline(id, options) {
|
|
41
|
-
return findStation(id).getTimelinePrediction(options);
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
return provider;
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
}
|
package/dist/sources/noaa.js
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import { getDistance } from "geolib";
|
|
3
|
-
import makeFetchHappen from "make-fetch-happen";
|
|
4
|
-
import FileCache from "../cache.js";
|
|
5
|
-
const mdapiUrl = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi";
|
|
6
|
-
const stationsUrl = `${mdapiUrl}/stations.json?type=tidepredictions`;
|
|
7
|
-
const dataGetterUrl = "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter";
|
|
8
|
-
// Datum names NOAA's datagetter accepts as a `datum` parameter for predictions.
|
|
9
|
-
// The MDAPI datums endpoint also returns ranges/inequalities/intervals
|
|
10
|
-
// (GT, MN, DHQ, DLQ, HWI, LWI) that are not valid prediction datums, so we
|
|
11
|
-
// filter to this allowlist before offering them as options.
|
|
12
|
-
const PREDICTION_DATUMS = new Set([
|
|
13
|
-
"STND",
|
|
14
|
-
"MHHW",
|
|
15
|
-
"MHW",
|
|
16
|
-
"MTL",
|
|
17
|
-
"MSL",
|
|
18
|
-
"MLW",
|
|
19
|
-
"MLLW",
|
|
20
|
-
"NAVD",
|
|
21
|
-
"CRD",
|
|
22
|
-
"IGLD",
|
|
23
|
-
"LWD",
|
|
24
|
-
]);
|
|
25
|
-
function formatDate(date) {
|
|
26
|
-
return date.toISOString().slice(0, 10).replace(/-/g, "");
|
|
27
|
-
}
|
|
28
|
-
function toStation(s) {
|
|
29
|
-
return {
|
|
30
|
-
id: `noaa/${s.id}`,
|
|
31
|
-
name: s.name,
|
|
32
|
-
latitude: s.lat,
|
|
33
|
-
longitude: s.lng,
|
|
34
|
-
region: s.state,
|
|
35
|
-
country: "US",
|
|
36
|
-
continent: "North America",
|
|
37
|
-
// We request predictions with time_zone=gmt and return UTC timestamps.
|
|
38
|
-
timezone: "UTC",
|
|
39
|
-
type: s.type === "R" ? "reference" : "subordinate",
|
|
40
|
-
source: {
|
|
41
|
-
id: "noaa",
|
|
42
|
-
name: "NOAA CO-OPS",
|
|
43
|
-
url: "https://tidesandcurrents.noaa.gov",
|
|
44
|
-
},
|
|
45
|
-
license: {
|
|
46
|
-
type: "public-domain",
|
|
47
|
-
commercial_use: true,
|
|
48
|
-
url: "https://tidesandcurrents.noaa.gov/disclaimers.html",
|
|
49
|
-
},
|
|
50
|
-
// NOAA's station list doesn't expose datum offsets or harmonics; we only
|
|
51
|
-
// serve MLLW predictions, so there's nothing to populate here.
|
|
52
|
-
datums: {},
|
|
53
|
-
defaultDatum: "MLLW",
|
|
54
|
-
harmonic_constituents: [],
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
function toStationSummary(s) {
|
|
58
|
-
return {
|
|
59
|
-
id: `noaa/${s.id}`,
|
|
60
|
-
name: s.name,
|
|
61
|
-
latitude: s.lat,
|
|
62
|
-
longitude: s.lng,
|
|
63
|
-
region: s.state,
|
|
64
|
-
country: "US",
|
|
65
|
-
type: s.type === "R" ? "reference" : "subordinate",
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
function toExtreme(p) {
|
|
69
|
-
const high = p.type === "H";
|
|
70
|
-
return {
|
|
71
|
-
time: new Date(`${p.t}Z`),
|
|
72
|
-
level: Number(p.v),
|
|
73
|
-
high,
|
|
74
|
-
low: !high,
|
|
75
|
-
label: high ? "High" : "Low",
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
export default function (app) {
|
|
79
|
-
return {
|
|
80
|
-
id: "noaa",
|
|
81
|
-
title: "NOAA (US only)",
|
|
82
|
-
async start(props) {
|
|
83
|
-
const fetch = props?._fetch ?? makeFetchHappen.defaults({
|
|
84
|
-
cachePath: path.join(app.getDataDirPath(), "noaa", "http-cache"),
|
|
85
|
-
});
|
|
86
|
-
const allStations = await loadStations(fetch, app);
|
|
87
|
-
// Datums rarely change, so persist them per-station across restarts
|
|
88
|
-
// (NOAA sends no cache headers, so make-fetch-happen won't cache them).
|
|
89
|
-
// The in-memory promise map also de-dupes concurrent/repeat lookups.
|
|
90
|
-
const datumsCache = new FileCache(path.join(app.getDataDirPath(), "noaa", "datums"));
|
|
91
|
-
const datumsInFlight = new Map();
|
|
92
|
-
function fetchDatums(stationId) {
|
|
93
|
-
let pending = datumsInFlight.get(stationId);
|
|
94
|
-
if (!pending) {
|
|
95
|
-
pending = loadDatums(stationId);
|
|
96
|
-
datumsInFlight.set(stationId, pending);
|
|
97
|
-
}
|
|
98
|
-
return pending;
|
|
99
|
-
}
|
|
100
|
-
async function loadDatums(stationId) {
|
|
101
|
-
const cached = (await datumsCache.get(stationId));
|
|
102
|
-
if (cached)
|
|
103
|
-
return cached;
|
|
104
|
-
let res;
|
|
105
|
-
try {
|
|
106
|
-
res = await fetch(`${mdapiUrl}/stations/${stationId}/datums.json`);
|
|
107
|
-
}
|
|
108
|
-
catch (e) {
|
|
109
|
-
// Network error — don't cache, so it's retried next time.
|
|
110
|
-
app.debug(`NOAA: failed to fetch datums for ${stationId}: ${e}`);
|
|
111
|
-
return {};
|
|
112
|
-
}
|
|
113
|
-
if (!res.ok)
|
|
114
|
-
return {};
|
|
115
|
-
const body = (await res.json());
|
|
116
|
-
const datums = {};
|
|
117
|
-
for (const d of body.datums ?? []) {
|
|
118
|
-
if (PREDICTION_DATUMS.has(d.name))
|
|
119
|
-
datums[d.name] = d.value;
|
|
120
|
-
}
|
|
121
|
-
// Cache the parsed result (including a legitimately empty set) so we
|
|
122
|
-
// don't re-request stations that have no published datums.
|
|
123
|
-
await datumsCache.set(stationId, datums);
|
|
124
|
-
return datums;
|
|
125
|
-
}
|
|
126
|
-
// Build a full Station, including the datums that power the datum
|
|
127
|
-
// selector in the UI. Used by the station-detail endpoints; the
|
|
128
|
-
// position-based predictions reuse it so the embedded station is
|
|
129
|
-
// consistent. Repeat lookups are served from cache.
|
|
130
|
-
async function buildStation(s, distance) {
|
|
131
|
-
const station = toStation(s);
|
|
132
|
-
station.datums = await fetchDatums(s.id);
|
|
133
|
-
if (distance !== undefined)
|
|
134
|
-
station.distance = distance;
|
|
135
|
-
return station;
|
|
136
|
-
}
|
|
137
|
-
async function fetchPredictions(stationId, options, interval) {
|
|
138
|
-
const endpoint = new URL(dataGetterUrl);
|
|
139
|
-
endpoint.search = new URLSearchParams({
|
|
140
|
-
product: "predictions",
|
|
141
|
-
application: "signalk.org/node-server",
|
|
142
|
-
begin_date: formatDate(options.start),
|
|
143
|
-
end_date: formatDate(options.end),
|
|
144
|
-
datum: options.datum ?? "MLLW",
|
|
145
|
-
station: stationId,
|
|
146
|
-
time_zone: "gmt",
|
|
147
|
-
units: options.units === "feet" ? "english" : "metric",
|
|
148
|
-
interval,
|
|
149
|
-
format: "json",
|
|
150
|
-
}).toString();
|
|
151
|
-
app.debug(`Fetching tides from NOAA: ${endpoint}`);
|
|
152
|
-
const res = await fetch(endpoint.toString());
|
|
153
|
-
if (!res.ok)
|
|
154
|
-
throw new Error("Failed to fetch NOAA tides: " + res.statusText);
|
|
155
|
-
const body = (await res.json());
|
|
156
|
-
if (body.error)
|
|
157
|
-
throw new Error(body.error.message);
|
|
158
|
-
return body.predictions;
|
|
159
|
-
}
|
|
160
|
-
function closestStation(position) {
|
|
161
|
-
return nearStations(position, 1)[0];
|
|
162
|
-
}
|
|
163
|
-
function nearStations(position, limit = 10, maxDistance) {
|
|
164
|
-
const withDist = allStations.map((s) => ({
|
|
165
|
-
...s,
|
|
166
|
-
distance: getDistance(position, {
|
|
167
|
-
latitude: s.lat,
|
|
168
|
-
longitude: s.lng,
|
|
169
|
-
}),
|
|
170
|
-
}));
|
|
171
|
-
let sorted = withDist.sort((a, b) => a.distance - b.distance);
|
|
172
|
-
if (maxDistance) {
|
|
173
|
-
sorted = sorted.filter((s) => s.distance <= maxDistance);
|
|
174
|
-
}
|
|
175
|
-
return sorted.slice(0, limit);
|
|
176
|
-
}
|
|
177
|
-
function findNoaaStation(id) {
|
|
178
|
-
const rawId = id.replace(/^noaa\//, "");
|
|
179
|
-
const found = allStations.find((s) => s.id === rawId);
|
|
180
|
-
if (!found)
|
|
181
|
-
throw new Error(`Station not found: ${id}`);
|
|
182
|
-
return found;
|
|
183
|
-
}
|
|
184
|
-
const provider = {
|
|
185
|
-
async getExtremesPrediction(options) {
|
|
186
|
-
const nearest = closestStation(options);
|
|
187
|
-
const predictions = await fetchPredictions(nearest.id, options, "hilo");
|
|
188
|
-
return {
|
|
189
|
-
datum: options.datum ?? "MLLW",
|
|
190
|
-
units: options.units ?? "meters",
|
|
191
|
-
station: await buildStation(nearest, nearest.distance),
|
|
192
|
-
distance: nearest.distance,
|
|
193
|
-
extremes: predictions.map(toExtreme),
|
|
194
|
-
};
|
|
195
|
-
},
|
|
196
|
-
async getTimelinePrediction(options) {
|
|
197
|
-
const nearest = closestStation(options);
|
|
198
|
-
const predictions = await fetchPredictions(nearest.id, options, "10");
|
|
199
|
-
return {
|
|
200
|
-
datum: options.datum ?? "MLLW",
|
|
201
|
-
units: options.units ?? "meters",
|
|
202
|
-
station: await buildStation(nearest, nearest.distance),
|
|
203
|
-
distance: nearest.distance,
|
|
204
|
-
timeline: predictions.map((p) => ({
|
|
205
|
-
time: new Date(`${p.t}Z`),
|
|
206
|
-
level: Number(p.v),
|
|
207
|
-
})),
|
|
208
|
-
};
|
|
209
|
-
},
|
|
210
|
-
findStation(id) {
|
|
211
|
-
return buildStation(findNoaaStation(id));
|
|
212
|
-
},
|
|
213
|
-
searchStations(query, options) {
|
|
214
|
-
const q = query.toLowerCase();
|
|
215
|
-
const limit = options?.maxResults ?? 10;
|
|
216
|
-
return allStations
|
|
217
|
-
.filter((s) => s.name.toLowerCase().includes(q) || s.id.includes(q))
|
|
218
|
-
.slice(0, limit)
|
|
219
|
-
.map(toStationSummary);
|
|
220
|
-
},
|
|
221
|
-
stationsNear(options) {
|
|
222
|
-
return nearStations(options, options.maxResults, options.maxDistance).map((s) => ({
|
|
223
|
-
...toStation(s),
|
|
224
|
-
distance: s.distance,
|
|
225
|
-
}));
|
|
226
|
-
},
|
|
227
|
-
stations() {
|
|
228
|
-
return allStations.map(toStationSummary);
|
|
229
|
-
},
|
|
230
|
-
async getStationExtremes(id, options) {
|
|
231
|
-
const noaaStation = findNoaaStation(id);
|
|
232
|
-
const predictions = await fetchPredictions(noaaStation.id, options, "hilo");
|
|
233
|
-
return {
|
|
234
|
-
datum: options.datum ?? "MLLW",
|
|
235
|
-
units: options.units ?? "meters",
|
|
236
|
-
station: await buildStation(noaaStation),
|
|
237
|
-
extremes: predictions.map(toExtreme),
|
|
238
|
-
};
|
|
239
|
-
},
|
|
240
|
-
async getStationTimeline(id, options) {
|
|
241
|
-
const noaaStation = findNoaaStation(id);
|
|
242
|
-
const predictions = await fetchPredictions(noaaStation.id, options, "10");
|
|
243
|
-
return {
|
|
244
|
-
datum: options.datum ?? "MLLW",
|
|
245
|
-
units: options.units ?? "meters",
|
|
246
|
-
station: await buildStation(noaaStation),
|
|
247
|
-
timeline: predictions.map((p) => ({
|
|
248
|
-
time: new Date(`${p.t}Z`),
|
|
249
|
-
level: Number(p.v),
|
|
250
|
-
})),
|
|
251
|
-
};
|
|
252
|
-
},
|
|
253
|
-
};
|
|
254
|
-
return provider;
|
|
255
|
-
},
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
async function loadStations(fetch, app) {
|
|
259
|
-
app.debug("NOAA: Loading tide stations");
|
|
260
|
-
const res = await fetch(stationsUrl);
|
|
261
|
-
if (!res.ok)
|
|
262
|
-
throw new Error(`Failed to download stations: ${res.statusText}`);
|
|
263
|
-
const data = (await res.json());
|
|
264
|
-
app.debug(`NOAA: Loaded ${data.stations.length} stations`);
|
|
265
|
-
return data.stations;
|
|
266
|
-
}
|