@signalk/tracks-plugin 3.2.0 → 3.4.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 +37 -15
- package/dist/asyncTrackStore.d.ts +5 -4
- package/dist/import.js +27 -6
- package/dist/importedTracks.d.ts +2 -4
- package/dist/index.html +4 -0
- package/dist/index.js +266 -69
- package/dist/index.js.map +1 -1
- package/dist/sqliteStore.d.ts +12 -3
- package/dist/store.d.ts +8 -3
- package/dist/trackApi.d.ts +2 -0
- package/dist/trackProvider.d.ts +62 -2
- package/dist/trackStoreProtocol.d.ts +1 -1
- package/dist/trackStoreWorker.js +40 -3
- package/dist/trackStoreWorker.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/docs/history-and-storage.md +6 -1
- package/package.json +1 -1
- package/public/import.js +27 -6
- package/public/index.html +4 -0
package/dist/sqliteStore.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TrackQuery } from './timeWindow.js';
|
|
2
2
|
import { TrackStore } from './store.js';
|
|
3
|
-
import { BinEntry, Context, Debug, ImportedTrack, ImportFilter, LatLngTuple, RecordedSpan, RestoreResult, TimedPosition, TimedTrackCollection, TimeWindow, TrackCollection, TrackParams } from './types.js';
|
|
3
|
+
import { BinEntry, Context, Debug, ImportedTrack, StoredImport, ImportFilter, LatLngTuple, RecordedSpan, RestoreResult, TimedPosition, TimedTrackCollection, TimeWindow, TrackCollection, TrackParams } from './types.js';
|
|
4
4
|
export declare class TransactionStateError extends Error {
|
|
5
5
|
}
|
|
6
6
|
export interface SqliteStoreConfig {
|
|
@@ -132,6 +132,15 @@ export declare class SqliteTrackStore implements TrackStore {
|
|
|
132
132
|
* same id fails rather than replacing the first.
|
|
133
133
|
*/
|
|
134
134
|
storeImport(track: ImportedTrack): void;
|
|
135
|
+
/**
|
|
136
|
+
* Write a track imported for a vessel into its recording, in place of what
|
|
137
|
+
* was recorded over the same time. The positions within each segment's
|
|
138
|
+
* span go to the recycle bin first, one entry per segment, so a restore
|
|
139
|
+
* brings them back beside the import. Every span is binned before any point
|
|
140
|
+
* is written, so segments that overlap in time cannot bin each other.
|
|
141
|
+
* Resolves to the bin entries made.
|
|
142
|
+
*/
|
|
143
|
+
recordTrack(context: Context, segments: TimedPosition[][], deletedAt: number, keepEmpty: boolean): number[];
|
|
135
144
|
/**
|
|
136
145
|
* Move a vessel's positions recorded within `[from, to]` into the recycle
|
|
137
146
|
* bin, resolving to the entry's id and whether the store held the vessel at
|
|
@@ -176,9 +185,9 @@ export declare class SqliteTrackStore implements TrackStore {
|
|
|
176
185
|
* earlier with it, since they have no track left to be restored into.
|
|
177
186
|
*/
|
|
178
187
|
private purge;
|
|
179
|
-
getImport(id: string):
|
|
188
|
+
getImport(id: string): StoredImport | undefined;
|
|
180
189
|
/** Imported tracks the filter lets through, in the order they were stored. */
|
|
181
|
-
findImports(filter?: ImportFilter):
|
|
190
|
+
findImports(filter?: ImportFilter): StoredImport[];
|
|
182
191
|
private importFrom;
|
|
183
192
|
/** Worker startup snapshot; queries and name lookups must not open a second database. */
|
|
184
193
|
storedNames(): {
|
package/dist/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BinEntry, Context, Debug, ImportedTrack, ImportFilter, LatLngTuple, RecordedSpan, RestoreResult, TimedPosition, TimedTrackCollection, TimeWindow, TrackCollection, TrackParams } from './types.js';
|
|
1
|
+
import { BinEntry, Context, Debug, ImportedTrack, StoredImport, ImportFilter, LatLngTuple, RecordedSpan, RestoreResult, TimedPosition, TimedTrackCollection, TimeWindow, TrackCollection, TrackParams } from './types.js';
|
|
2
2
|
import { TrackQuery } from './timeWindow.js';
|
|
3
3
|
/**
|
|
4
4
|
* A track store: somewhere positions are accumulated and queried back.
|
|
@@ -96,8 +96,8 @@ export interface TrackStore {
|
|
|
96
96
|
* back at once.
|
|
97
97
|
*/
|
|
98
98
|
storeImport?(track: ImportedTrack): void | Promise<void>;
|
|
99
|
-
getImport?(id: string):
|
|
100
|
-
findImports?(filter?: ImportFilter):
|
|
99
|
+
getImport?(id: string): StoredImport | undefined | Promise<StoredImport | undefined>;
|
|
100
|
+
findImports?(filter?: ImportFilter): StoredImport[] | Promise<StoredImport[]>;
|
|
101
101
|
/**
|
|
102
102
|
* The recycle bin. Optional like the import methods: a store without it
|
|
103
103
|
* cannot delete. A delete moves rows into the bin rather than removing them,
|
|
@@ -113,6 +113,11 @@ export interface TrackStore {
|
|
|
113
113
|
id?: number;
|
|
114
114
|
known: boolean;
|
|
115
115
|
}>;
|
|
116
|
+
/**
|
|
117
|
+
* Write a track imported for a vessel into its recording, binning what was
|
|
118
|
+
* recorded over the same time; resolves to the bin entries made.
|
|
119
|
+
*/
|
|
120
|
+
recordTrack?(context: Context, segments: TimedPosition[][], deletedAt: number, keepEmpty: boolean): number[] | Promise<number[]>;
|
|
116
121
|
binImport?(importId: string, span: {
|
|
117
122
|
from?: number;
|
|
118
123
|
to?: number;
|
package/dist/trackApi.d.ts
CHANGED
|
@@ -59,6 +59,8 @@ export interface TrackProperties {
|
|
|
59
59
|
contextName?: string;
|
|
60
60
|
/** The track's own name, as an imported GPX `<trk><name>` carries. */
|
|
61
61
|
name?: string;
|
|
62
|
+
/** When a client stored the track, as an ISO 8601 time; absent on a recorded one. */
|
|
63
|
+
imported?: string;
|
|
62
64
|
/** Absent, with `to`, for a track whose points carry no times. */
|
|
63
65
|
from?: string;
|
|
64
66
|
to?: string;
|
package/dist/trackProvider.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TrackStore } from './store.js';
|
|
2
2
|
import { TrackApi } from './trackApi.js';
|
|
3
|
-
import { TimedPosition, TimeWindow } from './types.js';
|
|
3
|
+
import { ImportedTrack, TimedPosition, TimeWindow } from './types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Serves the v2 Track API from this plugin's store.
|
|
6
6
|
*
|
|
@@ -54,5 +54,65 @@ export interface TrackProviderDeps {
|
|
|
54
54
|
/** Called after a delete, so whatever hides deleted spans from history reloads them. */
|
|
55
55
|
deleted: () => void;
|
|
56
56
|
reconcileWithHistory: (context: string, stored: TimedPosition[], window: TimeWindow | undefined, resolutionMs: number | undefined) => Promise<TimedPosition[]>;
|
|
57
|
+
/**
|
|
58
|
+
* What the store and history hold for a context over a window, rejecting
|
|
59
|
+
* when history could not be read: an import that fills only the gaps
|
|
60
|
+
* cannot tell a gap from an outage.
|
|
61
|
+
*/
|
|
62
|
+
recordedOver: (context: string, window: TimeWindow) => Promise<TimedPosition[]>;
|
|
63
|
+
}
|
|
64
|
+
/** The Track API provider, plus the import route's way in with an overlap choice. */
|
|
65
|
+
export type TrackProvider = TrackApi & {
|
|
66
|
+
importTrack(track: ImportedTrack, onOverlap: OnOverlap): Promise<KeptTrack>;
|
|
67
|
+
};
|
|
68
|
+
export declare function createTrackProvider(deps: TrackProviderDeps): TrackProvider;
|
|
69
|
+
/**
|
|
70
|
+
* What an import does where the vessel's recording already holds positions.
|
|
71
|
+
* `keep` leaves them alone and fills only the stretches with nothing; `replace`
|
|
72
|
+
* puts the import in their place, sending them to the recycle bin.
|
|
73
|
+
*/
|
|
74
|
+
export type OnOverlap = 'keep' | 'replace';
|
|
75
|
+
/**
|
|
76
|
+
* The shortest stretch without positions that an import kept beside a
|
|
77
|
+
* recording may fill. Filling every pause between recorded points would
|
|
78
|
+
* interleave two sources minute by minute, and any offset between them would
|
|
79
|
+
* draw as a sawtooth; whole stretches keep each part of the line from one.
|
|
80
|
+
*/
|
|
81
|
+
export declare const MIN_FILLED_GAP_MS: number;
|
|
82
|
+
export interface KeepTrackDeps {
|
|
83
|
+
store: Required<Pick<TrackStore, 'storeImport' | 'recordTrack'>>;
|
|
84
|
+
selfContext: string;
|
|
85
|
+
historyKnows: (context: string) => Promise<boolean>;
|
|
86
|
+
/** The vessel's positions over a window, from the store and history alike. */
|
|
87
|
+
recorded: (context: string, from: number, to: number) => Promise<TimedPosition[]>;
|
|
88
|
+
deleted: () => void;
|
|
89
|
+
}
|
|
90
|
+
export interface KeptTrack {
|
|
91
|
+
id: string;
|
|
92
|
+
/** Points left out because the vessel already had positions there. */
|
|
93
|
+
overlapping: number;
|
|
57
94
|
}
|
|
58
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Keep a client's track, resolving to the id that addresses it.
|
|
97
|
+
*
|
|
98
|
+
* A timed track of the own vessel is the vessel's own passage, from before
|
|
99
|
+
* the plugin recorded it or from another plotter, so it is written into the
|
|
100
|
+
* recording and answers as `recorded:<context>`: with `keep`, only where the
|
|
101
|
+
* recording has nothing (see {@link fillGaps}); with `replace`, in place of
|
|
102
|
+
* what was recorded over the same time. Everything else is kept as an import:
|
|
103
|
+
* another vessel's track is not this boat's recording, and an untimed one
|
|
104
|
+
* cannot be placed in it.
|
|
105
|
+
*/
|
|
106
|
+
export declare function keepTrack(track: ImportedTrack, deps: KeepTrackDeps, onOverlap: OnOverlap): Promise<KeptTrack>;
|
|
107
|
+
/**
|
|
108
|
+
* The parts of an import that fall where a recording has nothing for at least
|
|
109
|
+
* `minGap`, as runs to record, and how many points were left out.
|
|
110
|
+
*
|
|
111
|
+
* Each recorded point covers its own instant, or a history bucket's whole
|
|
112
|
+
* span, so a coarse provider read still counts as covering the time between
|
|
113
|
+
* its buckets. A run never crosses a recorded stretch or a segment boundary.
|
|
114
|
+
*/
|
|
115
|
+
export declare function fillGaps(segments: TimedPosition[][], recorded: readonly TimedPosition[], minGap: number): {
|
|
116
|
+
runs: TimedPosition[][];
|
|
117
|
+
overlapping: number;
|
|
118
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SqliteTrackStore } from './sqliteStore.js';
|
|
2
|
-
export type StoreMethod = 'newPosition' | 'recordName' | 'initialTrack' | 'prune' | 'get' | 'getTimed' | 'getAllTracks' | 'getFilteredTracks' | 'getFilteredTimedTracks' | 'storeImport' | 'getImport' | 'findImports' | 'binRecorded' | 'binImport' | 'restoreFromBin' | 'purgeFromBin' | 'binEntries' | 'deletedSpans' | 'close';
|
|
2
|
+
export type StoreMethod = 'newPosition' | 'recordName' | 'initialTrack' | 'prune' | 'get' | 'getTimed' | 'getAllTracks' | 'getFilteredTracks' | 'getFilteredTimedTracks' | 'storeImport' | 'getImport' | 'findImports' | 'binRecorded' | 'recordTrack' | 'binImport' | 'restoreFromBin' | 'purgeFromBin' | 'binEntries' | 'deletedSpans' | 'close';
|
|
3
3
|
export type Operation = {
|
|
4
4
|
[K in StoreMethod]: {
|
|
5
5
|
id: number;
|
package/dist/trackStoreWorker.js
CHANGED
|
@@ -11305,6 +11305,7 @@ var coveringFor = (bounds, maxCells) => {
|
|
|
11305
11305
|
});
|
|
11306
11306
|
});
|
|
11307
11307
|
};
|
|
11308
|
+
var SELECT_IMPORTS = "SELECT * FROM imported_tracks JOIN import_times USING (id)";
|
|
11308
11309
|
/** Longitude intervals as `[west, east]`, either of which may cross the antimeridian. */
|
|
11309
11310
|
var longitudesOverlap = (a, b) => {
|
|
11310
11311
|
const pieces = ([west, east]) => west <= east ? [[west, east]] : [[west, 180], [-180, east]];
|
|
@@ -11398,6 +11399,13 @@ var SqliteTrackStore = class {
|
|
|
11398
11399
|
north REAL NOT NULL
|
|
11399
11400
|
);
|
|
11400
11401
|
CREATE INDEX IF NOT EXISTS idx_imported_context ON imported_tracks(context);
|
|
11402
|
+
-- When each import was kept. Its own table, like names, so a database
|
|
11403
|
+
-- written before it existed opens unchanged; it stays while the import
|
|
11404
|
+
-- is in the recycle bin, for a restore, and goes when that is purged.
|
|
11405
|
+
CREATE TABLE IF NOT EXISTS import_times (
|
|
11406
|
+
id TEXT PRIMARY KEY,
|
|
11407
|
+
imported_at INTEGER NOT NULL
|
|
11408
|
+
);
|
|
11401
11409
|
CREATE TABLE IF NOT EXISTS imported_points (
|
|
11402
11410
|
track_id TEXT NOT NULL,
|
|
11403
11411
|
seq INTEGER NOT NULL,
|
|
@@ -11455,6 +11463,9 @@ var SqliteTrackStore = class {
|
|
|
11455
11463
|
);
|
|
11456
11464
|
CREATE INDEX IF NOT EXISTS idx_binned_imported_points ON binned_imported_points(bin_id);
|
|
11457
11465
|
`);
|
|
11466
|
+
const now = Date.now();
|
|
11467
|
+
this.db.prepare(`INSERT OR IGNORE INTO import_times (id, imported_at)
|
|
11468
|
+
SELECT id, ? FROM imported_tracks UNION SELECT id, ? FROM binned_imported_tracks`).run(now, now);
|
|
11458
11469
|
this.insert = this.db.prepare("INSERT INTO positions (context, timestamp, lat, lon, s2cell) VALUES (?, ?, ?, ?, ?)");
|
|
11459
11470
|
this.insertName = this.db.prepare(`INSERT INTO names (context, name, timestamp) VALUES (?, ?, ?)
|
|
11460
11471
|
ON CONFLICT(context) DO UPDATE SET name = excluded.name, timestamp = excluded.timestamp
|
|
@@ -11692,6 +11703,7 @@ var SqliteTrackStore = class {
|
|
|
11692
11703
|
storeImport(track) {
|
|
11693
11704
|
const points = track.segments.flat();
|
|
11694
11705
|
if (points.length === 0) throw new Error("An imported track needs at least one point");
|
|
11706
|
+
if (this.db.prepare("SELECT 1 AS found FROM import_times WHERE id = ?").get(track.id)) throw new Error(`An import with id ${track.id} already exists`);
|
|
11695
11707
|
let south = points[0].position[0];
|
|
11696
11708
|
let north = south;
|
|
11697
11709
|
let firstTime;
|
|
@@ -11707,11 +11719,32 @@ var SqliteTrackStore = class {
|
|
|
11707
11719
|
const [west, east] = longitudeSpan(points.map(({ position }) => position[1]));
|
|
11708
11720
|
this.db.prepare(`INSERT INTO imported_tracks (id, context, name, metadata, first_time, last_time, west, south, east, north)
|
|
11709
11721
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(track.id, track.context ?? null, track.name ?? null, JSON.stringify(track.metadata), firstTime ?? null, lastTime ?? null, west, south, east, north);
|
|
11722
|
+
this.db.prepare("INSERT INTO import_times (id, imported_at) VALUES (?, ?)").run(track.id, Date.now());
|
|
11710
11723
|
const insert = this.db.prepare("INSERT INTO imported_points (track_id, seq, segment, timestamp, lat, lon) VALUES (?, ?, ?, ?, ?, ?)");
|
|
11711
11724
|
let seq = 0;
|
|
11712
11725
|
for (const [segment, segmentPoints] of track.segments.entries()) for (const { position, timestamp } of segmentPoints) insert.run(track.id, seq++, segment, timestamp ?? null, position[0], position[1]);
|
|
11713
11726
|
}
|
|
11714
11727
|
/**
|
|
11728
|
+
* Write a track imported for a vessel into its recording, in place of what
|
|
11729
|
+
* was recorded over the same time. The positions within each segment's
|
|
11730
|
+
* span go to the recycle bin first, one entry per segment, so a restore
|
|
11731
|
+
* brings them back beside the import. Every span is binned before any point
|
|
11732
|
+
* is written, so segments that overlap in time cannot bin each other.
|
|
11733
|
+
* Resolves to the bin entries made.
|
|
11734
|
+
*/
|
|
11735
|
+
recordTrack(context, segments, deletedAt, keepEmpty) {
|
|
11736
|
+
const binned = [];
|
|
11737
|
+
for (const points of segments) {
|
|
11738
|
+
const first = points[0];
|
|
11739
|
+
const last = points.at(-1);
|
|
11740
|
+
if (first === void 0 || last === void 0) continue;
|
|
11741
|
+
const { id } = this.binRecorded(context, first.timestamp, last.timestamp, deletedAt, keepEmpty);
|
|
11742
|
+
if (id !== void 0) binned.push(id);
|
|
11743
|
+
}
|
|
11744
|
+
for (const points of segments) for (const { position, timestamp } of points) this.store(context, position, timestamp);
|
|
11745
|
+
return binned;
|
|
11746
|
+
}
|
|
11747
|
+
/**
|
|
11715
11748
|
* Move a vessel's positions recorded within `[from, to]` into the recycle
|
|
11716
11749
|
* bin, resolving to the entry's id and whether the store held the vessel at
|
|
11717
11750
|
* all.
|
|
@@ -11861,9 +11894,10 @@ var SqliteTrackStore = class {
|
|
|
11861
11894
|
if (entry.import_id === null) this.db.prepare("UPDATE recycle_bin SET purged = 1 WHERE id = ?").run(id);
|
|
11862
11895
|
else this.db.prepare("DELETE FROM recycle_bin WHERE id = ?").run(id);
|
|
11863
11896
|
}
|
|
11897
|
+
if (entry.whole === 1 && entry.import_id !== null) this.db.prepare("DELETE FROM import_times WHERE id = ?").run(entry.import_id);
|
|
11864
11898
|
}
|
|
11865
11899
|
getImport(id) {
|
|
11866
|
-
const row = this.db.prepare(
|
|
11900
|
+
const row = this.db.prepare(`${SELECT_IMPORTS} WHERE id = ?`).get(id);
|
|
11867
11901
|
return row ? this.importFrom(row) : void 0;
|
|
11868
11902
|
}
|
|
11869
11903
|
/** Imported tracks the filter lets through, in the order they were stored. */
|
|
@@ -11884,7 +11918,7 @@ var SqliteTrackStore = class {
|
|
|
11884
11918
|
params.push(filter.bbox.ne[0], filter.bbox.sw[0]);
|
|
11885
11919
|
}
|
|
11886
11920
|
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
11887
|
-
let rows = this.db.prepare(
|
|
11921
|
+
let rows = this.db.prepare(`${SELECT_IMPORTS} ${where} ORDER BY imported_tracks.rowid`).all(...params);
|
|
11888
11922
|
const box = filter.bbox;
|
|
11889
11923
|
if (box) rows = rows.filter((row) => longitudesOverlap([row.west, row.east], [box.sw[1], box.ne[1]]));
|
|
11890
11924
|
return rows.map((row) => this.importFrom(row));
|
|
@@ -11910,7 +11944,8 @@ var SqliteTrackStore = class {
|
|
|
11910
11944
|
...row.context === null ? {} : { context: row.context },
|
|
11911
11945
|
...row.name === null ? {} : { name: row.name },
|
|
11912
11946
|
metadata: JSON.parse(row.metadata),
|
|
11913
|
-
segments
|
|
11947
|
+
segments,
|
|
11948
|
+
importedAt: row.imported_at
|
|
11914
11949
|
};
|
|
11915
11950
|
}
|
|
11916
11951
|
/** Worker startup snapshot; queries and name lookups must not open a second database. */
|
|
@@ -11999,6 +12034,7 @@ function mutate(operation, target) {
|
|
|
11999
12034
|
var ACKNOWLEDGED = /* @__PURE__ */ new Set([
|
|
12000
12035
|
"storeImport",
|
|
12001
12036
|
"binRecorded",
|
|
12037
|
+
"recordTrack",
|
|
12002
12038
|
"binImport",
|
|
12003
12039
|
"restoreFromBin",
|
|
12004
12040
|
"purgeFromBin"
|
|
@@ -12007,6 +12043,7 @@ function acknowledge(operation, target) {
|
|
|
12007
12043
|
switch (operation.method) {
|
|
12008
12044
|
case "storeImport": return target.storeImport(...operation.args);
|
|
12009
12045
|
case "binRecorded": return target.binRecorded(...operation.args);
|
|
12046
|
+
case "recordTrack": return target.recordTrack(...operation.args);
|
|
12010
12047
|
case "binImport": return target.binImport(...operation.args);
|
|
12011
12048
|
case "restoreFromBin": return target.restoreFromBin(...operation.args);
|
|
12012
12049
|
case "purgeFromBin": return target.purgeFromBin(...operation.args);
|