@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.
Files changed (41) hide show
  1. package/README.md +181 -14
  2. package/dist/bootstrap.test.d.ts +1 -0
  3. package/dist/e2e.test-utils.d.ts +49 -0
  4. package/dist/glitchFilter.d.ts +51 -0
  5. package/dist/glitchFilter.test.d.ts +1 -0
  6. package/dist/harness.test-utils.d.ts +59 -0
  7. package/dist/historyReconcile.test.d.ts +1 -0
  8. package/dist/index.d.ts +135 -0
  9. package/dist/index.js +12898 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/plugin.e2e.test.d.ts +1 -0
  12. package/dist/provider.e2e.test.d.ts +1 -0
  13. package/dist/readme.test.d.ts +1 -0
  14. package/dist/reconcile.d.ts +16 -0
  15. package/dist/reconcile.test.d.ts +1 -0
  16. package/dist/routes.test.d.ts +1 -0
  17. package/dist/selfTrack.test.d.ts +1 -0
  18. package/dist/source.test.d.ts +1 -0
  19. package/dist/sourceWatch.d.ts +44 -0
  20. package/dist/sourceWatch.test.d.ts +1 -0
  21. package/dist/sqlitePlugin.test.d.ts +1 -0
  22. package/dist/sqliteStore.d.ts +95 -0
  23. package/dist/sqliteStore.test.d.ts +1 -0
  24. package/dist/stateGate.d.ts +40 -0
  25. package/dist/stateGate.test.d.ts +1 -0
  26. package/dist/store.d.ts +63 -0
  27. package/dist/store.test.d.ts +1 -0
  28. package/dist/timeWindow.d.ts +50 -0
  29. package/dist/timeWindow.test.d.ts +1 -0
  30. package/dist/trackApi.d.ts +77 -0
  31. package/dist/trackApi.e2e.test.d.ts +1 -0
  32. package/dist/trackProvider.d.ts +21 -0
  33. package/dist/trackProvider.test.d.ts +1 -0
  34. package/dist/tracks.d.ts +67 -0
  35. package/dist/tracks.test.d.ts +1 -0
  36. package/dist/types.d.ts +55 -0
  37. package/dist/utils.d.ts +31 -0
  38. package/dist/utils.test.d.ts +1 -0
  39. package/package.json +59 -8
  40. package/index.js +0 -1
  41. package/signalk-tracks-plugin-1.0.0.tgz +0 -0
package/README.md CHANGED
@@ -1,30 +1,168 @@
1
- # @signalk/tracks and @signalk/tracks-plugin
2
- This repo contains two npm modules:
3
- - `@signalk/tracks` - RxJs based module that accumulates positions into a track, using a configured time resolution and retains a sliding window of points. Contains the code for both a Signal K server plugin that implements the track api and client side "TrackAccumulator" class that manages the track for a single vessel. TrackAccumulator can optionally bootstrap the track data from the server. The result is available as `Observable<LatLng[]>`.
4
- - `@signalk/tracks-plugin` Convenience module that exposes the plugin part of `@signalk/tracks`.
1
+ # @signalk/tracks-plugin
5
2
 
3
+ Signal K server plugin that accumulates vessel positions into tracks and implements the track API.
6
4
 
7
- # Tracks
8
- Plugin for tracks accumulation and the track API
5
+ Positions are accumulated into a per-vessel sliding window using a configured time resolution. The
6
+ **Where tracks come from after a restart** setting chooses what happens to them:
9
7
 
