@signalk/tracks-plugin 3.0.0-beta.2 → 3.0.0-beta.4

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 CHANGED
@@ -133,7 +133,8 @@ so thinning never shortens the track._
133
133
  ],
134
134
  "times": [["2026-08-14T09:00:00.000Z", "2026-08-14T09:01:00.000Z"]],
135
135
  "context": "vessels.urn:mrn:imo:mmsi:123456789",
136
- "isSelf": true
136
+ "isSelf": true,
137
+ "name": "Own Ship"
137
138
  }
138
139
  ```
139
140
 
@@ -146,6 +147,11 @@ _`context` is the fully qualified context the track belongs to, and `isSelf` say
146
147
  the own vessel. Asking for `self` resolves the alias, so the response tells you which vessel
147
148
  `self` actually is._
148
149
 
150
+ _`name` is a display label, the way a chart plotter shows one: `Own Ship`, or `AIS <shipname>`
151
+ falling back to `AIS <mmsi>` and finally the raw context. It is for putting in a list — the v2
152
+ Track API's `contextName` carries the undecorated vessel name instead, and is absent for a
153
+ vessel that has not sent one._
154
+
149
155
  ---
150
156
 
151
157
  **Retrieve tracks for all vessels:**
@@ -155,7 +161,7 @@ the own vessel. Asking for `self` resolves the alias, so the response tells you
155
161
  _Every vessel the plugin holds a track for. Add `?radius=` to narrow it to vessels near your own._
156
162
 
157
163
  _Each entry carries `isSelf`, so the own vessel can be told from an AIS target without
158
- string-matching the context against the server's self identity._
164
+ string-matching the context against the server's self-identity, and `name` for display._
159
165
 
160
166
  _`?times` works here too, adding a `times` array to every vessel's entry. Note that asking for
161
167
  times also segments each track on the gap threshold, so `coordinates` and `times` line up;
@@ -35,6 +35,16 @@ export interface E2EOptions {
35
35
  * depends on — rather than against a provider's private storage.
36
36
  */
37
37
  plugins?: Record<string, Record<string, unknown>>;
38
+ /**
39
+ * Plugin id to pin as the server's default history provider.
40
+ *
41
+ * The server takes the *first* provider that registers as the default, so a
42
+ * second one installed in the server checkout — KIP registers as a history
43
+ * provider, and a dev checkout usually has it — silently wins and answers
44
+ * every unqualified history query with nothing. Naming the one the test
45
+ * installed makes the run independent of whatever else is lying around.
46
+ */
47
+ historyProvider?: string;
38
48
  /** Port to boot on, when a test needs its own server. */
39
49
  port?: number;
40
50
  }
package/dist/gpx.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { TimedPosition } from './types.js';
2
+ /** Identity of the vessel a track belongs to, as far as it is known. */
3
+ export interface GpxTrackIdentity {
4
+ /** Display label, e.g. `Own Ship` or `AIS Ariadne`. Becomes `<name>`. */
5
+ name: string;
6
+ /** Fully qualified Signal K context, when there is one. */
7
+ context?: string;
8
+ }
9
+ export interface GpxTrack extends GpxTrackIdentity {
10
+ /** Segments as `segment()` produces them: a recording gap starts a new one. */
11
+ segments: TimedPosition[][];
12
+ }
13
+ /**
14
+ * Serialise tracks as a GPX 1.1 document.
15
+ *
16
+ * Several tracks go in one file, each as its own `<trk>`. GPX allows it, and
17
+ * it is what makes "select three trips, export" produce one file rather than
18
+ * three — TimeZero writes one file per vessel, which is a choice rather than a
19
+ * constraint of the format.
20
+ *
21
+ * The Signal K context travels in `<extensions>`. TimeZero puts an opaque
22
+ * GUID there and drops the MMSI entirely, so its exports cannot round-trip an
23
+ * identity; carrying the context means ours can, while staying readable by
24
+ * anything else, since `<extensions>` is ignorable by specification.
25
+ */
26
+ export declare function toGpx(tracks: GpxTrack[], now?: Date): string;
27
+ /**
28
+ * Parse a GPX document into tracks.
29
+ *
30
+ * Structure follows GPX's own hierarchy: the root is `<gpx>`, tracks are its
31
+ * children, segments are a track's, points are a segment's. Anything else —
32
+ * a `<trkseg>` inside an `<extensions>` payload, a `<trk>` below the root — is
33
+ * not track data, and a namespace check alone cannot tell the difference,
34
+ * since a payload declaring no namespace inherits GPX's own.
35
+ *
36
+ * Deliberately forgiving about structure and strict about values: elements it
37
+ * does not recognise are ignored rather than rejected, but a coordinate that is
38
+ * not an `xsd:decimal` in range, or a time that is not an `xsd:dateTime`, is
39
+ * refused. One NaN reaching the store stretches every bounding box that track
40
+ * appears in.
41
+ *
42
+ * **Points are sorted by time within each segment.** TimeZero paginates its
43
+ * export in blocks of 150 and the block seams overlap: the last point of a
44
+ * block can be a few milliseconds newer than the first point of the next. Out
45
+ * of 5,550 points in a real export, 16 pairs are out of order, every one on a
46
+ * 150-point boundary. Anything downstream that assumes ordering — time-window
47
+ * queries, the history reconciliation — would quietly misbehave on such a file.
48
+ *
49
+ * Sorting stops at the segment boundary, deliberately: a segment is a stretch
50
+ * of continuous recording, and reordering points across a gap would merge two
51
+ * passages into one line. So `segments.flat()` is not globally sorted for a
52
+ * file whose segments interleave in time, and a caller that needs that must
53
+ * sort for itself.
54
+ *
55
+ * A document the parser rejects yields no tracks rather than throwing: this
56
+ * reads files the plugin did not write, and an import that fails should say so
57
+ * by returning nothing rather than by taking the caller down.
58
+ */
59
+ export declare function fromGpx(xml: string): GpxTrack[];
@@ -0,0 +1 @@
1
+ export {};
@@ -34,6 +34,13 @@ export interface TestHarness {
34
34
  /** The own vessel's navigation.state, as reported by `getSelfPath`. */
35
35
  setSelfState: (state: string | undefined) => void;
36
36
  stop: () => void;
37
+ /**
38
+ * Start the same plugin instance again, with the config it was built with.
39
+ *
40
+ * A fresh harness would not do: what a restart has to show is the state the
41
+ * instance drops on `stop()`, which a new instance never had.
42
+ */
43
+ restart: () => void;
37
44
  /**
38
45
  * How many positions the plugin's bus subscription has accepted.
39
46
  *
@@ -57,10 +64,71 @@ export interface HarnessOptions {
57
64
  selfPosition?: LatLngTuple;
58
65
  /** Initial navigation.state for the own vessel. */
59
66
  selfState?: string;
67
+ /**
68
+ * Data-model entries other vessels' paths resolve to, keyed by full path
69
+ * (e.g. `vessels.urn:...:244813000.name`). Omit to stand in for an older
70
+ * server with no `getPath`, where track naming falls back to the MMSI.
71
+ */
72
+ paths?: Record<string, unknown>;
60
73
  /**
61
74
  * Stand in for a server without the v2 Track API, to check the plugin still
62
75
  * starts when `registerTrackApiProvider` is absent.
63
76
  */
64
77
  withoutTrackApi?: boolean;
78
+ /**
79
+ * A history provider for the plugin to reconcile against.
80
+ *
81
+ * `contexts` is what `getContexts` lists, and `rows` what `getValues`
82
+ * returns — separately, because the difference between them is the whole
83
+ * point of the existence probe: a provider can know a vessel and still have
84
+ * no rows inside the asked window.
85
+ *
86
+ * `contextsSince` back-dates those contexts: a real provider filters its
87
+ * context list by the asked range (questdb builds a `WHERE` from it), so a
88
+ * stub that answers regardless of range cannot show what the probe misses.
89
+ * `getContextsRejects` and `getContextsHangs` cover the other two ways a
90
+ * provider can fail to answer.
91
+ */
92
+ history?: {
93
+ /** Read on every `getContexts` call, so a getter can count the probes. */
94
+ contexts?: string[];
95
+ contextsSince?: number;
96
+ rows?: unknown[];
97
+ withoutGetContexts?: boolean;
98
+ getContextsRejects?: boolean;
99
+ getContextsHangs?: boolean;
100
+ /**
101
+ * Hold every `getContexts` call open until the returned release is run.
102
+ *
103
+ * A provider that answers immediately cannot distinguish sharing an
104
+ * in-flight query from reusing a cached result: the first caller would
105
+ * have filled the cache before the rest arrived. Deferring the answer
106
+ * parks every caller inside the probe at once, so only sharing can
107
+ * collapse them.
108
+ */
109
+ deferContexts?: {
110
+ release: () => void;
111
+ wait: Promise<void>;
112
+ };
113
+ /** Fail the windowed read, leaving the provider unable to answer at all. */
114
+ getValuesRejects?: boolean;
115
+ /**
116
+ * Reject `getHistoryApi` itself, as the server does when no provider is
117
+ * registered — the default install, and not an outage.
118
+ */
119
+ noProvider?: boolean;
120
+ /** Never resolve `getHistoryApi`: a registered provider that wedges. */
121
+ providerHangs?: boolean;
122
+ };
65
123
  }
124
+ /**
125
+ * A promise held open until a test releases it.
126
+ *
127
+ * Lets a test observe that every request has reached the provider before any
128
+ * answer exists, which a timed wait can only assume.
129
+ */
130
+ export declare function deferred(): {
131
+ release: () => void;
132
+ wait: Promise<void>;
133
+ };
66
134
  export declare function createHarness(options?: HarnessOptions): TestHarness;
package/dist/index.d.ts CHANGED
@@ -29,6 +29,19 @@ interface HistoryValuesQuery {
29
29
  }
30
30
  interface HistoryApi {
31
31
  getValues(query: HistoryValuesQuery): Promise<HistoryValuesResponse>;
32
+ /**
33
+ * Contexts the provider holds data for, within a time range.
34
+ *
35
+ * Optional here though the upstream interface requires it: a provider
36
+ * registered against an older server-api may not implement it, and a missing
37
+ * method must degrade rather than throw.
38
+ */
39
+ getContexts?(query: HistoryContextsQuery): Promise<unknown[]>;
40
+ }
41
+ /** `ContextsRequest` upstream: a time range, with Instants rather than strings. */
42
+ interface HistoryContextsQuery {
43
+ from: Temporal.Instant;
44
+ to: Temporal.Instant;
32
45
  }
33
46
  interface HistoryValuesResponse {
34
47
  context: string;
@@ -49,6 +62,14 @@ interface App {
49
62
  };
50
63
  };
51
64
  getSelfPath: (path: string) => unknown;
65
+ /**
66
+ * Read any path in the full data model, including other vessels'.
67
+ *
68
+ * `getSelfPath` only reaches the own vessel, so naming an AIS track needs
69
+ * this. Optional: absent on older servers, where tracks fall back to the
70
+ * MMSI parsed out of the context.
71
+ */
72
+ getPath?: (path: string) => unknown;
52
73
  selfContext: string;
53
74
  /** Shown against the plugin in the server's dashboard. Absent on older servers. */
54
75
  setPluginStatus?: (msg: string) => void;
@@ -105,6 +126,8 @@ interface TracksPluginConfig {
105
126
  pauseWhenState?: string[];
106
127
  }
107
128
  export default function ThePlugin(app: App): Plugin;
129
+ export { fromGpx, toGpx } from './gpx.js';
130
+ export type { GpxTrack, GpxTrackIdentity } from './gpx.js';
108
131
  export { Tracks, TrackAccumulator } from './tracks.js';
109
132
  export type { TracksConfig } from './tracks.js';
110
133
  export type { TrackStore } from './store.js';
@@ -0,0 +1,30 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Tracks</title>
7
+ <link rel="stylesheet" href="./style.css" />
8
+ </head>
9
+ <body>
10
+ <header>
11
+ <h1>Tracks</h1>
12
+ <p id="status" role="status">Loading…</p>
13
+ </header>
14
+ <main>
15
+ <table id="tracks" hidden>
16
+ <thead>
17
+ <tr>
18
+ <th scope="col">Vessel</th>
19
+ <th scope="col">From</th>
20
+ <th scope="col">To</th>
21
+ <th scope="col" class="num">Points</th>
22
+ <th scope="col">Export</th>
23
+ </tr>
24
+ </thead>
25
+ <tbody></tbody>
26
+ </table>
27
+ </main>
28
+ <script type="module" src="./tracks.js"></script>
29
+ </body>
30
+ </html>