@signalk/tracks-plugin 3.3.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 +24 -11
- package/dist/asyncTrackStore.d.ts +2 -2
- package/dist/import.js +27 -6
- package/dist/index.html +4 -0
- package/dist/index.js +218 -61
- package/dist/index.js.map +1 -1
- package/dist/sqliteStore.d.ts +3 -3
- package/dist/store.d.ts +3 -3
- package/dist/trackApi.d.ts +2 -2
- package/dist/trackProvider.d.ts +49 -6
- package/dist/trackStoreWorker.js +18 -3
- package/dist/trackStoreWorker.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/docs/history-and-storage.md +5 -3
- 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 {
|
|
@@ -185,9 +185,9 @@ export declare class SqliteTrackStore implements TrackStore {
|
|
|
185
185
|
* earlier with it, since they have no track left to be restored into.
|
|
186
186
|
*/
|
|
187
187
|
private purge;
|
|
188
|
-
getImport(id: string):
|
|
188
|
+
getImport(id: string): StoredImport | undefined;
|
|
189
189
|
/** Imported tracks the filter lets through, in the order they were stored. */
|
|
190
|
-
findImports(filter?: ImportFilter):
|
|
190
|
+
findImports(filter?: ImportFilter): StoredImport[];
|
|
191
191
|
private importFrom;
|
|
192
192
|
/** Worker startup snapshot; queries and name lookups must not open a second database. */
|
|
193
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,
|
package/dist/trackApi.d.ts
CHANGED
|
@@ -59,8 +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
|
-
/**
|
|
63
|
-
imported?:
|
|
62
|
+
/** When a client stored the track, as an ISO 8601 time; absent on a recorded one. */
|
|
63
|
+
imported?: string;
|
|
64
64
|
/** Absent, with `to`, for a track whose points carry no times. */
|
|
65
65
|
from?: string;
|
|
66
66
|
to?: string;
|
package/dist/trackProvider.d.ts
CHANGED
|
@@ -54,22 +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[]>;
|
|
57
63
|
}
|
|
58
|
-
|
|
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;
|
|
59
82
|
export interface KeepTrackDeps {
|
|
60
83
|
store: Required<Pick<TrackStore, 'storeImport' | 'recordTrack'>>;
|
|
61
84
|
selfContext: string;
|
|
62
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[]>;
|
|
63
88
|
deleted: () => void;
|
|
64
89
|
}
|
|
90
|
+
export interface KeptTrack {
|
|
91
|
+
id: string;
|
|
92
|
+
/** Points left out because the vessel already had positions there. */
|
|
93
|
+
overlapping: number;
|
|
94
|
+
}
|
|
65
95
|
/**
|
|
66
96
|
* Keep a client's track, resolving to the id that addresses it.
|
|
67
97
|
*
|
|
68
98
|
* A timed track of the own vessel is the vessel's own passage, from before
|
|
69
99
|
* the plugin recorded it or from another plotter, so it is written into the
|
|
70
|
-
* recording
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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.
|
|
74
114
|
*/
|
|
75
|
-
export declare function
|
|
115
|
+
export declare function fillGaps(segments: TimedPosition[][], recorded: readonly TimedPosition[], minGap: number): {
|
|
116
|
+
runs: TimedPosition[][];
|
|
117
|
+
overlapping: number;
|
|
118
|
+
};
|
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,6 +11719,7 @@ 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]);
|
|
@@ -11881,9 +11894,10 @@ var SqliteTrackStore = class {
|
|
|
11881
11894
|
if (entry.import_id === null) this.db.prepare("UPDATE recycle_bin SET purged = 1 WHERE id = ?").run(id);
|
|
11882
11895
|
else this.db.prepare("DELETE FROM recycle_bin WHERE id = ?").run(id);
|
|
11883
11896
|
}
|
|
11897
|
+
if (entry.whole === 1 && entry.import_id !== null) this.db.prepare("DELETE FROM import_times WHERE id = ?").run(entry.import_id);
|
|
11884
11898
|
}
|
|
11885
11899
|
getImport(id) {
|
|
11886
|
-
const row = this.db.prepare(
|
|
11900
|
+
const row = this.db.prepare(`${SELECT_IMPORTS} WHERE id = ?`).get(id);
|
|
11887
11901
|
return row ? this.importFrom(row) : void 0;
|
|
11888
11902
|
}
|
|
11889
11903
|
/** Imported tracks the filter lets through, in the order they were stored. */
|
|
@@ -11904,7 +11918,7 @@ var SqliteTrackStore = class {
|
|
|
11904
11918
|
params.push(filter.bbox.ne[0], filter.bbox.sw[0]);
|
|
11905
11919
|
}
|
|
11906
11920
|
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
11907
|
-
let rows = this.db.prepare(
|
|
11921
|
+
let rows = this.db.prepare(`${SELECT_IMPORTS} ${where} ORDER BY imported_tracks.rowid`).all(...params);
|
|
11908
11922
|
const box = filter.bbox;
|
|
11909
11923
|
if (box) rows = rows.filter((row) => longitudesOverlap([row.west, row.east], [box.sw[1], box.ne[1]]));
|
|
11910
11924
|
return rows.map((row) => this.importFrom(row));
|
|
@@ -11930,7 +11944,8 @@ var SqliteTrackStore = class {
|
|
|
11930
11944
|
...row.context === null ? {} : { context: row.context },
|
|
11931
11945
|
...row.name === null ? {} : { name: row.name },
|
|
11932
11946
|
metadata: JSON.parse(row.metadata),
|
|
11933
|
-
segments
|
|
11947
|
+
segments,
|
|
11948
|
+
importedAt: row.imported_at
|
|
11934
11949
|
};
|
|
11935
11950
|
}
|
|
11936
11951
|
/** Worker startup snapshot; queries and name lookups must not open a second database. */
|