8
+ | Setting | Behaviour |
9
+ | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
10
+ | `history` | _(default)_ Held in memory, and the own vessel's track is refilled on startup from a history provider such as `signalk-to-influxdb2` or `signalk-questdb`. |
11
+ | `sqlite` | Recorded to a database file in the plugin's data directory, so every vessel's track survives a restart with no other plugin required. |
12
+ | `memory` | Held in memory only; tracks start empty after a restart. |
13
+
14
+ All three work with no other plugin installed — with `history` and no provider present, tracks simply
15
+ build up from live data. `sqlite` also takes a retention setting, in days, for how long to keep
16
+ positions.
17
+
18
+ This replaces the earlier `bootstrapFromHistory` checkbox. Existing configurations keep working:
19
+ the setting is still read, with `false` mapping to `memory` and anything else to `history`.
20
+
21
+ The package also exports a client side `TrackAccumulator` class that manages the track for a single
22
+ vessel, exposing the result as `Observable<LatLngTuple[]>`.
23
+
24
+ ## Where a track comes from
25
+
26
+ With a history provider installed — [signalk-questdb](https://www.npmjs.com/package/signalk-questdb),
27
+ [signalk-to-influxdb2](https://www.npmjs.com/package/signalk-to-influxdb2) — a track is answered from
28
+ both it and the plugin's own store: the provider is the finer record for as long as its retention
29
+ reaches, and the store is what remains of everything older or of any period the provider missed.
30
+
31
+ Nothing needs configuring for this, and the plugin works exactly as before with no provider
32
+ installed. See [docs/history-and-storage.md](docs/history-and-storage.md) for what that means in
33
+ practice.
34
+
35
+ ## Glitch filtering
36
+
37
+ A receiver occasionally reports a position far from the vessel — a bad almanac, a multipath
38
+ reflection, a unit resetting. On a live map it flickers past. In a stored track it is permanent:
39
+ one spike stretches the bounding box across an ocean and draws a line over the chart every time
40
+ the track is rendered.
41
+
42
+ Positions implying a speed above **Discard positions implying a speed above this (knots)** since
43
+ the previous accepted fix are discarded, defaulting to 100 knots. That is well above any real
44
+ vessel, and glitches usually miss it by orders of magnitude rather than by a little. The test is
45
+ speed rather than distance, so a track resuming after a long gap — a passage with the plugin
46
+ stopped, or an AIS target reappearing — is not filtered. Set it to 0 to record everything.
47
+
48
+ The same check runs over positions loaded from a history provider at startup, since those carry
49
+ the same glitches.
50
+
51
+ ## Pausing while not under way
52
+
53
+ A boat on a mooring for the winter emits a position every second and travels nowhere. Setting
54
+ **Pause recording while navigation.state is one of** stops those months costing any rows.
55
+
56
+ Off by default, and it needs `navigation.state` to be set — by
57
+ [signalk-autostate](https://github.com/meri-imperiumi/signalk-autostate) or by hand. A vessel
58
+ that reports no state is always recorded, so nothing changes for an install that has not opted in.
59
+
60
+ Two limits are deliberate. **`anchored` is offered but rarely wanted**: an anchor alarm watches
61
+ exactly the track a vessel makes while swinging on its rode, so pausing there would break it.
62
+ And **AIS targets are never paused** — their navigational status comes from the transponder and
63
+ is often stale, so a vessel under way still reporting `moored` would otherwise vanish from the
64
+ track.
65
+
66
+ While paused, the plugin says so in its status on the server dashboard.
67
+
68
+ ## Position sources
69
+
70
+ `navigation.position` often arrives from several sources at once — an internal GPS, an AIS
71
+ transponder, a plotter echoing its own fix. Signal K decides which one wins through **source
72
+ priority**, and the stream this plugin records from is already filtered by it, so normally only
73
+ the winning source is stored.
74
+
75
+ When no priority rule matches the path, though, every source comes through. Their fixes are
76
+ metres apart, so the track zigzags between receivers instead of following the boat. If that is
77
+ happening, the plugin says so in its status on the server dashboard, naming the sources it has
78
+ seen. The fix is to set a source priority for `navigation.position` in the server settings.
10
79
 
11
80
  # Usage:
12
81
 
13
- __Retrieve tracks for individual vessel:__
82
+ **Retrieve track for an individual vessel:**
83
+
84
+ `/signalk/v1/api/vessels/<vesselId>/track`
85
+
86
+ _`<vesselId>` may be `self` or a fully qualified context such as `urn:mrn:imo:mmsi:123456789`._
14
87
 
15
- `/signalk/v1/api/tracks/<vesselId>`
88
+ **Retrieve the own vessel's track:**
89
+
90
+ `/signalk/v1/api/self/track`
91
+
92
+ ---
93
+
94
+ **Narrow a track to a time window:**
95
+
96
+ `/signalk/v1/api/self/track?from=2026-08-09T06:00:00Z&to=2026-08-09T12:00:00Z`
97
+
98
+ `/signalk/v1/api/self/track?duration=6h`
99
+
100
+ _`from` and `to` are ISO-8601 timestamps; `to` defaults to now. `duration` is a window
101
+ ending now. A window ending at now includes its most recent point; one ending earlier is
102
+ half-open, so consecutive windows tile without returning the shared point twice._
103
+
104
+ _`timespan` and `timespanOffset` are also accepted for Freeboard-SK compatibility, where
105
+ `timespan=23h&timespanOffset=1` means "23 hours ending an hour ago". They are not part of
106
+ the proposed track API and are expected to be superseded by `from`/`to`._
16
107
 
17
108
  ---
18
109
 
19
- __Retrieve tracks for all vessels:__
110
+ **Reduce the number of points returned:**
111
+
112
+ `/signalk/v1/api/self/track?duration=24h&resolution=5m`
113
+
114
+ _`resolution` is the minimum spacing between returned points. Durations accept a bare
115
+ number of seconds or an `s`/`m`/`h`/`d` suffix. The first and last points are always kept,
116
+ so thinning never shortens the track._
117
+
118
+ ---
119
+
120
+ **Retrieve the time each position was recorded:**
121
+
122
+ `/signalk/v1/api/self/track?duration=6h&times`
123
+
124
+ ```json
125
+ {
126
+ "type": "MultiLineString",
127
+ "coordinates": [
128
+ [
129
+ [24.9, 60.1],
130
+ [25.0, 60.2]
131
+ ]
132
+ ],
133
+ "times": [["2026-08-14T09:00:00.000Z", "2026-08-14T09:01:00.000Z"]],
134
+ "context": "vessels.urn:mrn:imo:mmsi:123456789",
135
+ "isSelf": true
136
+ }
137
+ ```
138
+
139
+ _`times` adds a `times` array positionally aligned with `coordinates`: `times[i][j]` is when
140
+ `coordinates[i][j]` was recorded, as ISO-8601 UTC. It is opt-in because the response grows by
141
+ roughly a third and clients that only draw the geometry have no use for it. Accepts
142
+ `true`/`1`/`yes` and `false`/`0`/`no`; a valueless `?times` reads as true._
143
+
144
+ _`context` is the fully qualified context the track belongs to, and `isSelf` says whether it is
145
+ the own vessel. Asking for `self` resolves the alias, so the response tells you which vessel
146
+ `self` actually is._
147
+
148
+ ---
149
+
150
+ **Retrieve tracks for all vessels:**
20
151
 
21
152
  `/signalk/v1/api/tracks`
22
153
 
23
154
  _If `maxRadius` is specified only vessels with last track position within this distance are returned._
24
155
 
156
+ _Each entry carries `isSelf`, so the own vessel can be told from an AIS target without
157
+ string-matching the context against the server's self identity._
158
+
159
+ _`?times` works here too, adding a `times` array to every vessel's entry. Note that asking for
160
+ times also segments each track on the gap threshold, so `coordinates` and `times` line up;
161
+ without `times` each vessel keeps its single unsegmented line._
162
+
25
163
  ---
26
164
 
27
- __Retrieve tracks for all vessels within a given radius (in meters) from your vessel position:__
165
+ **Retrieve tracks for all vessels within a given radius (in meters) from your vessel position:**
28
166
 
29
167
  `/signalk/v1/api/tracks?radius=50000`
30
168
 
@@ -32,14 +170,43 @@ _Note: This value overrides the `maxRadius` value specified in plugin configurat
32
170
 
33
171
  ---
34
172
 
35
- __Retrieve tracks for all vessels within a bounded area:__
173
+ **Retrieve tracks for all vessels within a bounded area:**
36
174
 
37
175
  `/signalk/v1/api/tracks?bbox=130,-35,139,-33`
38
176
 
39
- _Bounded area is defined as `lon1, lat1, lon2, lat2`_
177
+ _Bounded area is defined as `west, south, east, north` — GeoJSON coordinate order, the
178
+ same as the coordinates this endpoint returns, the Resources API, and the v2 Track API._
40
179
 
41
- _`lon1, lat1` = lower left corner of bounded area_
180
+ _A box crossing the antimeridian is expressed with `west` greater than `east`, for
181
+ example `bbox=175,-10,-175,10`._
42
182
 
43
- _`lon2, lat2` = upper right corner of bounded area_
183
+ > **Changed:** this parameter was `lat1, lon1, lat2, lon2` — latitude first — up to and
184
+ > including 2.0.2. A box in the old order is still four valid numbers, so it will not be
185
+ > rejected; it will simply describe a different area. Freeboard-SK is unaffected, as it
186
+ > filters by `radius` rather than `bbox`.
44
187
 
45
188
  ---
189
+
190
+ # Development
191
+
192
+ ```bash
193
+ npm ci
194
+ npm run build # vite library build -> dist/
195
+ npm test # vitest
196
+ npm run test:e2e # against a real signalk-server; see below
197
+ npm run typecheck # tsc --noEmit
198
+ npm run lint # eslint
199
+ npm run format # prettier --write
200
+ ```
201
+
202
+ `npm run test:e2e` packs the plugin, installs it into a throwaway config directory, boots a real
203
+ Signal K server against it and feeds positions as deltas — so it covers plugin loading, route
204
+ mounting and the delta path, none of which the unit suite can. It needs a built server checkout;
205
+ set `SIGNALK_SERVER_DIR` if yours is not at `~/dev/xxx_signalk-server`. A second tier installs a
206
+ real history provider (signalk-questdb) into that server and exercises the startup bootstrap
207
+ through it; it skips itself if no QuestDB is reachable at `QUESTDB_URL`. Neither tier runs in CI.
208
+
209
+ The package is ESM only and targets Node >= 22.5.0, the release that added `node:sqlite`. ESM alone
210
+ would only need 20.19, the first release in which the Signal K server's `require()`-based plugin
211
+ loader can load an ES module, but the sqlite track source raises the floor. The server itself
212
+ requires Node >= 22, so this rules out nothing that could have run the plugin anyway.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ /** QuestDB endpoint used by the history-provider tier. */
2
+ export declare const QUESTDB_URL: string;
3
+ export interface E2EServer {
4
+ url: string;
5
+ configDir: string;
6
+ /** Send a position delta as a provider would, over the WebSocket API. */
7
+ feed: (context: string, position: [number, number], timestamp?: number, source?: string) => Promise<void>;
8
+ /** GET a path under /signalk/v1/api and parse the JSON. */
9
+ api: (path: string) => Promise<unknown>;
10
+ /**
11
+ * GET a path under /signalk/v2/api, keeping the status.
12
+ *
13
+ * The status is what distinguishes "no provider registered" (501) from an
14
+ * answered query, which is the thing a provider registration test is about.
15
+ */
16
+ apiV2: (path: string) => Promise<{
17
+ status: number;
18
+ body: unknown;
19
+ }>;
20
+ /** The server's own vessel context, as it resolved it. */
21
+ selfContext: string;
22
+ stop: () => void;
23
+ }
24
+ export interface E2EOptions {
25
+ /** Plugin configuration written to plugin-config-data/tracks.json. */
26
+ config?: Record<string, unknown>;
27
+ /** Seconds to wait for the server to answer before giving up. */
28
+ timeoutSeconds?: number;
29
+ /**
30
+ * Other plugins to install and enable alongside this one, as
31
+ * `{ 'npm-package-name': pluginConfiguration }`.
32
+ *
33
+ * Used to stand up a real history provider so the bootstrap can be exercised
34
+ * through `getHistoryApi()` — the interface this plugin actually depends on —
35
+ * rather than against a provider's private storage.
36
+ */
37
+ plugins?: Record<string, Record<string, unknown>>;
38
+ /** Port to boot on, when a test needs its own server. */
39
+ port?: number;
40
+ }
41
+ export declare function startServer(options?: E2EOptions): Promise<E2EServer>;
42
+ /**
43
+ * Whether a QuestDB is listening, so the provider tier can skip cleanly.
44
+ *
45
+ * A liveness probe only. What the provider stores, and how, is its own
46
+ * business: this plugin reaches it through `getHistoryApi()` and must keep
47
+ * working across any change to that.
48
+ */
49
+ export declare function questdbAvailable(): Promise<boolean>;
@@ -0,0 +1,51 @@
1
+ import { Context, LatLngTuple } from './types.js';
2
+ /**
3
+ * Drops positions that imply an impossible speed since the last accepted one.
4
+ *
5
+ * A GPS occasionally emits a fix hundreds of miles away — a bad almanac, a
6
+ * multipath reflection, a receiver resetting. On a live map it flickers past.
7
+ * In a stored track it is permanent: one spike stretches the bounding box
8
+ * across an ocean, inflates the distance travelled, and drags the line across
9
+ * the chart every time the track is drawn.
10
+ *
11
+ * Speed, not distance, is the test. A vessel can legitimately jump a long way
12
+ * between two fixes if the gap between them is long enough — after a passage
13
+ * with the plugin stopped, or when a slow-reporting AIS target reappears. What
14
+ * cannot happen is covering that distance faster than a vessel moves.
15
+ *
16
+ * Requested in https://github.com/SignalK/tracks/issues/48
17
+ */
18
+ /**
19
+ * Default ceiling in knots.
20
+ *
21
+ * Above any displacement vessel and most planing craft, and well above the
22
+ * ~50kn an AIS Class A target reports at the top of its scale, so a real fix is
23
+ * never dropped. Glitches are typically wrong by whole degrees of latitude, so
24
+ * they miss this by orders of magnitude rather than by a little.
25
+ */
26
+ export declare const DEFAULT_MAX_SPEED_KNOTS = 100;
27
+ export interface GlitchFilterConfig {
28
+ /** Ceiling in knots. 0 or below disables the filter. */
29
+ maxSpeedKnots: number;
30
+ }
31
+ export declare class GlitchFilter {
32
+ private readonly last;
33
+ private readonly maxSpeedKnots;
34
+ /** Rejections per context, for the plugin status line. */
35
+ private readonly rejected;
36
+ constructor(config: GlitchFilterConfig);
37
+ /**
38
+ * Whether to keep this position.
39
+ *
40
+ * Accepting updates the reference point, so a sustained move — a genuinely
41
+ * fast vessel, or a track resuming after a gap — is not fought after the
42
+ * first fix.
43
+ */
44
+ accept(context: Context, position: LatLngTuple, timestamp: number): boolean;
45
+ /** How many positions have been rejected for a context. */
46
+ rejectedCount(context: Context): number;
47
+ /** Total rejections across every context. */
48
+ totalRejected(): number;
49
+ /** Forget every reference point; used when the plugin restarts. */
50
+ clear(): void;
51
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ import { Express } from 'express';
2
+ import { TrackApi } from './trackApi.js';
3
+ import { LatLngTuple } from './types.js';
4
+ /**
5
+ * Test harness that stands the plugin up behind a real express app, mounted the
6
+ * way the Signal K server mounts it (`app.use('/signalk/v1/api', …)`), so route
7
+ * patterns and parameter parsing are exercised rather than mocked.
8
+ *
9
+ * Note the server runs express 4; `express` is pinned to ^4 in devDependencies
10
+ * so the wildcard route syntax under test matches production.
11
+ */
12
+ export declare const SELF_CONTEXT = "vessels.urn:mrn:imo:mmsi:123456789";
13
+ export declare const OTHER_CONTEXT = "vessels.urn:mrn:imo:mmsi:987654321";
14
+ export interface TestHarness {
15
+ app: Express;
16
+ /**
17
+ * Feed a position delta as the streambundle would. `timestamp` dates the
18
+ * point, so time-window queries can be tested without faking the clock.
19
+ *
20
+ * Note `throttleTime` thins on the leading edge against the wall clock, so a
21
+ * synchronous burst yields a single point regardless of the timestamps given.
22
+ * Use `seedTrack` to install a back-dated track instead.
23
+ */
24
+ emit: (context: string, position: LatLngTuple, timestamp?: number, source?: string) => void;
25
+ /** Status messages the plugin has pushed to the server dashboard. */
26
+ statuses: string[];
27
+ /**
28
+ * Install a track with explicit timestamps, bypassing the throttled bus. This
29
+ * is the entry point the History API bootstrap uses.
30
+ */
31
+ seedTrack: (context: string, positions: LatLngTuple[], timestamps: number[]) => void;
32
+ /** Self position as reported by `getSelfPath`, used for radius filtering. */
33
+ setSelfPosition: (position: LatLngTuple | undefined) => void;
34
+ /** The own vessel's navigation.state, as reported by `getSelfPath`. */
35
+ setSelfState: (state: string | undefined) => void;
36
+ stop: () => void;
37
+ errors: unknown[][];
38
+ /**
39
+ * The v2 Track API provider the plugin registered, or undefined when the
40
+ * server offered no `registerTrackApiProvider`.
41
+ */
42
+ trackProvider: () => TrackApi | undefined;
43
+ /** How many times the plugin registered a provider. */
44
+ registrations: () => number;
45
+ }
46
+ export interface HarnessOptions {
47
+ /** Plugin config overrides. Bootstrap is disabled by default so tests stay hermetic. */
48
+ config?: Record<string, unknown>;
49
+ selfContext?: string;
50
+ selfPosition?: LatLngTuple;
51
+ /** Initial navigation.state for the own vessel. */
52
+ selfState?: string;
53
+ /**
54
+ * Stand in for a server without the v2 Track API, to check the plugin still
55
+ * starts when `registerTrackApiProvider` is absent.
56
+ */
57
+ withoutTrackApi?: boolean;
58
+ }
59
+ export declare function createHarness(options?: HarnessOptions): TestHarness;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,135 @@
1
+ import { Temporal } from '@js-temporal/polyfill';
2
+ import { Router } from 'express';
3
+ import { TrackApi } from './trackApi.js';
4
+ import { TrackStore } from './store.js';
5
+ import { Context, Debug, Position } from './types.js';
6
+ export interface ContextPosition {
7
+ context: Context;
8
+ value: Position;
9
+ /** ISO-8601 time the value was recorded, as carried by the Signal K delta. */
10
+ timestamp?: string;
11
+ /**
12
+ * Which source produced the value.
13
+ *
14
+ * The bus is priority-filtered, so normally this is the one source the
15
+ * server selected. Seeing several for one context means no priority rule is
16
+ * matching the path — which is what `SourceWatch` reports on.
17
+ */
18
+ $source?: string;
19
+ }
20
+ interface HistoryValuesQuery {
21
+ context: string;
22
+ from: Temporal.Instant;
23
+ to: Temporal.Instant;
24
+ pathSpecs: {
25
+ path: string;
26
+ aggregate: string;
27
+ }[];
28
+ resolution: number;
29
+ }
30
+ interface HistoryApi {
31
+ getValues(query: HistoryValuesQuery): Promise<HistoryValuesResponse>;
32
+ }
33
+ interface HistoryValuesResponse {
34
+ context: string;
35
+ range: {
36
+ from: string;
37
+ to: string;
38
+ };
39
+ values: unknown[];
40
+ /** Each element: [timestamp_string, [lon, lat]] */
41
+ data: unknown[];
42
+ }
43
+ interface App {
44
+ debug: Debug;
45
+ error: (...args: unknown[]) => void;
46
+ streambundle: {
47
+ getBus: (path: string) => {
48
+ onValue: (cb: (x: ContextPosition) => void) => () => void;
49
+ };
50
+ };
51
+ getSelfPath: (path: string) => unknown;
52
+ selfContext: string;
53
+ /** Shown against the plugin in the server's dashboard. Absent on older servers. */
54
+ setPluginStatus?: (msg: string) => void;
55
+ /** Plugin-private directory for persistent data; absent on older servers. */
56
+ getDataDirPath?: () => string;
57
+ /** Resolves the named provider, or the configured default when omitted. */
58
+ getHistoryApi?: (providerId?: string) => Promise<HistoryApi>;
59
+ /**
60
+ * Offer this plugin's tracks to the v2 Track API. Absent on servers older
61
+ * than SignalK/signalk-server#2995.
62
+ *
63
+ * The server unregisters the provider itself when the plugin stops, so
64
+ * `stop()` here must not do it a second time.
65
+ */
66
+ registerTrackApiProvider?: (provider: TrackApi) => void;
67
+ config?: {
68
+ settings?: {
69
+ historyApi?: {
70
+ defaultProvider?: string;
71
+ };
72
+ };
73
+ };
74
+ }
75
+ interface Plugin {
76
+ start: (c: TracksPluginConfig) => void;
77
+ stop: () => void;
78
+ signalKApiRoutes: (r: Router) => Router;
79
+ id: string;
80
+ name: string;
81
+ description: string;
82
+ schema: Record<string, unknown>;
83
+ /**
84
+ * The live accumulator, or undefined before `start()`.
85
+ *
86
+ * Exposed so a track can be installed or inspected without going through the
87
+ * position bus, which throttles against the wall clock. The Signal K server
88
+ * does not use this.
89
+ */
90
+ getTracks: () => TrackStore | undefined;
91
+ }
92
+ /**
93
+ * Where tracks come from when the server starts.
94
+ *
95
+ * - `memory`: start empty, accumulate from the position bus only.
96
+ * - `history`: refill from a History provider at startup, then accumulate.
97
+ * - `sqlite`: persist to disk, so a restart resumes from what was recorded.
98
+ *
99
+ * One setting rather than a boolean per source: they are alternatives, and as
100
+ * independent booleans a user could enable two and have the same positions
101
+ * loaded twice.
102
+ */
103
+ export type TrackSource = 'memory' | 'history' | 'sqlite';
104
+ interface TracksPluginConfig {
105
+ resolution?: number;
106
+ pointsToKeep?: number;
107
+ maxAge?: number;
108
+ maxRadius?: number;
109
+ source?: TrackSource;
110
+ /** @deprecated superseded by `source`; still honoured for existing configs. */
111
+ bootstrapFromHistory?: boolean;
112
+ /** Days of history to keep when `source` is `sqlite`. 0 keeps everything. */
113
+ retentionDays?: number;
114
+ /** Minutes without a fix that start a new track segment. 0 disables. */
115
+ segmentGapMinutes?: number;
116
+ /** Speed above which a position is treated as a glitch. 0 disables. */
117
+ maxSpeedKnots?: number;
118
+ /** navigation.state values that pause recording of the own vessel. */
119
+ pauseWhenState?: string[];
120
+ }
121
+ /**
122
+ * Resolve the effective source, honouring the setting this replaced.
123
+ *
124
+ * `bootstrapFromHistory` shipped as a boolean defaulting to true, so an
125
+ * installed plugin has it saved in its config either way. Reading `source`
126
+ * first and only then falling back keeps those installs working: an explicit
127
+ * `false` still means "do not touch the History API", and everything else
128
+ * lands on the old default of `history`.
129
+ */
130
+ export declare const resolveSource: (config: TracksPluginConfig) => TrackSource;
131
+ export default function ThePlugin(app: App): Plugin;
132
+ export { Tracks, TrackAccumulator } from './tracks.js';
133
+ export type { TracksConfig } from './tracks.js';
134
+ export type { TrackStore } from './store.js';
135
+ export type * from './types.js';