@signalk/tracks-plugin 3.0.0-beta.6 → 3.0.0-beta.7
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 +7 -0
- package/dist/index.js +59 -9
- package/dist/index.js.map +1 -1
- package/dist/timeWindow.d.ts +1 -1
- package/dist/utils.d.ts +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,13 @@ _`timespan` and `timespanOffset` are also accepted for Freeboard-SK compatibilit
|
|
|
106
106
|
`timespan=23h×panOffset=1` means "23 hours ending an hour ago". They are not part of
|
|
107
107
|
the proposed track API and are expected to be superseded by `from`/`to`._
|
|
108
108
|
|
|
109
|
+
_A parameter these routes do not read is rejected with `400` rather than ignored, so a
|
|
110
|
+
typo such as `?duratoin=6h` fails rather than quietly returning the whole retained track.
|
|
111
|
+
`bbox` and `radius` are **not** accepted here: they select which vessels to return and so
|
|
112
|
+
belong to `/tracks` below. To ask whether one vessel's track passed through a box, use the
|
|
113
|
+
v2 Track API, which matches any position in the window ([being designed in
|
|
114
|
+
SignalK/signalk-server#2504](https://github.com/SignalK/signalk-server/issues/2504))._
|
|
115
|
+
|
|
109
116
|
---
|
|
110
117
|
|
|
111
118
|
**Reduce the number of points returned:**
|
package/dist/index.js
CHANGED
|
@@ -135,6 +135,20 @@ function nearestLongitude(lng, origin) {
|
|
|
135
135
|
* `resolution` is shared by both and expresses the minimum spacing between
|
|
136
136
|
* returned points, thinning the track for wide time ranges.
|
|
137
137
|
*/
|
|
138
|
+
/**
|
|
139
|
+
* The query keys these routes read. Kept beside the schema so the two cannot
|
|
140
|
+
* drift: a key added to one without the other either fails validation or is
|
|
141
|
+
* rejected as unknown.
|
|
142
|
+
*/
|
|
143
|
+
var QUERY_KEYS = [
|
|
144
|
+
"from",
|
|
145
|
+
"to",
|
|
146
|
+
"duration",
|
|
147
|
+
"timespan",
|
|
148
|
+
"timespanOffset",
|
|
149
|
+
"resolution",
|
|
150
|
+
"times"
|
|
151
|
+
];
|
|
138
152
|
var QuerySchema = Type.Object({
|
|
139
153
|
from: Type.Optional(Type.String()),
|
|
140
154
|
to: Type.Optional(Type.String()),
|
|
@@ -200,7 +214,12 @@ function firstString(value) {
|
|
|
200
214
|
* `timespan`/`timespanOffset` pair. Absent all of them, no window is applied
|
|
201
215
|
* and the whole retained track is returned, as before.
|
|
202
216
|
*/
|
|
203
|
-
function parseTrackQuery(query, now = Date.now()) {
|
|
217
|
+
function parseTrackQuery(query, now = Date.now(), alsoAllowed = []) {
|
|
218
|
+
const unknown = Object.keys(query).filter((key) => !QUERY_KEYS.includes(key) && !alsoAllowed.includes(key));
|
|
219
|
+
if (unknown.length > 0) {
|
|
220
|
+
const allowed = [...QUERY_KEYS, ...alsoAllowed].join(", ");
|
|
221
|
+
throw new TimeWindowError(`Unknown query parameter${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")}; accepted: ${allowed}`);
|
|
222
|
+
}
|
|
204
223
|
const raw = {
|
|
205
224
|
from: firstString(query.from),
|
|
206
225
|
to: firstString(query.to),
|
|
@@ -411,21 +430,44 @@ function toFiniteNumber(value) {
|
|
|
411
430
|
const parsed = Number(trimmed);
|
|
412
431
|
return Number.isFinite(parsed) ? parsed : void 0;
|
|
413
432
|
}
|
|
433
|
+
/**
|
|
434
|
+
* A query parameter was present but malformed.
|
|
435
|
+
*
|
|
436
|
+
* Distinct from absent: a filter nobody asked for is not an error, but one
|
|
437
|
+
* that cannot be honoured must not quietly widen the query. Carries its own
|
|
438
|
+
* message so the handler can hand it to the client, the way `TimeWindowError`
|
|
439
|
+
* already does for the time window.
|
|
440
|
+
*/
|
|
441
|
+
var QueryParameterError = class extends Error {};
|
|
442
|
+
/**
|
|
443
|
+
* A well-formed query the server cannot currently answer.
|
|
444
|
+
*
|
|
445
|
+
* `radius` measures from the own vessel, so it needs a self position. Not
|
|
446
|
+
* having one is an outage, not a client mistake -- the same distinction the
|
|
447
|
+
* single-vessel route already draws for a history provider it cannot read.
|
|
448
|
+
*/
|
|
449
|
+
var SelfPositionUnavailableError = class extends Error {};
|
|
414
450
|
function validateParameters(params, defaultMaxRadius) {
|
|
415
451
|
let bbox = null;
|
|
416
452
|
const rawBbox = firstValue(params.bbox);
|
|
417
453
|
if (rawBbox !== void 0) {
|
|
418
454
|
const b = rawBbox.split(",").map(toFiniteNumber);
|
|
419
455
|
const [west, south, east, north] = b;
|
|
420
|
-
if (b.length
|
|
456
|
+
if (b.length !== 4 || west === void 0 || south === void 0 || east === void 0 || north === void 0) throw new QueryParameterError("bbox must be four comma-separated numbers: west,south,east,north");
|
|
457
|
+
if (south > north) throw new QueryParameterError(`bbox south (${south}) must not be greater than north (${north})`);
|
|
458
|
+
bbox = {
|
|
421
459
|
sw: [south, west],
|
|
422
460
|
ne: [north, east]
|
|
423
461
|
};
|
|
424
462
|
}
|
|
425
463
|
let radius = null;
|
|
426
464
|
const rawRadius = firstValue(params.radius);
|
|
427
|
-
if (rawRadius !== void 0)
|
|
428
|
-
|
|
465
|
+
if (rawRadius !== void 0) {
|
|
466
|
+
const parsed = toFiniteNumber(rawRadius);
|
|
467
|
+
if (parsed === void 0) throw new QueryParameterError(`radius must be a number in meters, got '${rawRadius}'`);
|
|
468
|
+
if (parsed < 0) throw new QueryParameterError(`radius must not be negative, got ${parsed}`);
|
|
469
|
+
radius = parsed;
|
|
470
|
+
} else if (defaultMaxRadius) radius = defaultMaxRadius;
|
|
429
471
|
return {
|
|
430
472
|
bbox,
|
|
431
473
|
radius
|
|
@@ -463,7 +505,7 @@ function createMatcher(params, selfPosition, debug) {
|
|
|
463
505
|
const inBounds = createInBounds(params.bbox);
|
|
464
506
|
return params.intersects ? (track) => track.some(inBounds) : (track) => inBounds(lastPoint(track));
|
|
465
507
|
} else if (params.radius !== null) {
|
|
466
|
-
if (!selfPosition) throw new
|
|
508
|
+
if (!selfPosition) throw new SelfPositionUnavailableError("No self position to calculate radius values");
|
|
467
509
|
const radius = params.radius;
|
|
468
510
|
const distanceFromSelf = createDistanceTo(selfPosition, debug);
|
|
469
511
|
return (track) => distanceFromSelf(lastPoint(track)) < radius;
|
|
@@ -13667,6 +13709,8 @@ function ThePlugin(app) {
|
|
|
13667
13709
|
router.get("/vessels/:vesselId/track.gpx", gpxHandler((req) => resolveContext(String(req.params.vesselId), app.selfContext)));
|
|
13668
13710
|
router.get("/self/track.gpx", gpxHandler(() => app.selfContext));
|
|
13669
13711
|
router.get("/self/track", singleTrackHandler(() => app.selfContext));
|
|
13712
|
+
/** Only the collection route filters spatially; see AGENTS.md on v1 vs v2. */
|
|
13713
|
+
const SPATIAL_PARAMS = ["bbox", "radius"];
|
|
13670
13714
|
const allTracksHandler = (req, res) => {
|
|
13671
13715
|
app.debug(req.query);
|
|
13672
13716
|
if (!tracks) {
|
|
@@ -13674,14 +13718,15 @@ function ThePlugin(app) {
|
|
|
13674
13718
|
return;
|
|
13675
13719
|
}
|
|
13676
13720
|
let query;
|
|
13721
|
+
let params;
|
|
13677
13722
|
try {
|
|
13678
|
-
query = parseTrackQuery(req.query);
|
|
13723
|
+
query = parseTrackQuery(req.query, Date.now(), SPATIAL_PARAMS);
|
|
13724
|
+
params = validateParameters(req.query, void 0);
|
|
13679
13725
|
} catch (err) {
|
|
13680
13726
|
res.status(400);
|
|
13681
|
-
res.json({ message: err instanceof TimeWindowError ? err.message : "Invalid query parameters" });
|
|
13727
|
+
res.json({ message: err instanceof TimeWindowError || err instanceof QueryParameterError ? err.message : "Invalid query parameters" });
|
|
13682
13728
|
return;
|
|
13683
13729
|
}
|
|
13684
|
-
const params = validateParameters(req.query, void 0);
|
|
13685
13730
|
const selfPosition = getVesselPosition();
|
|
13686
13731
|
(query.times ? tracks.getFilteredTimedTracks(params, selfPosition, app.debug, query).then((tc) => Object.entries(tc).reduce((acc, [context, points]) => {
|
|
13687
13732
|
const segments = segment(points, segmentGap);
|
|
@@ -13703,7 +13748,12 @@ function ThePlugin(app) {
|
|
|
13703
13748
|
return acc;
|
|
13704
13749
|
}, {}))).then((trks) => {
|
|
13705
13750
|
res.json(trks);
|
|
13706
|
-
}).catch(() => {
|
|
13751
|
+
}).catch((err) => {
|
|
13752
|
+
if (err instanceof SelfPositionUnavailableError) {
|
|
13753
|
+
res.status(503);
|
|
13754
|
+
res.json({ message: "No position for the own vessel, so radius cannot be measured" });
|
|
13755
|
+
return;
|
|
13756
|
+
}
|
|
13707
13757
|
res.status(404);
|
|
13708
13758
|
res.json({ message: `No track available for vessels.` });
|
|
13709
13759
|
});
|