@signalk/tracks-plugin 2.0.2 → 3.0.0-beta.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 +181 -14
- package/dist/bootstrap.test.d.ts +1 -0
- package/dist/e2e.test-utils.d.ts +49 -0
- package/dist/glitchFilter.d.ts +51 -0
- package/dist/glitchFilter.test.d.ts +1 -0
- package/dist/harness.test-utils.d.ts +59 -0
- package/dist/historyReconcile.test.d.ts +1 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.js +12898 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.e2e.test.d.ts +1 -0
- package/dist/provider.e2e.test.d.ts +1 -0
- package/dist/readme.test.d.ts +1 -0
- package/dist/reconcile.d.ts +16 -0
- package/dist/reconcile.test.d.ts +1 -0
- package/dist/routes.test.d.ts +1 -0
- package/dist/selfTrack.test.d.ts +1 -0
- package/dist/source.test.d.ts +1 -0
- package/dist/sourceWatch.d.ts +44 -0
- package/dist/sourceWatch.test.d.ts +1 -0
- package/dist/sqlitePlugin.test.d.ts +1 -0
- package/dist/sqliteStore.d.ts +95 -0
- package/dist/sqliteStore.test.d.ts +1 -0
- package/dist/stateGate.d.ts +40 -0
- package/dist/stateGate.test.d.ts +1 -0
- package/dist/store.d.ts +63 -0
- package/dist/store.test.d.ts +1 -0
- package/dist/timeWindow.d.ts +50 -0
- package/dist/timeWindow.test.d.ts +1 -0
- package/dist/trackApi.d.ts +77 -0
- package/dist/trackApi.e2e.test.d.ts +1 -0
- package/dist/trackProvider.d.ts +21 -0
- package/dist/trackProvider.test.d.ts +1 -0
- package/dist/tracks.d.ts +67 -0
- package/dist/tracks.test.d.ts +1 -0
- package/dist/types.d.ts +55 -0
- package/dist/utils.d.ts +31 -0
- package/dist/utils.test.d.ts +1 -0
- package/package.json +59 -8
- package/index.js +0 -1
- package/signalk-tracks-plugin-1.0.0.tgz +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TimedPosition } from './types.js';
|
|
2
|
+
export interface ReconcileResult {
|
|
3
|
+
positions: TimedPosition[];
|
|
4
|
+
/** Buckets served from the history provider. */
|
|
5
|
+
fromHistory: number;
|
|
6
|
+
/** Buckets served from the plugin's own store. */
|
|
7
|
+
fromStore: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Merge history and store positions into one track.
|
|
11
|
+
*
|
|
12
|
+
* `resolution` is the bucket width in milliseconds; it should be the
|
|
13
|
+
* resolution the history provider was asked for, so its rows land one per
|
|
14
|
+
* bucket.
|
|
15
|
+
*/
|
|
16
|
+
export declare function reconcile(history: TimedPosition[], stored: TimedPosition[], resolution: number): ReconcileResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Context } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Watches which `$source` each context's positions arrive from.
|
|
4
|
+
*
|
|
5
|
+
* `navigation.position` is a rapid update — a GPS may report at 10Hz — and a
|
|
6
|
+
* typical boat has several receivers: an internal GPS, an AIS transponder, a
|
|
7
|
+
* chart plotter echoing its own fix.
|
|
8
|
+
*
|
|
9
|
+
* Signal K resolves which one wins through source priority, and the bus this
|
|
10
|
+
* plugin listens on is the filtered one: the server applies `toPreferredDelta`
|
|
11
|
+
* before emitting `delta`, which is what feeds `getBus()`. So with a priority
|
|
12
|
+
* rule in place, only the winner arrives and there is nothing to warn about.
|
|
13
|
+
*
|
|
14
|
+
* The gap is when no rule *matches* the path. Then the engine passes every
|
|
15
|
+
* source through, the fixes from receivers metres apart interleave, and the
|
|
16
|
+
* track zigzags between them rather than following the boat. The fix is to
|
|
17
|
+
* configure source priority for `navigation.position`; this watcher exists to
|
|
18
|
+
* say so, because the symptom (a track that looks noisy) does not obviously
|
|
19
|
+
* point at the cause.
|
|
20
|
+
*
|
|
21
|
+
* Detection is therefore empirical rather than reading the priority config:
|
|
22
|
+
* seeing several sources here *is* the evidence that no rule is matching,
|
|
23
|
+
* which a config that merely exists would not tell us.
|
|
24
|
+
*/
|
|
25
|
+
export declare class SourceWatch {
|
|
26
|
+
private readonly seen;
|
|
27
|
+
/** Record that `context` produced a position from `$source`. */
|
|
28
|
+
add(context: Context, source: string | undefined): void;
|
|
29
|
+
/** Sources seen for a context, in the order they first appeared. */
|
|
30
|
+
sourcesFor(context: Context): string[];
|
|
31
|
+
/** Contexts receiving positions from more than one source. */
|
|
32
|
+
conflicted(): Context[];
|
|
33
|
+
/**
|
|
34
|
+
* A status line naming the problem, or undefined when nothing is wrong.
|
|
35
|
+
*
|
|
36
|
+
* `selfContext` is called out by name because the own vessel's track is the
|
|
37
|
+
* one a user looks at, and it is the one whose priority they can fix. Other
|
|
38
|
+
* vessels are usually AIS, where multiple receivers are expected and less
|
|
39
|
+
* worth nagging about — they are counted, not named.
|
|
40
|
+
*/
|
|
41
|
+
warning(selfContext?: Context): string | undefined;
|
|
42
|
+
/** Forget everything seen; used when the plugin restarts. */
|
|
43
|
+
clear(): void;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { TrackQuery } from './timeWindow.js';
|
|
2
|
+
import { TrackStore } from './store.js';
|
|
3
|
+
import { Context, Debug, LatLngTuple, TimedPosition, TimedTrackCollection, TimeWindow, TrackCollection, TrackParams } from './types.js';
|
|
4
|
+
export interface SqliteStoreConfig {
|
|
5
|
+
/** Absolute path to the database file, or ':memory:'. */
|
|
6
|
+
file: string;
|
|
7
|
+
/**
|
|
8
|
+
* Minimum spacing between stored positions, in ms. 0 stores every one.
|
|
9
|
+
*
|
|
10
|
+
* The same `resolution` the in-memory accumulator applies with
|
|
11
|
+
* `throttleTime`, enforced here on write instead. Without it a boat emitting
|
|
12
|
+
* position at 8 Hz writes ~700k rows a day rather than the ~1.4k a 60s
|
|
13
|
+
* resolution implies — a difference that lands on an SD card.
|
|
14
|
+
*/
|
|
15
|
+
resolution?: number;
|
|
16
|
+
/** Drop positions older than this many ms. 0 keeps everything. */
|
|
17
|
+
retention?: number;
|
|
18
|
+
/** Cells per bounding box covering. */
|
|
19
|
+
maxCells?: number;
|
|
20
|
+
/** A gap longer than this ms starts a new track segment. */
|
|
21
|
+
segmentGap?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A position store backed by SQLite, so tracks survive a restart.
|
|
25
|
+
*
|
|
26
|
+
* Uses node:sqlite rather than better-sqlite3: it is built into Node >= 22, so
|
|
27
|
+
* installing the plugin on a Raspberry Pi needs no native compilation.
|
|
28
|
+
*
|
|
29
|
+
* The spatial index is Teppo Kurki's design from SignalK/tracks#11 — an S2 cell
|
|
30
|
+
* id per position, queried as id ranges — as is segmenting a track on a time
|
|
31
|
+
* gap. Both are reimplemented here rather than rebased; see that PR for the
|
|
32
|
+
* original.
|
|
33
|
+
*/
|
|
34
|
+
export declare class SqliteTrackStore implements TrackStore {
|
|
35
|
+
private readonly db;
|
|
36
|
+
private readonly insert;
|
|
37
|
+
private readonly maxCells;
|
|
38
|
+
private readonly segmentGap;
|
|
39
|
+
private readonly retention;
|
|
40
|
+
private readonly resolution;
|
|
41
|
+
private readonly debug;
|
|
42
|
+
/** Timestamp of the last position stored per context, for throttling. */
|
|
43
|
+
private readonly lastStored;
|
|
44
|
+
constructor(config: SqliteStoreConfig, debug: Debug);
|
|
45
|
+
newPosition(context: Context, position: LatLngTuple, timestamp?: number): void;
|
|
46
|
+
private store;
|
|
47
|
+
initialTrack(context: Context, track: LatLngTuple[], timestamps?: number[]): void;
|
|
48
|
+
private rowsFor;
|
|
49
|
+
private knows;
|
|
50
|
+
get(context: Context, window?: TimeWindow): Promise<LatLngTuple[]>;
|
|
51
|
+
getTimed(context: Context, window?: TimeWindow): Promise<TimedPosition[]>;
|
|
52
|
+
private contexts;
|
|
53
|
+
getAllTracks(query?: TrackQuery): Promise<{
|
|
54
|
+
context: string;
|
|
55
|
+
track: LatLngTuple[];
|
|
56
|
+
}[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Tracks whose last position matches the spatial predicate.
|
|
59
|
+
*
|
|
60
|
+
* Two filters run, and they are not redundant despite both testing bounds:
|
|
61
|
+
*
|
|
62
|
+
* - `contextsInBounds` asks the cell index which contexts have *any* position
|
|
63
|
+
* near the box. It is a cheap prefilter that keeps the query off every row
|
|
64
|
+
* in the table, and it decides nothing on its own.
|
|
65
|
+
* - `matcher` — the same predicate the in-memory store uses — then tests each
|
|
66
|
+
* track's *last* position, which is what the endpoint actually means by
|
|
67
|
+
* "in this box". That test is authoritative.
|
|
68
|
+
*
|
|
69
|
+
* Because `matcher` has the final say, dropping the exact-bounds check inside
|
|
70
|
+
* `contextsInBounds` does not change any result; it only widens the candidate
|
|
71
|
+
* set. It is kept because a prefilter that returns half the table is not
|
|
72
|
+
* worth running, not because correctness depends on it.
|
|
73
|
+
*/
|
|
74
|
+
getFilteredTracks(params: TrackParams, selfPosition?: LatLngTuple, debug?: Debug, query?: TrackQuery): Promise<TrackCollection>;
|
|
75
|
+
/**
|
|
76
|
+
* The same filtering as `getFilteredTracks`, keeping each point's timestamp.
|
|
77
|
+
*
|
|
78
|
+
* The untimed form is derived from this one so the two cannot disagree about
|
|
79
|
+
* which tracks match — both the cell prefilter and the authoritative
|
|
80
|
+
* last-position test run once, here.
|
|
81
|
+
*/
|
|
82
|
+
getFilteredTimedTracks(params: TrackParams, selfPosition?: LatLngTuple, debug?: Debug, query?: TrackQuery): Promise<TimedTrackCollection>;
|
|
83
|
+
/** Contexts with at least one position inside `bounds`, via the cell index. */
|
|
84
|
+
private contextsInBounds;
|
|
85
|
+
/**
|
|
86
|
+
* Split a context's positions into segments, breaking where the recording
|
|
87
|
+
* stopped for longer than `segmentGap` — an overnight stop should not draw a
|
|
88
|
+
* straight line across the anchorage.
|
|
89
|
+
*/
|
|
90
|
+
segments(context: Context, window?: TimeWindow): LatLngTuple[][];
|
|
91
|
+
prune(maxAge: number): void;
|
|
92
|
+
close(): void;
|
|
93
|
+
/** Unsigned round-trip check, used by the tests. */
|
|
94
|
+
cellIdsFor(context: Context): bigint[];
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Context } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Off unless a user opts in, per the request and because the states worth
|
|
4
|
+
* pausing on depend on how a boat is used.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_PAUSE_STATES: string[];
|
|
7
|
+
/**
|
|
8
|
+
* The states a boat is plausibly parked in, offered in the plugin UI.
|
|
9
|
+
*
|
|
10
|
+
* A subset of the `navigation.state` enum in `@signalk/signalk-schema` rather
|
|
11
|
+
* than the whole of it: most of that list is AIS navigational status, and
|
|
12
|
+
* offering `trawling-hauling` as a reason to stop recording would be noise.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PAUSABLE_STATES: readonly ["moored", "not-under-way", "anchored", "aground"];
|
|
15
|
+
export declare class StateGate {
|
|
16
|
+
private readonly pauseStates;
|
|
17
|
+
private readonly selfContext;
|
|
18
|
+
/** Last state seen, for the plugin status line. */
|
|
19
|
+
private lastState;
|
|
20
|
+
private pausedCount;
|
|
21
|
+
constructor(selfContext: string, pauseStates: readonly string[]);
|
|
22
|
+
/** Whether the gate can ever pause anything. */
|
|
23
|
+
get enabled(): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Whether to record this position.
|
|
26
|
+
*
|
|
27
|
+
* `state` is whatever `navigation.state` currently reads, or undefined when
|
|
28
|
+
* the vessel does not report it. Undefined records: a boat with no state at
|
|
29
|
+
* all must not silently stop being tracked.
|
|
30
|
+
*/
|
|
31
|
+
accept(context: Context, state: string | undefined): boolean;
|
|
32
|
+
/** The state last seen for the own vessel, or undefined if none yet. */
|
|
33
|
+
get state(): string | undefined;
|
|
34
|
+
/** How many positions have been skipped. */
|
|
35
|
+
get skipped(): number;
|
|
36
|
+
/** A status line while paused, or undefined when recording. */
|
|
37
|
+
status(): string | undefined;
|
|
38
|
+
/** Forget what was seen; used when the plugin restarts. */
|
|
39
|
+
clear(): void;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Context, Debug, LatLngTuple, TimedPosition, TimedTrackCollection, TimeWindow, TrackCollection, TrackParams } from './types.js';
|
|
2
|
+
import { TrackQuery } from './timeWindow.js';
|
|
3
|
+
/**
|
|
4
|
+
* A track store: somewhere positions are accumulated and queried back.
|
|
5
|
+
*
|
|
6
|
+
* The in-memory `Tracks` accumulator is one implementation; a persistent store
|
|
7
|
+
* is another. The interface is deliberately the surface the plugin already
|
|
8
|
+
* calls in `index.ts` and nothing more, so that adding an implementation cannot
|
|
9
|
+
* quietly widen what a store is expected to do.
|
|
10
|
+
*
|
|
11
|
+
* Note what is *not* here. `getAllTracks` and `getFilteredTracks` are derived
|
|
12
|
+
* operations — filtering is a predicate over whole tracks, which the in-memory
|
|
13
|
+
* implementation applies in JS and a database implementation will want to push
|
|
14
|
+
* into the query. Both stay on the interface for that reason: a store that can
|
|
15
|
+
* filter in SQL must be allowed to, rather than being forced to materialise
|
|
16
|
+
* every track so a caller can filter it afterwards.
|
|
17
|
+
*/
|
|
18
|
+
export interface TrackStore {
|
|
19
|
+
/** Record a position for a context. `timestamp` defaults to now. */
|
|
20
|
+
newPosition(context: Context, position: LatLngTuple, timestamp?: number): void;
|
|
21
|
+
/**
|
|
22
|
+
* Seed a context's track, replacing whatever it held.
|
|
23
|
+
*
|
|
24
|
+
* Used by the History API bootstrap at startup. `timestamps` is positional
|
|
25
|
+
* against `track`; points without one are dated to the start of time so a
|
|
26
|
+
* time-window query treats them as older than anything live.
|
|
27
|
+
*/
|
|
28
|
+
initialTrack(context: Context, track: LatLngTuple[], timestamps?: number[]): void;
|
|
29
|
+
/**
|
|
30
|
+
* Positions for a context, oldest first, optionally narrowed to a window.
|
|
31
|
+
*
|
|
32
|
+
* Rejects when the context is unknown; the route handlers turn that into a
|
|
33
|
+
* 404. Resolving with `[]` would be indistinguishable from a vessel that is
|
|
34
|
+
* known but has not moved.
|
|
35
|
+
*/
|
|
36
|
+
get(context: Context, window?: TimeWindow): Promise<LatLngTuple[]>;
|
|
37
|
+
/** As `get`, but keeping the timestamp of each point. */
|
|
38
|
+
getTimed(context: Context, window?: TimeWindow): Promise<TimedPosition[]>;
|
|
39
|
+
/** Every known context and its track, thinned to `query.resolution`. */
|
|
40
|
+
getAllTracks(query?: TrackQuery): Promise<{
|
|
41
|
+
context: string;
|
|
42
|
+
track: LatLngTuple[];
|
|
43
|
+
}[]>;
|
|
44
|
+
/** As `getFilteredTracks`, but keeping the timestamp of each point. */
|
|
45
|
+
getFilteredTimedTracks(params: TrackParams, selfPosition?: LatLngTuple, debug?: Debug, query?: TrackQuery): Promise<TimedTrackCollection>;
|
|
46
|
+
/**
|
|
47
|
+
* Tracks whose *last* position matches a spatial predicate.
|
|
48
|
+
*
|
|
49
|
+
* Matching on the last position rather than any position is what makes this
|
|
50
|
+
* "vessels currently near here" rather than "vessels that ever passed here".
|
|
51
|
+
*/
|
|
52
|
+
getFilteredTracks(params: TrackParams, selfPosition?: LatLngTuple, debug?: Debug, query?: TrackQuery): Promise<TrackCollection>;
|
|
53
|
+
/** Drop contexts whose newest position is older than `maxAge` ms. */
|
|
54
|
+
prune(maxAge: number): void;
|
|
55
|
+
/**
|
|
56
|
+
* Release any resources held by the store.
|
|
57
|
+
*
|
|
58
|
+
* A no-op for the in-memory implementation, which is why it is optional; a
|
|
59
|
+
* store holding file handles must implement it and the plugin calls it from
|
|
60
|
+
* `stop()`.
|
|
61
|
+
*/
|
|
62
|
+
close?(): void;
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { TimedPosition, TimeWindow } from './types.js';
|
|
2
|
+
export declare class TimeWindowError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export declare function parseDuration(value: string): number;
|
|
5
|
+
export interface TrackQuery {
|
|
6
|
+
window?: TimeWindow;
|
|
7
|
+
/** Minimum spacing between returned points, in milliseconds. */
|
|
8
|
+
resolution?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Serve the recording time of every point alongside the geometry.
|
|
11
|
+
*
|
|
12
|
+
* Opt-in rather than always-on: the response grows by roughly a third, and
|
|
13
|
+
* the clients reading these routes today (Freeboard-SK) draw the geometry
|
|
14
|
+
* and have no use for the times. Consumers that need per-point time — an
|
|
15
|
+
* IMO-style AIS display drawing dots equally spaced by time, per
|
|
16
|
+
* SignalK/signalk-server#2504 — ask for it.
|
|
17
|
+
*/
|
|
18
|
+
times?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the time-window parameters into an absolute `[from, to)` window.
|
|
22
|
+
*
|
|
23
|
+
* Precedence: explicit `from`/`to` win; then `duration`; then the Freeboard
|
|
24
|
+
* `timespan`/`timespanOffset` pair. Absent all of them, no window is applied
|
|
25
|
+
* and the whole retained track is returned, as before.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseTrackQuery(query: Record<string, unknown>, now?: number): TrackQuery;
|
|
28
|
+
/**
|
|
29
|
+
* Drop points closer together than `resolution` milliseconds, always keeping
|
|
30
|
+
* the first and last so the track keeps its extent.
|
|
31
|
+
*/
|
|
32
|
+
export declare function thin(points: TimedPosition[], resolution: number | undefined): TimedPosition[];
|
|
33
|
+
/**
|
|
34
|
+
* Split points into segments wherever recording stopped for longer than `gap`.
|
|
35
|
+
*
|
|
36
|
+
* A track is not one continuous line: a vessel that stops for the night, or
|
|
37
|
+
* sails out of AIS range and back, leaves a hole. Joining across it draws a
|
|
38
|
+
* straight line through places the vessel never went — across a headland, or
|
|
39
|
+
* over an anchorage it actually sat still in.
|
|
40
|
+
*
|
|
41
|
+
* Operates on timestamped points rather than inside a store, so both the
|
|
42
|
+
* in-memory and sqlite stores segment identically, and so it composes with
|
|
43
|
+
* `thin()` — thinning first, then segmenting, keeps a thinned-away gap from
|
|
44
|
+
* being invented as a join.
|
|
45
|
+
*
|
|
46
|
+
* `gap` of 0 or undefined returns a single segment, preserving the old shape.
|
|
47
|
+
* An empty input returns no segments rather than one empty one, so a caller can
|
|
48
|
+
* distinguish "no track" from "a track with no points".
|
|
49
|
+
*/
|
|
50
|
+
export declare function segment(points: TimedPosition[], gap: number | undefined): TimedPosition[][];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
2
|
+
/**
|
|
3
|
+
* The v2 Track API contract, declared locally.
|
|
4
|
+
*
|
|
5
|
+
* These mirror `@signalk/server-api/tracks`, and are copied here for the same
|
|
6
|
+
* reason the History API types are: depending on the package would pin this
|
|
7
|
+
* plugin to a server version, and the subpath is not published yet. Swap the
|
|
8
|
+
* imports when it is.
|
|
9
|
+
*
|
|
10
|
+
* Specified in https://github.com/SignalK/signalk-server/issues/2504 and
|
|
11
|
+
* implemented in https://github.com/SignalK/signalk-server/pull/2995.
|
|
12
|
+
*/
|
|
13
|
+
/** `[west, south, east, north]` — GeoJSON coordinate order. */
|
|
14
|
+
export type TrackBoundingBox = [number, number, number, number];
|
|
15
|
+
/**
|
|
16
|
+
* A request as the server hands it to a provider.
|
|
17
|
+
*
|
|
18
|
+
* Not every field is honoured here. `maxPoints`, `simplify`, `epsilon` and
|
|
19
|
+
* `properties` are accepted by the API and ignored by this provider: it serves
|
|
20
|
+
* positions, and has no co-recorded values to attach or geometry simplifier to
|
|
21
|
+
* run. A caller gets the full, unsimplified track for the window it asked for,
|
|
22
|
+
* which is a superset of what it requested rather than a wrong answer. The
|
|
23
|
+
* response echoes `resolution` when one was requested, so a client can tell a
|
|
24
|
+
* thinned track from a full one.
|
|
25
|
+
*
|
|
26
|
+
* Simplification and co-recorded properties are tracked separately; see
|
|
27
|
+
* SignalK/tracks.
|
|
28
|
+
*/
|
|
29
|
+
export interface TracksRequest {
|
|
30
|
+
contexts?: string[];
|
|
31
|
+
from?: Temporal.Instant;
|
|
32
|
+
to?: Temporal.Instant;
|
|
33
|
+
duration?: Temporal.Duration;
|
|
34
|
+
bbox?: TrackBoundingBox;
|
|
35
|
+
resolution?: Temporal.Duration;
|
|
36
|
+
maxPoints?: number;
|
|
37
|
+
simplify?: boolean;
|
|
38
|
+
epsilon?: number;
|
|
39
|
+
times?: boolean;
|
|
40
|
+
properties?: string[];
|
|
41
|
+
geometry?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface TrackProperties {
|
|
44
|
+
context: string;
|
|
45
|
+
isSelf: boolean;
|
|
46
|
+
contextName?: string;
|
|
47
|
+
from: string;
|
|
48
|
+
to: string;
|
|
49
|
+
bbox?: TrackBoundingBox;
|
|
50
|
+
pointCount: number;
|
|
51
|
+
resolution?: string;
|
|
52
|
+
epsilon?: number;
|
|
53
|
+
coordTimes?: string[][];
|
|
54
|
+
appliedProperties?: string[];
|
|
55
|
+
values?: Record<string, (number | string | null)[][]>;
|
|
56
|
+
}
|
|
57
|
+
export interface TrackFeature {
|
|
58
|
+
type: 'Feature';
|
|
59
|
+
geometry: {
|
|
60
|
+
type: 'MultiLineString';
|
|
61
|
+
/** `[longitude, latitude]` positions, per segment. */
|
|
62
|
+
coordinates: [number, number][][];
|
|
63
|
+
} | null;
|
|
64
|
+
properties: TrackProperties;
|
|
65
|
+
}
|
|
66
|
+
export interface TracksResponse {
|
|
67
|
+
type: 'FeatureCollection';
|
|
68
|
+
features: TrackFeature[];
|
|
69
|
+
}
|
|
70
|
+
export interface TrackApi {
|
|
71
|
+
getTracks(query: TracksRequest): Promise<TracksResponse>;
|
|
72
|
+
getTrackContexts(query: TracksRequest): Promise<string[]>;
|
|
73
|
+
}
|
|
74
|
+
/** Present on servers that carry the Track API; absent on older ones. */
|
|
75
|
+
export interface WithTrackApi {
|
|
76
|
+
registerTrackApiProvider?: (provider: TrackApi) => void;
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { TrackStore } from './store.js';
|
|
2
|
+
import { TrackApi } from './trackApi.js';
|
|
3
|
+
/**
|
|
4
|
+
* Serves the v2 Track API from this plugin's store.
|
|
5
|
+
*
|
|
6
|
+
* The store answers the hard parts — spatial filtering, time windows, thinning
|
|
7
|
+
* — so what is left here is mostly translation: v2's GeoJSON coordinate order
|
|
8
|
+
* and Temporal values in, GeoJSON Features out. Segmentation is done here,
|
|
9
|
+
* by `segment()`, because it is what turns a thinned track into the
|
|
10
|
+
* MultiLineString the wire format wants.
|
|
11
|
+
*
|
|
12
|
+
* Registered alongside the v1 routes rather than replacing them, so
|
|
13
|
+
* Freeboard-SK keeps working until it moves.
|
|
14
|
+
*/
|
|
15
|
+
export interface TrackProviderDeps {
|
|
16
|
+
store: () => TrackStore | undefined;
|
|
17
|
+
selfContext: () => string;
|
|
18
|
+
/** Gap in ms that starts a new segment; 0 leaves the track as one line. */
|
|
19
|
+
segmentGap: () => number;
|
|
20
|
+
}
|
|
21
|
+
export declare function createTrackProvider(deps: TrackProviderDeps): TrackApi;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/tracks.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Subject, Connectable, Observable } from 'rxjs';
|
|
2
|
+
import { TrackQuery } from './timeWindow.js';
|
|
3
|
+
import { TrackStore } from './store.js';
|
|
4
|
+
import { Context, Debug, LatLngTuple, TimedPosition, TimedTrackCollection, TimeWindow, TrackCollection, TrackParams } from './types.js';
|
|
5
|
+
interface TracksMap {
|
|
6
|
+
[context: string]: TrackAccumulator;
|
|
7
|
+
}
|
|
8
|
+
interface VesselTrack {
|
|
9
|
+
context: string;
|
|
10
|
+
track: LatLngTuple[];
|
|
11
|
+
}
|
|
12
|
+
export interface TracksConfig {
|
|
13
|
+
resolution: number;
|
|
14
|
+
pointsToKeep: number;
|
|
15
|
+
fetchInitialTrack?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The in-memory store: a bounded sliding window of positions per context,
|
|
19
|
+
* held in RxJS accumulators and lost on restart. `bootstrapSelfTrack()` in
|
|
20
|
+
* index.ts is what re-hydrates it from a History provider at startup.
|
|
21
|
+
*/
|
|
22
|
+
export declare class Tracks implements TrackStore {
|
|
23
|
+
tracks: TracksMap;
|
|
24
|
+
debug: Debug;
|
|
25
|
+
config: TracksConfig;
|
|
26
|
+
constructor(config: TracksConfig, debug: Debug);
|
|
27
|
+
newPosition(context: Context, position: LatLngTuple, timestamp?: number): void;
|
|
28
|
+
initialTrack(context: Context, track: LatLngTuple[], timestamps?: number[]): void;
|
|
29
|
+
getAccumulator(context: Context, createIfMissing?: boolean): TrackAccumulator | undefined;
|
|
30
|
+
get(context: Context, window?: TimeWindow): Promise<LatLngTuple[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Track points with their timestamps, optionally narrowed to a time window.
|
|
33
|
+
*
|
|
34
|
+
* Rejects with EmptyError if the stream completes without emitting; the route
|
|
35
|
+
* handlers turn that into the same 404 as an unknown context.
|
|
36
|
+
*/
|
|
37
|
+
getTimed(context: Context, window?: TimeWindow): Promise<TimedPosition[]>;
|
|
38
|
+
getAllTracks(query?: TrackQuery): Promise<VesselTrack[]>;
|
|
39
|
+
getFilteredTracks(params: TrackParams, selfPosition?: LatLngTuple, debug?: Debug, query?: TrackQuery): Promise<TrackCollection>;
|
|
40
|
+
/**
|
|
41
|
+
* The same filtering as `getFilteredTracks`, keeping each point's timestamp.
|
|
42
|
+
*
|
|
43
|
+
* The untimed form is derived from this one so the two cannot disagree about
|
|
44
|
+
* which tracks match — the filter runs once, here.
|
|
45
|
+
*/
|
|
46
|
+
getFilteredTimedTracks(params: TrackParams, selfPosition?: LatLngTuple, debug?: Debug, query?: TrackQuery): Promise<TimedTrackCollection>;
|
|
47
|
+
prune(maxAge: number): void;
|
|
48
|
+
}
|
|
49
|
+
interface AccumulatorParams {
|
|
50
|
+
resolution: number;
|
|
51
|
+
pointsToKeep: number;
|
|
52
|
+
fetchTrackFor?: string;
|
|
53
|
+
}
|
|
54
|
+
export declare class TrackAccumulator {
|
|
55
|
+
initialTrack: Subject<TimedPosition[]>;
|
|
56
|
+
input: Subject<TimedPosition>;
|
|
57
|
+
latestLatLngTuple: number;
|
|
58
|
+
accumulatedTrack: Connectable<TimedPosition[]>;
|
|
59
|
+
/** Timestamped points, used to answer time-window queries. */
|
|
60
|
+
timedTrack: Observable<TimedPosition[]>;
|
|
61
|
+
/** Positions only — the long-standing public shape. */
|
|
62
|
+
track: Observable<LatLngTuple[]>;
|
|
63
|
+
constructor({ resolution, pointsToKeep, fetchTrackFor }: AccumulatorParams);
|
|
64
|
+
nextLatLngTuple(position: LatLngTuple, timestamp?: number): void;
|
|
65
|
+
setInitialTrack(track: LatLngTuple[], timestamps?: number[]): void;
|
|
66
|
+
}
|
|
67
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type Context = string;
|
|
2
|
+
export type LatLngTuple = [number, number];
|
|
3
|
+
export type LngLatTuple = [number, number];
|
|
4
|
+
/**
|
|
5
|
+
* A track point with the time it was recorded, in epoch milliseconds.
|
|
6
|
+
*
|
|
7
|
+
* Positions are stored as bare `LatLngTuple`s on the wire and in the public
|
|
8
|
+
* `track` observable; timestamps are kept alongside so time-window queries
|
|
9
|
+
* (`from`/`to`/`duration`) can slice a track without changing that shape.
|
|
10
|
+
*/
|
|
11
|
+
export interface TimedPosition {
|
|
12
|
+
position: LatLngTuple;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A time window in epoch milliseconds, half-open as `[from, to)`.
|
|
17
|
+
*
|
|
18
|
+
* Half-open so the bands a client requests to cover a long trail tile exactly:
|
|
19
|
+
* `[now-24h, now-1h)` and `[now-1h, now]` share the boundary point without
|
|
20
|
+
* returning it twice. `inclusiveEnd` closes the final band so the newest fix is
|
|
21
|
+
* not dropped from a "last hour" query.
|
|
22
|
+
*/
|
|
23
|
+
export interface TimeWindow {
|
|
24
|
+
from: number;
|
|
25
|
+
to: number;
|
|
26
|
+
inclusiveEnd?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface Position {
|
|
29
|
+
latitude: number;
|
|
30
|
+
longitude: number;
|
|
31
|
+
}
|
|
32
|
+
export interface TrackCollection {
|
|
33
|
+
[key: string]: LatLngTuple[];
|
|
34
|
+
}
|
|
35
|
+
/** As `TrackCollection`, but each point keeps the time it was recorded. */
|
|
36
|
+
export interface TimedTrackCollection {
|
|
37
|
+
[key: string]: TimedPosition[];
|
|
38
|
+
}
|
|
39
|
+
export interface GeoBounds {
|
|
40
|
+
ne: LatLngTuple;
|
|
41
|
+
sw: LatLngTuple;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Express `req.query` shape: a value may be absent, a single string, or repeated
|
|
45
|
+
* into an array when the same key appears more than once in the query string.
|
|
46
|
+
*/
|
|
47
|
+
export type QueryParameters = Record<string, unknown>;
|
|
48
|
+
export interface TrackParams {
|
|
49
|
+
bbox: GeoBounds | null;
|
|
50
|
+
radius: number | null;
|
|
51
|
+
}
|
|
52
|
+
export interface Debug {
|
|
53
|
+
(...args: unknown[]): void;
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { LatLngTuple, GeoBounds, QueryParameters, TimedPosition, TrackParams, Debug } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The recording times of a segment, as ISO-8601 UTC, positionally aligned with
|
|
4
|
+
* the segment's coordinates.
|
|
5
|
+
*
|
|
6
|
+
* UTC because a track outlives the timezone it was recorded in: a passage that
|
|
7
|
+
* crosses a zone boundary, or a database restored on a boat that has since
|
|
8
|
+
* moved, must not shift. ISO-8601 rather than the epoch milliseconds held
|
|
9
|
+
* internally because that is what the rest of Signal K puts on the wire.
|
|
10
|
+
*/
|
|
11
|
+
export declare function toIsoTimes(segment: TimedPosition[]): string[];
|
|
12
|
+
export declare function createInBounds(bounds: GeoBounds): (position: LatLngTuple | null) => boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Split a bounds that crosses the antimeridian into two that do not.
|
|
15
|
+
*
|
|
16
|
+
* A bounds wraps when its west edge is numerically greater than its east edge
|
|
17
|
+
* (sw lng 179, ne lng -179): the box runs 179 -> 180 -> -179, the short way
|
|
18
|
+
* across the Pacific. `createInBounds` handles that directly with its +360
|
|
19
|
+
* arithmetic, but anything that has to describe the box *geometrically* cannot
|
|
20
|
+
* — a GeoJSON polygon with those corners is read as a band going the long way
|
|
21
|
+
* round, and an S2 covering of it comes back empty, which turns "vessels near
|
|
22
|
+
* Fiji" into a silent no results rather than an error.
|
|
23
|
+
*
|
|
24
|
+
* Returns one bounds unchanged when there is no wrap, so callers can always
|
|
25
|
+
* iterate the result.
|
|
26
|
+
*/
|
|
27
|
+
export declare function splitAtAntimeridian(bounds: GeoBounds): GeoBounds[];
|
|
28
|
+
export declare function validateParameters(params: QueryParameters, defaultMaxRadius: number | undefined): TrackParams;
|
|
29
|
+
export declare function createDistanceTo([lat1d, lon1d]: LatLngTuple, debug?: Debug): (d: LatLngTuple | null) => number;
|
|
30
|
+
export declare function resolveContext(vesselId: string, selfContext?: string): string;
|
|
31
|
+
export declare function createMatcher(params: TrackParams, selfPosition?: LatLngTuple, debug?: Debug): (track: LatLngTuple[]) => boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|