@signalk/tracks-plugin 3.0.0-beta.0 → 3.0.0-beta.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/app-icon.svg +42 -0
- package/dist/index.js +93 -8
- package/dist/index.js.map +1 -1
- package/dist/timeWindow.d.ts +18 -0
- package/dist/trackApi.d.ts +9 -7
- package/dist/types.d.ts +10 -0
- package/package.json +8 -2
package/app-icon.svg
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="Tracks">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="sea" x1="0" y1="0" x2="0" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#1b6ea8"/>
|
|
5
|
+
<stop offset="1" stop-color="#0b3a5e"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<clipPath id="card">
|
|
8
|
+
<rect x="0" y="0" width="128" height="128" rx="22"/>
|
|
9
|
+
</clipPath>
|
|
10
|
+
</defs>
|
|
11
|
+
|
|
12
|
+
<g clip-path="url(#card)">
|
|
13
|
+
<rect width="128" height="128" fill="url(#sea)"/>
|
|
14
|
+
|
|
15
|
+
<!-- Chart grid, as on the sibling plugins' icons. -->
|
|
16
|
+
<g stroke="#bee3f4" stroke-opacity="0.28" stroke-width="1" fill="none">
|
|
17
|
+
<path d="M0 32 H128 M0 64 H128 M0 96 H128"/>
|
|
18
|
+
<path d="M32 0 V128 M64 0 V128 M96 0 V128"/>
|
|
19
|
+
</g>
|
|
20
|
+
|
|
21
|
+
<!-- The track itself: the recorded path, drawn through its own points. -->
|
|
22
|
+
<path d="M24 102 C 40 94, 42 72, 58 64 S 84 58, 94 44"
|
|
23
|
+
fill="none" stroke="#0b3a5e" stroke-opacity="0.45"
|
|
24
|
+
stroke-width="9" stroke-linecap="round"/>
|
|
25
|
+
<path d="M24 102 C 40 94, 42 72, 58 64 S 84 58, 94 44"
|
|
26
|
+
fill="none" stroke="#ffd76a" stroke-width="5"
|
|
27
|
+
stroke-linecap="round" stroke-linejoin="round"/>
|
|
28
|
+
|
|
29
|
+
<!-- Position fixes. The track is a series of recorded points, not a
|
|
30
|
+
drawn line, and the icon should read that way at 32px too. -->
|
|
31
|
+
<g fill="#fffaf0" stroke="#0b3a5e" stroke-opacity="0.5" stroke-width="1.5">
|
|
32
|
+
<circle cx="24" cy="102" r="4.5"/>
|
|
33
|
+
<circle cx="49" cy="74" r="4"/>
|
|
34
|
+
<circle cx="72" cy="61" r="4"/>
|
|
35
|
+
</g>
|
|
36
|
+
|
|
37
|
+
<!-- The vessel, at the newest fix, heading along the track. -->
|
|
38
|
+
<path d="M94 34 L102 52 L94 47 L86 52 Z" fill="#fffaf0"
|
|
39
|
+
stroke="#0b3a5e" stroke-opacity="0.5" stroke-width="1.5"
|
|
40
|
+
stroke-linejoin="round"/>
|
|
41
|
+
</g>
|
|
42
|
+
</svg>
|
package/dist/index.js
CHANGED
|
@@ -161,6 +161,42 @@ function thin(points, resolution) {
|
|
|
161
161
|
return result;
|
|
162
162
|
}
|
|
163
163
|
/**
|
|
164
|
+
* Thin to at most `maxPoints`, widening the spacing until the budget fits.
|
|
165
|
+
*
|
|
166
|
+
* A budget, not a fidelity contract: the caller has said how many points it can
|
|
167
|
+
* afford to transfer and draw, whatever shape the track is. Returns the
|
|
168
|
+
* resolution actually applied so the response can report it — a client that is
|
|
169
|
+
* given 29 points instead of the 546 it would otherwise have received should be
|
|
170
|
+
* able to see the spacing that produced them.
|
|
171
|
+
*
|
|
172
|
+
* Widening by doubling from the span-derived estimate rather than binary
|
|
173
|
+
* searching: `thin` keeps the first point and the last unconditionally, so the
|
|
174
|
+
* count is not a clean function of the spacing and an exact solve would be
|
|
175
|
+
* false precision. Two or three passes settle it in practice.
|
|
176
|
+
*/
|
|
177
|
+
function thinToBudget(points, maxPoints, resolution) {
|
|
178
|
+
const start = thin(points, resolution);
|
|
179
|
+
if (start.length <= maxPoints) return {
|
|
180
|
+
points: start,
|
|
181
|
+
resolution
|
|
182
|
+
};
|
|
183
|
+
if (maxPoints < 2) return {
|
|
184
|
+
points: start.slice(0, Math.max(0, maxPoints)),
|
|
185
|
+
resolution
|
|
186
|
+
};
|
|
187
|
+
const span = start[start.length - 1].timestamp - start[0].timestamp;
|
|
188
|
+
let spacing = Math.max(1, Math.ceil(span / (maxPoints - 1)), resolution ?? 0);
|
|
189
|
+
let result = thin(points, spacing);
|
|
190
|
+
while (result.length > maxPoints && spacing <= span) {
|
|
191
|
+
spacing *= 2;
|
|
192
|
+
result = thin(points, spacing);
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
points: result,
|
|
196
|
+
resolution: spacing
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
164
200
|
* Split points into segments wherever recording stopped for longer than `gap`.
|
|
165
201
|
*
|
|
166
202
|
* A track is not one continuous line: a vessel that stops for the night, or
|
|
@@ -313,7 +349,7 @@ function resolveContext(vesselId, selfContext) {
|
|
|
313
349
|
function createMatcher(params, selfPosition, debug) {
|
|
314
350
|
if (params.bbox) {
|
|
315
351
|
const inBounds = createInBounds(params.bbox);
|
|
316
|
-
return (track) => inBounds(lastPoint(track));
|
|
352
|
+
return params.intersects ? (track) => track.some(inBounds) : (track) => inBounds(lastPoint(track));
|
|
317
353
|
} else if (params.radius !== null) {
|
|
318
354
|
if (!selfPosition) throw new Error("No self position to calculate radius values");
|
|
319
355
|
const radius = params.radius;
|
|
@@ -511,21 +547,33 @@ var totalMilliseconds = (duration) => duration.total({
|
|
|
511
547
|
unit: "milliseconds",
|
|
512
548
|
relativeTo: CALENDAR_REFERENCE
|
|
513
549
|
});
|
|
550
|
+
/**
|
|
551
|
+
* Milliseconds back to a Duration, for reporting the spacing applied.
|
|
552
|
+
*
|
|
553
|
+
* Exact, not rounded to something tidier. A budget-derived spacing lands on
|
|
554
|
+
* values like 24470ms, and `PT24.47S` does read as false precision — but the
|
|
555
|
+
* field says what was applied, and a client re-querying with a rounded `PT24S`
|
|
556
|
+
* gets 51 points where the budget it asked for was 50. Reproducibility beats
|
|
557
|
+
* tidiness.
|
|
558
|
+
*/
|
|
559
|
+
var msToDuration = (ms) => Temporal.Duration.from({
|
|
560
|
+
milliseconds: Math.trunc(ms),
|
|
561
|
+
nanoseconds: Math.round((ms - Math.trunc(ms)) * 1e6)
|
|
562
|
+
}).round({ largestUnit: "hour" });
|
|
514
563
|
var toLngLat$1 = ([lat, lng]) => [lng, lat];
|
|
515
564
|
/** Bounding box of the returned geometry, in GeoJSON order. */
|
|
516
565
|
var boundsOf = (points) => {
|
|
517
566
|
if (points.length === 0) return;
|
|
518
|
-
let west = points[0].position[1];
|
|
519
|
-
let east = west;
|
|
520
567
|
let south = points[0].position[0];
|
|
521
568
|
let north = south;
|
|
569
|
+
const longitudes = [];
|
|
522
570
|
for (const { position } of points) {
|
|
523
571
|
const [lat, lng] = position;
|
|
524
|
-
if (lng < west) west = lng;
|
|
525
|
-
if (lng > east) east = lng;
|
|
526
572
|
if (lat < south) south = lat;
|
|
527
573
|
if (lat > north) north = lat;
|
|
574
|
+
longitudes.push(lng);
|
|
528
575
|
}
|
|
576
|
+
const [west, east] = longitudeSpan(longitudes);
|
|
529
577
|
return [
|
|
530
578
|
west,
|
|
531
579
|
south,
|
|
@@ -533,6 +581,36 @@ var boundsOf = (points) => {
|
|
|
533
581
|
north
|
|
534
582
|
];
|
|
535
583
|
};
|
|
584
|
+
/**
|
|
585
|
+
* The narrower of the two longitude intervals covering these points.
|
|
586
|
+
*
|
|
587
|
+
* A plain min/max describes a track spanning the antimeridian as almost the
|
|
588
|
+
* whole globe: two fixes at 179 and -179 are two degrees apart, but min/max
|
|
589
|
+
* reports 358. RFC 7946 says a box crossing the antimeridian is written with
|
|
590
|
+
* west greater than east, so the wrap-around interval is expressible — pick it
|
|
591
|
+
* whenever it is the shorter of the two.
|
|
592
|
+
*
|
|
593
|
+
* The widest gap between adjacent longitudes is the part of the globe the track
|
|
594
|
+
* does not cover, so the complement of that gap is the tightest interval that
|
|
595
|
+
* does.
|
|
596
|
+
*/
|
|
597
|
+
var longitudeSpan = (longitudes) => {
|
|
598
|
+
const sorted = [...longitudes].sort((a, b) => a - b);
|
|
599
|
+
const min = sorted[0];
|
|
600
|
+
const max = sorted[sorted.length - 1];
|
|
601
|
+
let widestGap = 360 - (max - min);
|
|
602
|
+
let west = min;
|
|
603
|
+
let east = max;
|
|
604
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
605
|
+
const gap = sorted[i] - sorted[i - 1];
|
|
606
|
+
if (gap > widestGap) {
|
|
607
|
+
widestGap = gap;
|
|
608
|
+
west = sorted[i];
|
|
609
|
+
east = sorted[i - 1];
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return [west, east];
|
|
613
|
+
};
|
|
536
614
|
function createTrackProvider(deps) {
|
|
537
615
|
const matching = async (query) => {
|
|
538
616
|
const store = deps.store();
|
|
@@ -542,7 +620,8 @@ function createTrackProvider(deps) {
|
|
|
542
620
|
const wanted = query.contexts?.length ? new Set(query.contexts.map(resolveSelf(deps.selfContext()))) : void 0;
|
|
543
621
|
const collection = await store.getFilteredTimedTracks({
|
|
544
622
|
bbox: toGeoBounds(query.bbox),
|
|
545
|
-
radius: null
|
|
623
|
+
radius: null,
|
|
624
|
+
intersects: true
|
|
546
625
|
}, void 0, void 0, {
|
|
547
626
|
...window ? { window } : {},
|
|
548
627
|
...resolution === void 0 ? {} : { resolution }
|
|
@@ -560,8 +639,14 @@ function createTrackProvider(deps) {
|
|
|
560
639
|
const matched = await matching(query);
|
|
561
640
|
const gap = deps.segmentGap();
|
|
562
641
|
const selfContext = deps.selfContext();
|
|
642
|
+
const requested = query.resolution ? totalMilliseconds(query.resolution) : void 0;
|
|
563
643
|
const features = [];
|
|
564
|
-
for (const [context,
|
|
644
|
+
for (const [context, all] of matched) {
|
|
645
|
+
const { points, resolution: appliedMs } = query.maxPoints === void 0 ? {
|
|
646
|
+
points: all,
|
|
647
|
+
resolution: requested
|
|
648
|
+
} : thinToBudget(all, query.maxPoints, requested);
|
|
649
|
+
if (points.length === 0) continue;
|
|
565
650
|
const segments = segment(points, gap);
|
|
566
651
|
const bbox = boundsOf(points);
|
|
567
652
|
features.push({
|
|
@@ -577,7 +662,7 @@ function createTrackProvider(deps) {
|
|
|
577
662
|
to: new Date(points[points.length - 1].timestamp).toISOString(),
|
|
578
663
|
...bbox ? { bbox } : {},
|
|
579
664
|
pointCount: points.length,
|
|
580
|
-
...
|
|
665
|
+
...appliedMs === void 0 ? {} : { resolution: msToDuration(appliedMs).toString() },
|
|
581
666
|
...query.times ? { coordTimes: segments.map(toIsoTimes) } : {}
|
|
582
667
|
}
|
|
583
668
|
});
|