@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.
@@ -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): ImportedTrack | undefined;
188
+ getImport(id: string): StoredImport | undefined;
189
189
  /** Imported tracks the filter lets through, in the order they were stored. */
190
- findImports(filter?: ImportFilter): ImportedTrack[];
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): ImportedTrack | undefined | Promise<ImportedTrack | undefined>;
100
- findImports?(filter?: ImportFilter): ImportedTrack[] | Promise<ImportedTrack[]>;
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,
@@ -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
- /** Set on a track a client stored, absent on a recorded one. */
63
- imported?: true;
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;
@@ -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
- export declare function createTrackProvider(deps: TrackProviderDeps): TrackApi;
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 in place of what was recorded over the same time and answers
71
- * as `recorded:<context>`. Everything else is kept as an import: another
72
- * vessel's track is not this boat's recording, and an untimed one cannot be
73
- * placed in it.
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 keepTrack(track: ImportedTrack, deps: KeepTrackDeps): Promise<string>;
115
+ export declare function fillGaps(segments: TimedPosition[][], recorded: readonly TimedPosition[], minGap: number): {
116
+ runs: TimedPosition[][];
117
+ overlapping: number;
118
+ };
@@ -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("SELECT * FROM imported_tracks WHERE id = ?").get(id);
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(`SELECT * FROM imported_tracks ${where} ORDER BY rowid`).all(...params);
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. */