signalk-tides 2.2.0 → 2.3.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/.github/mise.node24.toml +3 -0
- package/.github/workflows/ci.yml +6 -0
- package/.github/workflows/lockfile.yml +31 -0
- package/.github/workflows/release.yml +16 -1
- package/CONTRIBUTING.md +11 -0
- package/README.md +3 -2
- package/dist/calculations.js +20 -0
- package/dist/index.js +201 -41
- package/dist/middleware.js +90 -65
- package/mise.toml +2 -0
- package/package.json +25 -24
- package/public/assets/main-FMwgC28o.js +1 -0
- package/public/assets/maplibre-CXEYa9hE.js +814 -0
- package/public/assets/maplibre-gl-worker-BWso-Bu4.js +10 -0
- package/public/assets/useUnitPreferences-Bnl2Hfv0.js +12 -0
- package/public/assets/useUnitPreferences-D6HIp3mv.css +1 -0
- package/public/assets/widget-DrHqs948.js +1 -0
- package/public/index.html +3 -3
- package/public/widget.html +3 -3
- package/tides-dev-server.ts +5 -5
- package/public/assets/main-68xVY5Ec.js +0 -1
- package/public/assets/maplibre-CcFST_Y2.js +0 -814
- package/public/assets/maplibre-gl-worker-CRiIRpYb.js +0 -15
- package/public/assets/useUnitPreferences-BoZImJ0r.css +0 -1
- package/public/assets/useUnitPreferences-lYEQOWom.js +0 -12
- package/public/assets/widget-DyJmPMCf.js +0 -1
package/dist/middleware.js
CHANGED
|
@@ -1,78 +1,103 @@
|
|
|
1
|
-
import { findStation, nearestStation } from "
|
|
1
|
+
import { findStation, nearestStation } from "slackwater";
|
|
2
|
+
import { MAX_STATION_KM, hasTideModel, isNoStationNearby, isStationNotFound, } from "./calculations.js";
|
|
3
|
+
const nearestToVessel = ({ latitude, longitude }) => nearestStation({
|
|
4
|
+
latitude,
|
|
5
|
+
longitude,
|
|
6
|
+
maxDistance: MAX_STATION_KM,
|
|
7
|
+
});
|
|
2
8
|
/**
|
|
3
9
|
* Middleware that resolves the vessel/default station alias and bare
|
|
4
10
|
* location-based queries (/extremes, /timeline) against the configured default
|
|
5
11
|
* station, falling back to the station nearest the vessel's position when no
|
|
6
|
-
* default is set.
|
|
12
|
+
* default is set or it can't be used.
|
|
7
13
|
*/
|
|
8
14
|
export function withVesselPosition(router, getPosition, getDefaultStationId) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (!
|
|
41
|
-
|
|
15
|
+
// signalk-server runs Express 4, which ignores a rejected handler and leaves
|
|
16
|
+
// the request hanging, so every failure has to reach next().
|
|
17
|
+
return async (req, res, next) => {
|
|
18
|
+
try {
|
|
19
|
+
// HTTP 303 See Other: the response can be found under a different URI
|
|
20
|
+
const redirect = (toPath) => res.redirect(303, `${req.baseUrl}${toPath}`);
|
|
21
|
+
// The configured default station, or null when unset, "auto", stale, or unpredictable.
|
|
22
|
+
const defaultStation = async () => {
|
|
23
|
+
const id = getDefaultStationId?.();
|
|
24
|
+
if (!id || id === "auto")
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const station = await findStation(id);
|
|
28
|
+
return hasTideModel(station) ? station : null;
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
// A stale or invalid saved id falls back to position; a database that
|
|
32
|
+
// failed to load has to reach next().
|
|
33
|
+
if (isStationNotFound(e))
|
|
34
|
+
return null;
|
|
35
|
+
throw e;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
// Redirect the vessel/default alias to the configured (or nearest) station.
|
|
39
|
+
if (/^\/stations\/vessel\/default(?:\/|$)/.test(req.path)) {
|
|
40
|
+
const toStation = (station) => redirect(req.url.replace(/^\/stations\/vessel\/default\b/, `/stations/${station.id}`));
|
|
41
|
+
// A configured default station resolves without a position
|
|
42
|
+
const station = await defaultStation();
|
|
43
|
+
if (station)
|
|
44
|
+
return toStation(station);
|
|
45
|
+
const pos = getPosition();
|
|
46
|
+
if (!pos) {
|
|
47
|
+
res.status(503).json({ message: "Vessel position not available" });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return toStation(await nearestToVessel(pos));
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
if (isNoStationNearby(e)) {
|
|
55
|
+
res.status(404).json({ message: "No nearby stations found" });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
res.status(500).json({ message: "Failed to find nearest station" });
|
|
59
|
+
return;
|
|
42
60
|
}
|
|
43
|
-
return toStation(nearest);
|
|
44
61
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
// Bare location-based queries: redirect to the configured station when set,
|
|
63
|
+
// otherwise inject the vessel position so slackwater uses the nearest station.
|
|
64
|
+
const isLocationQueryPath = req.path === "/extremes" || req.path === "/timeline";
|
|
65
|
+
if (isLocationQueryPath && !req.query.latitude && !req.query.longitude) {
|
|
66
|
+
const station = await defaultStation();
|
|
67
|
+
if (station) {
|
|
68
|
+
return redirect(req.url.replace(/^\/(extremes|timeline)\b/, `/stations/${station.id}/$1`));
|
|
69
|
+
}
|
|
70
|
+
const pos = getPosition();
|
|
71
|
+
if (pos) {
|
|
72
|
+
// The Slackwater route takes any distance, so check the range here
|
|
73
|
+
try {
|
|
74
|
+
await nearestToVessel(pos);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
if (!isNoStationNearby(e))
|
|
78
|
+
throw e;
|
|
79
|
+
res.status(404).json({ message: "No nearby stations found" });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Express 5 makes `req.query` a read-only getter, so assigning to its
|
|
83
|
+
// properties is silently lost. Shadow it with an own data property,
|
|
84
|
+
// which behaves identically on Express 4 and 5.
|
|
85
|
+
Object.defineProperty(req, "query", {
|
|
86
|
+
value: {
|
|
87
|
+
...req.query,
|
|
88
|
+
latitude: String(pos.latitude),
|
|
89
|
+
longitude: String(pos.longitude),
|
|
90
|
+
},
|
|
91
|
+
writable: true,
|
|
92
|
+
configurable: true,
|
|
93
|
+
enumerable: true,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
49
96
|
}
|
|
97
|
+
router(req, res, next);
|
|
50
98
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const isLocationQueryPath = req.path === "/extremes" || req.path === "/timeline";
|
|
54
|
-
if (isLocationQueryPath && !req.query.latitude && !req.query.longitude) {
|
|
55
|
-
const station = defaultStation();
|
|
56
|
-
if (station) {
|
|
57
|
-
return redirect(req.url.replace(/^\/(extremes|timeline)\b/, `/stations/${station.id}/$1`));
|
|
58
|
-
}
|
|
59
|
-
const pos = getPosition();
|
|
60
|
-
if (pos) {
|
|
61
|
-
// Express 5 makes `req.query` a read-only getter, so assigning to its
|
|
62
|
-
// properties is silently lost. Shadow it with an own data property,
|
|
63
|
-
// which behaves identically on Express 4 and 5.
|
|
64
|
-
Object.defineProperty(req, "query", {
|
|
65
|
-
value: {
|
|
66
|
-
...req.query,
|
|
67
|
-
latitude: String(pos.latitude),
|
|
68
|
-
longitude: String(pos.longitude),
|
|
69
|
-
},
|
|
70
|
-
writable: true,
|
|
71
|
-
configurable: true,
|
|
72
|
-
enumerable: true,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
next(error);
|
|
75
101
|
}
|
|
76
|
-
router(req, res, next);
|
|
77
102
|
};
|
|
78
103
|
}
|
package/mise.toml
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-tides",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Tidal predictions for the vessel's position, powered by
|
|
3
|
+
"version": "2.3.0",
|
|
4
|
+
"description": "Tidal predictions for the vessel's position, powered by Slackwater.",
|
|
5
|
+
"homepage": "https://openwaters.io/tides/slackwater/",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "./dist/index.js",
|
|
7
8
|
"exports": "./dist/index.js",
|
|
@@ -34,42 +35,42 @@
|
|
|
34
35
|
],
|
|
35
36
|
"license": "Apache-2.0",
|
|
36
37
|
"dependencies": {
|
|
37
|
-
"@
|
|
38
|
-
"@
|
|
39
|
-
"@
|
|
40
|
-
"express": "^5.
|
|
41
|
-
"neaps": "^0.8.0",
|
|
38
|
+
"@signalk/server-api": "^2.35.0",
|
|
39
|
+
"@slackwater/api": "^1.0.0-beta.4",
|
|
40
|
+
"@slackwater/react": "^1.0.0-beta.2",
|
|
41
|
+
"express": "^5.3.0",
|
|
42
42
|
"react": "^19.1.0",
|
|
43
|
-
"react-dom": "^19.1
|
|
44
|
-
"signalk-plotterext-bus": "^0.
|
|
43
|
+
"react-dom": "^19.3.1",
|
|
44
|
+
"signalk-plotterext-bus": "^0.24.0",
|
|
45
|
+
"slackwater": "^1.0.0-beta.4"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@eslint/js": "^9.21.0",
|
|
48
49
|
"@tailwindcss/vite": "^4.1.3",
|
|
49
50
|
"@testing-library/dom": "^10.4.1",
|
|
50
51
|
"@testing-library/react": "^16.3.2",
|
|
51
|
-
"@testing-library/user-event": "^14.6.
|
|
52
|
+
"@testing-library/user-event": "^14.6.8",
|
|
52
53
|
"@types/express": "^5.0.6",
|
|
53
|
-
"@types/node": "^26.
|
|
54
|
+
"@types/node": "^26.6.5",
|
|
54
55
|
"@types/react": "^19.0.12",
|
|
55
56
|
"@types/react-dom": "^19.0.4",
|
|
56
57
|
"@types/supertest": "^7.2.0",
|
|
57
|
-
"@vitejs/plugin-react": "^6.
|
|
58
|
-
"@vitest/browser": "5.0.
|
|
59
|
-
"@vitest/browser-playwright": "5.0.
|
|
60
|
-
"@vitest/ui": "5.0.
|
|
61
|
-
"jsdom": "^30.
|
|
62
|
-
"maplibre-gl": "^6.
|
|
63
|
-
"oxlint": "^1.
|
|
64
|
-
"pkg-pr-new": "^0.0.
|
|
65
|
-
"playwright": "^1.
|
|
58
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
59
|
+
"@vitest/browser": "5.0.5",
|
|
60
|
+
"@vitest/browser-playwright": "5.0.5",
|
|
61
|
+
"@vitest/ui": "5.0.5",
|
|
62
|
+
"jsdom": "^30.1.3",
|
|
63
|
+
"maplibre-gl": "^6.15.0",
|
|
64
|
+
"oxlint": "^1.89.0",
|
|
65
|
+
"pkg-pr-new": "^0.0.92",
|
|
66
|
+
"playwright": "^1.64.1",
|
|
66
67
|
"react-map-gl": "^8.1.0",
|
|
67
|
-
"signalk-server": "^2.
|
|
68
|
-
"supertest": "^7.
|
|
68
|
+
"signalk-server": "^2.35.1",
|
|
69
|
+
"supertest": "^7.3.1",
|
|
69
70
|
"tailwindcss": "^4.1.3",
|
|
70
71
|
"typescript": "^7.0.2",
|
|
71
|
-
"vite": "^8.
|
|
72
|
-
"vitest": "5.0.
|
|
72
|
+
"vite": "^8.3.5",
|
|
73
|
+
"vitest": "5.0.5"
|
|
73
74
|
},
|
|
74
75
|
"overrides": {
|
|
75
76
|
"vite-plus": {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as e,c as t,d as n,f as r,i,l as a,n as o,r as s,s as c,t as l,u}from"./useUnitPreferences-Bnl2Hfv0.js";var d=u(),f=r(n(),1),p=()=>new URLSearchParams(window.location.search).get(`station`)||`vessel/default`;function m(){let[e,t]=(0,f.useState)(p);return(0,f.useEffect)(()=>{let e=()=>t(p());return window.addEventListener(`popstate`,e),()=>window.removeEventListener(`popstate`,e)},[]),[e,(0,f.useCallback)(e=>{let n=e.trim()||`vessel/default`,r=new URL(window.location.href);n===`vessel/default`?r.searchParams.delete(`station`):r.searchParams.set(`station`,n),window.history.pushState(window.history.state,``,r),t(n)},[])]}var h=a(),g={version:8,sources:{osm:{type:`raster`,tiles:[`https://tile.openstreetmap.org/{z}/{x}/{y}.png`],tileSize:256,attribution:`© OpenStreetMap contributors`}},layers:[{id:`osm`,type:`raster`,source:`osm`}]};function _({stationId:n,onSelect:r}){let a=(0,f.useRef)(null),[s,c]=(0,f.useState)(!1),[l,u]=(0,f.useState)(!1),[d,p]=(0,f.useState)(`list`),{data:m,error:_}=t(n),v=()=>{u(window.matchMedia(`(min-width: 1024px)`).matches),p(`list`),c(!0),a.current?.showModal()},y=e=>{r(e.id),a.current?.close()};return(0,h.jsxs)(h.Fragment,{children:[(0,h.jsx)(`button`,{type:`button`,onClick:v,"aria-label":`Browse stations`,className:`absolute hidden min-[20rem]:block right-2 top-2 sm:right-4 sm:top-4 md:right-6 md:top-6 lg:right-8 lg:top-8 xl:right-20 xl:top-20 z-10 rounded-md border border-(--slackwater-border) bg-(--slackwater-bg-subtle) p-2 text-(--slackwater-text) hover:bg-(--slackwater-bg)`,children:(0,h.jsxs)(`svg`,{width:`20`,height:`20`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,"aria-hidden":`true`,children:[(0,h.jsx)(`line`,{x1:`3`,y1:`6`,x2:`21`,y2:`6`}),(0,h.jsx)(`line`,{x1:`3`,y1:`12`,x2:`21`,y2:`12`}),(0,h.jsx)(`line`,{x1:`3`,y1:`18`,x2:`21`,y2:`18`})]})}),(0,h.jsx)(`dialog`,{ref:a,onClose:()=>c(!1),onClick:e=>{e.target===e.currentTarget&&e.currentTarget.close()},className:`m-auto w-[min(92vw,900px)] max-h-[85vh] rounded-lg border border-(--slackwater-border) bg-(--slackwater-bg) p-0 text-(--slackwater-text) backdrop:bg-black/50`,children:s&&(0,h.jsxs)(`div`,{className:`flex max-h-[85vh] flex-col`,children:[(0,h.jsxs)(`div`,{className:`flex items-center justify-between border-b border-(--slackwater-border) p-3`,children:[(0,h.jsx)(`button`,{type:`button`,onClick:()=>a.current?.close(),"aria-label":`Close`,className:`grid size-11 place-items-center rounded-md text-(--slackwater-text-muted) hover:text-(--slackwater-text)`,children:`✕`}),(0,h.jsx)(`h2`,{className:`text-base font-semibold`,children:`Stations`}),(0,h.jsx)(`button`,{type:`button`,onClick:()=>p(d===`map`?`list`:`map`),"aria-label":d===`map`?`Show list`:`Show map`,className:`grid size-11 place-items-center rounded-md text-(--slackwater-text-muted) hover:text-(--slackwater-text) lg:invisible`,children:d===`map`?(0,h.jsxs)(`svg`,{width:`20`,height:`20`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,children:[(0,h.jsx)(`line`,{x1:`8`,y1:`6`,x2:`21`,y2:`6`}),(0,h.jsx)(`line`,{x1:`8`,y1:`12`,x2:`21`,y2:`12`}),(0,h.jsx)(`line`,{x1:`8`,y1:`18`,x2:`21`,y2:`18`}),(0,h.jsx)(`line`,{x1:`3`,y1:`6`,x2:`3.01`,y2:`6`}),(0,h.jsx)(`line`,{x1:`3`,y1:`12`,x2:`3.01`,y2:`12`}),(0,h.jsx)(`line`,{x1:`3`,y1:`18`,x2:`3.01`,y2:`18`})]}):(0,h.jsxs)(`svg`,{width:`20`,height:`20`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,children:[(0,h.jsx)(`polygon`,{points:`3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21 3 6`}),(0,h.jsx)(`line`,{x1:`9`,y1:`3`,x2:`9`,y2:`18`}),(0,h.jsx)(`line`,{x1:`15`,y1:`6`,x2:`15`,y2:`21`})]})})]}),(0,h.jsxs)(`div`,{className:`flex min-h-0 flex-col gap-4 p-3 lg:flex-row`,children:[(0,h.jsxs)(`div`,{className:`w-full flex-col gap-3 overflow-y-auto lg:flex lg:w-80 ${d===`map`?`hidden`:`flex`}`,children:[(0,h.jsx)(i,{onSelect:y}),(0,h.jsx)(`button`,{type:`button`,onClick:()=>y({id:`vessel/default`}),className:`rounded-md border border-(--slackwater-border) px-3 py-2 text-left text-sm hover:bg-(--slackwater-bg-subtle)`,children:`📍 Vessel position`}),(0,h.jsx)(o,{stationId:n,onStationSelect:y})]}),(l||d===`map`)&&(m||_)&&(0,h.jsx)(`div`,{className:`h-[60vh] overflow-hidden rounded-md border border-(--slackwater-border) lg:h-auto lg:min-h-100 lg:flex-1`,children:(0,h.jsx)(e,{mapStyle:g,initialViewState:m?{longitude:m.longitude,latitude:m.latitude,zoom:10}:{longitude:0,latitude:20,zoom:1},focusStation:m?.id,onStationSelect:y})})]})]})})]})}var{VITE_SIGNALK_URL:v=window.location.toString()}={BASE_URL:`./`,DEV:!1,MODE:`production`,PROD:!0,SSR:!1},y=new URL(`/signalk/v2/api`,v).toString();function b(){let{units:e,ready:t}=l(),[n,r]=m();return t?(0,h.jsx)(s,{baseUrl:y,units:e,children:(0,h.jsxs)(`div`,{className:`relative`,children:[(0,h.jsx)(_,{stationId:n,onSelect:r}),(0,h.jsx)(c,{id:n,className:`overflow-y-auto p-2 sm:p-4 md:p-6 lg:p-8 xl:p-20`})]})}):null}(0,d.createRoot)(document.getElementById(`app`)).render((0,h.jsx)(b,{}));
|