@signalk/tracks-plugin 3.0.0-beta.5 → 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/app-icon.svg +42 -0
- package/dist/index.js +123 -11
- package/dist/index.js.map +1 -1
- package/dist/sqliteStore.d.ts +4 -0
- package/dist/store.d.ts +15 -0
- package/dist/timeWindow.d.ts +1 -1
- package/dist/tracks.d.ts +10 -0
- package/dist/utils.d.ts +21 -0
- package/docs/history-and-storage.md +94 -0
- package/docs/screenshots/tracks_1.png +0 -0
- package/package.json +7 -2
package/dist/sqliteStore.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface SqliteStoreConfig {
|
|
|
34
34
|
export declare class SqliteTrackStore implements TrackStore {
|
|
35
35
|
private readonly db;
|
|
36
36
|
private readonly insert;
|
|
37
|
+
private readonly insertName;
|
|
38
|
+
private readonly selectName;
|
|
37
39
|
private readonly maxCells;
|
|
38
40
|
private readonly segmentGap;
|
|
39
41
|
private readonly retention;
|
|
@@ -92,6 +94,8 @@ export declare class SqliteTrackStore implements TrackStore {
|
|
|
92
94
|
* straight line across the anchorage.
|
|
93
95
|
*/
|
|
94
96
|
segments(context: Context, window?: TimeWindow): LatLngTuple[][];
|
|
97
|
+
recordName(context: Context, name: string, timestamp?: number): void;
|
|
98
|
+
nameFor(context: Context): string | undefined;
|
|
95
99
|
prune(maxAge: number, keep?: Context): void;
|
|
96
100
|
close(): void;
|
|
97
101
|
/** Unsigned round-trip check, used by the tests. */
|
package/dist/store.d.ts
CHANGED
|
@@ -51,6 +51,21 @@ export interface TrackStore {
|
|
|
51
51
|
* "vessels currently near here" rather than "vessels that ever passed here".
|
|
52
52
|
*/
|
|
53
53
|
getFilteredTracks(params: TrackParams, selfPosition?: LatLngTuple, debug?: Debug, query?: TrackQuery): Promise<TrackCollection>;
|
|
54
|
+
/**
|
|
55
|
+
* Remember the name a context is currently known by.
|
|
56
|
+
*
|
|
57
|
+
* Names are resolved live from the server's data model on every request,
|
|
58
|
+
* because an AIS static report can arrive long after the first position. But
|
|
59
|
+
* the model is empty after a restart, so a track recorded for months would
|
|
60
|
+
* show as a bare MMSI until the vessel next transmits — and never again for
|
|
61
|
+
* one that has since left. Storing what was known closes that gap.
|
|
62
|
+
*
|
|
63
|
+
* `timestamp` dates the observation so a later report replaces an earlier
|
|
64
|
+
* one rather than the first sighting winning forever.
|
|
65
|
+
*/
|
|
66
|
+
recordName(context: Context, name: string, timestamp?: number): void;
|
|
67
|
+
/** The last name stored for a context, or undefined if none ever was. */
|
|
68
|
+
nameFor(context: Context): string | undefined;
|
|
54
69
|
/**
|
|
55
70
|
* Drop contexts whose newest position is older than `maxAge` ms.
|
|
56
71
|
*
|
package/dist/timeWindow.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface TrackQuery {
|
|
|
24
24
|
* `timespan`/`timespanOffset` pair. Absent all of them, no window is applied
|
|
25
25
|
* and the whole retained track is returned, as before.
|
|
26
26
|
*/
|
|
27
|
-
export declare function parseTrackQuery(query: Record<string, unknown>, now?: number): TrackQuery;
|
|
27
|
+
export declare function parseTrackQuery(query: Record<string, unknown>, now?: number, alsoAllowed?: readonly string[]): TrackQuery;
|
|
28
28
|
/**
|
|
29
29
|
* Drop points closer together than `resolution` milliseconds, always keeping
|
|
30
30
|
* the first and last so the track keeps its extent.
|
package/dist/tracks.d.ts
CHANGED
|
@@ -47,6 +47,16 @@ export declare class Tracks implements TrackStore {
|
|
|
47
47
|
* which tracks match — the filter runs once, here.
|
|
48
48
|
*/
|
|
49
49
|
getFilteredTimedTracks(params: TrackParams, selfPosition?: LatLngTuple, debug?: Debug, query?: TrackQuery): Promise<TimedTrackCollection>;
|
|
50
|
+
/**
|
|
51
|
+
* Remembered names, by context.
|
|
52
|
+
*
|
|
53
|
+
* In memory only: this store is the client-side accumulator, so there is no
|
|
54
|
+
* restart for a stored name to survive. It implements the same contract so
|
|
55
|
+
* the two stores stay interchangeable and the plugin needs no special case.
|
|
56
|
+
*/
|
|
57
|
+
private readonly names;
|
|
58
|
+
recordName(context: Context, name: string, timestamp?: number): void;
|
|
59
|
+
nameFor(context: Context): string | undefined;
|
|
50
60
|
prune(maxAge: number, keep?: Context): void;
|
|
51
61
|
}
|
|
52
62
|
interface AccumulatorParams {
|
package/dist/utils.d.ts
CHANGED
|
@@ -25,6 +25,25 @@ export declare function createInBounds(bounds: GeoBounds): (position: LatLngTupl
|
|
|
25
25
|
* iterate the result.
|
|
26
26
|
*/
|
|
27
27
|
export declare function splitAtAntimeridian(bounds: GeoBounds): GeoBounds[];
|
|
28
|
+
/**
|
|
29
|
+
* A query parameter was present but malformed.
|
|
30
|
+
*
|
|
31
|
+
* Distinct from absent: a filter nobody asked for is not an error, but one
|
|
32
|
+
* that cannot be honoured must not quietly widen the query. Carries its own
|
|
33
|
+
* message so the handler can hand it to the client, the way `TimeWindowError`
|
|
34
|
+
* already does for the time window.
|
|
35
|
+
*/
|
|
36
|
+
export declare class QueryParameterError extends Error {
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A well-formed query the server cannot currently answer.
|
|
40
|
+
*
|
|
41
|
+
* `radius` measures from the own vessel, so it needs a self position. Not
|
|
42
|
+
* having one is an outage, not a client mistake -- the same distinction the
|
|
43
|
+
* single-vessel route already draws for a history provider it cannot read.
|
|
44
|
+
*/
|
|
45
|
+
export declare class SelfPositionUnavailableError extends Error {
|
|
46
|
+
}
|
|
28
47
|
export declare function validateParameters(params: QueryParameters, defaultMaxRadius: number | undefined): TrackParams;
|
|
29
48
|
export declare function createDistanceTo([lat1d, lon1d]: LatLngTuple, debug?: Debug): (d: LatLngTuple | null) => number;
|
|
30
49
|
export declare function resolveContext(vesselId: string, selfContext?: string): string;
|
|
@@ -70,6 +89,8 @@ export declare function contextName(context: string, lookup: (path: string) => u
|
|
|
70
89
|
* context, because a label has to render something.
|
|
71
90
|
*/
|
|
72
91
|
export declare function trackLabel(context: string, selfContext: string | undefined, lookup: (path: string) => unknown): string;
|
|
92
|
+
/** Accepts both a bare string and the `{value}` wrapper deltas arrive in. */
|
|
93
|
+
export declare function unwrapString(raw: unknown): string | undefined;
|
|
73
94
|
/**
|
|
74
95
|
* A track label as a filename a browser can save.
|
|
75
96
|
*
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Where a track comes from
|
|
2
|
+
|
|
3
|
+
The plugin can answer a query from two places: its own store, and a history
|
|
4
|
+
provider such as [signalk-questdb](https://www.npmjs.com/package/signalk-questdb)
|
|
5
|
+
or [signalk-to-influxdb2](https://www.npmjs.com/package/signalk-to-influxdb2).
|
|
6
|
+
Neither is simply better than the other, which is why both are used.
|
|
7
|
+
|
|
8
|
+
| | The plugin's store | A history provider |
|
|
9
|
+
| ---------------------------- | ---------------------------------- | ------------------------------------------------ |
|
|
10
|
+
| How often a position is kept | coarse, at a configurable interval | fine, at whatever interval it was configured for |
|
|
11
|
+
| How long it is kept | see below | until its retention drops it |
|
|
12
|
+
| Size | small | far larger, which is why retention exists |
|
|
13
|
+
|
|
14
|
+
The plugin writes its positions to a SQLite file in its data directory, so they
|
|
15
|
+
survive a restart. How long they are kept depends on two settings: the own
|
|
16
|
+
vessel's track is kept indefinitely by default, and another vessel is dropped
|
|
17
|
+
30 days after its last fix.
|
|
18
|
+
|
|
19
|
+
So the provider is the finer record of the recent past, and the plugin's store
|
|
20
|
+
is what remains of everything older. The interval and the retention are both
|
|
21
|
+
settings on the plugin's configuration page, which is where their current
|
|
22
|
+
defaults are shown.
|
|
23
|
+
|
|
24
|
+
## What you get with no history provider
|
|
25
|
+
|
|
26
|
+
Everything comes from the plugin's own store, at whatever interval it is
|
|
27
|
+
configured to keep, for as far back as it has been running. Nothing else is needed, and no other plugin has to
|
|
28
|
+
be installed.
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
query window
|
|
32
|
+
├──────────────────────────────────────────────────┤
|
|
33
|
+
│ plugin store, at the configured interval │
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## What you get with one
|
|
37
|
+
|
|
38
|
+
The provider answers for the period it covers, and the plugin's store fills
|
|
39
|
+
everything else.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
query window: last two years
|
|
43
|
+
├───────────────────────────────────────┬──────────┤
|
|
44
|
+
│ plugin store, coarse │ provider │
|
|
45
|
+
│ │ finer │
|
|
46
|
+
└ retention begins
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A track can therefore change granularity partway along: coarse where it came
|
|
50
|
+
from the store, fine where the provider reached. That is expected. Passing
|
|
51
|
+
`resolution` on a query thins the result to a spacing you choose, which is the
|
|
52
|
+
way to get an evenly spaced track regardless of where each part came from.
|
|
53
|
+
|
|
54
|
+
## When the provider has holes
|
|
55
|
+
|
|
56
|
+
Retention is only one way a provider's coverage can be incomplete. All of these
|
|
57
|
+
happen, and all are handled the same way:
|
|
58
|
+
|
|
59
|
+
- it was installed after the boat had already been recording
|
|
60
|
+
- it was disabled for a while, or its database was down
|
|
61
|
+
- its retention has dropped the older data
|
|
62
|
+
|
|
63
|
+
In each case the provider simply returns nothing for that period, and the
|
|
64
|
+
plugin's store supplies it instead:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
├─────────┬──────────────┬─────────┬──────────────┤
|
|
68
|
+
│ store │ provider │ store │ provider │
|
|
69
|
+
│ │ │ ↑ provider was down │
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Nothing has to be configured for this. The plugin does not read the provider's
|
|
73
|
+
retention setting, or ask how long it has been running — it asks for the window
|
|
74
|
+
and uses what comes back, so coverage that changes underneath needs no
|
|
75
|
+
attention.
|
|
76
|
+
|
|
77
|
+
## Why the two never double up
|
|
78
|
+
|
|
79
|
+
A provider aggregates into buckets and stamps each one on its boundary:
|
|
80
|
+
`19:00:00.000`, `19:01:00.000`. The plugin's store keeps the time a fix
|
|
81
|
+
actually arrived: `19:00:01.212`. The same physical position therefore has two
|
|
82
|
+
different timestamps in the two sources, and simply merging them would keep
|
|
83
|
+
both.
|
|
84
|
+
|
|
85
|
+
Instead, each bucket of time is filled from exactly one source: the provider
|
|
86
|
+
where it has a position, the store everywhere else. Individual points are never
|
|
87
|
+
compared, so there is nothing to get wrong.
|
|
88
|
+
|
|
89
|
+
## If a vessel was not moving
|
|
90
|
+
|
|
91
|
+
Neither source has anything, and the track is correctly empty for that period
|
|
92
|
+
rather than filled in. A gap in a track means the vessel was not being
|
|
93
|
+
recorded — which, with the **Pause recording while navigation.state is one of**
|
|
94
|
+
setting, may be deliberate.
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signalk/tracks-plugin",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.7",
|
|
4
4
|
"description": "Plugin to keep track of tracks of vessels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -14,12 +14,17 @@
|
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
15
|
"//files": "app-icon.svg ships alongside dist because `files` is an allowlist: the app store probes the declared signalk.appIcon path inside the published tarball, so an icon left out here 404s and the plugin falls back to a placeholder.",
|
|
16
16
|
"files": [
|
|
17
|
+
"app-icon.svg",
|
|
17
18
|
"dist",
|
|
19
|
+
"docs",
|
|
18
20
|
"public"
|
|
19
21
|
],
|
|
20
22
|
"signalk": {
|
|
21
23
|
"displayName": "Tracks",
|
|
22
|
-
"appIcon": "./app-icon.svg"
|
|
24
|
+
"appIcon": "./app-icon.svg",
|
|
25
|
+
"screenshots": [
|
|
26
|
+
"./docs/screenshots/tracks_1.png"
|
|
27
|
+
]
|
|
23
28
|
},
|
|
24
29
|
"keywords": [
|
|
25
30
|
"signalk-node-server-plugin",
|