@signalk/tracks-plugin 3.1.1 → 3.2.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 +63 -0
- package/dist/asyncTrackStore.d.ts +24 -1
- package/dist/bin.js +285 -0
- package/dist/e2e.test-utils.d.ts +16 -2
- package/dist/gpx.d.ts +11 -2
- package/dist/gpxExport.test.d.ts +1 -0
- package/dist/gpxImport.test.d.ts +1 -0
- package/dist/import.js +98 -0
- package/dist/importedTracks.d.ts +54 -0
- package/dist/importedTracks.test.d.ts +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.html +52 -0
- package/dist/index.js +719 -71
- package/dist/index.js.map +1 -1
- package/dist/recycleBin.d.ts +20 -0
- package/dist/recycleBin.test.d.ts +1 -0
- package/dist/recycleBinApi.test.d.ts +1 -0
- package/dist/sqliteStore.d.ts +58 -1
- package/dist/store.d.ts +38 -1
- package/dist/style.css +78 -0
- package/dist/trackApi.d.ts +61 -5
- package/dist/trackProvider.d.ts +4 -0
- package/dist/trackStoreProtocol.d.ts +1 -1
- package/dist/trackStoreWorker.js +371 -1
- package/dist/trackStoreWorker.js.map +1 -1
- package/dist/tracks.js +132 -13
- package/dist/types.d.ts +74 -0
- package/dist/upload.d.ts +19 -0
- package/dist/{utils-CGIGp4Xf.js → utils-C0VkiidM.js} +32 -2
- package/dist/{utils-CGIGp4Xf.js.map → utils-C0VkiidM.js.map} +1 -1
- package/dist/utils.d.ts +14 -0
- package/docs/history-and-storage.md +16 -0
- package/package.json +1 -1
- package/public/bin.js +285 -0
- package/public/import.js +98 -0
- package/public/index.html +52 -0
- package/public/style.css +78 -0
- package/public/tracks.js +132 -13
package/dist/trackStoreWorker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as createMatcher,
|
|
1
|
+
import { S as thin, c as createMatcher, d as longitudeSpan, m as splitAtAntimeridian, n as SelfPositionUnavailableError, s as createInBounds } from "./utils-C0VkiidM.js";
|
|
2
2
|
import { parentPort, workerData } from "node:worker_threads";
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
4
|
//#region \0rolldown/runtime.js
|
|
@@ -11305,6 +11305,11 @@ var coveringFor = (bounds, maxCells) => {
|
|
|
11305
11305
|
});
|
|
11306
11306
|
});
|
|
11307
11307
|
};
|
|
11308
|
+
/** Longitude intervals as `[west, east]`, either of which may cross the antimeridian. */
|
|
11309
|
+
var longitudesOverlap = (a, b) => {
|
|
11310
|
+
const pieces = ([west, east]) => west <= east ? [[west, east]] : [[west, 180], [-180, east]];
|
|
11311
|
+
return pieces(a).some(([aWest, aEast]) => pieces(b).some(([bWest, bEast]) => aWest <= bEast && bWest <= aEast));
|
|
11312
|
+
};
|
|
11308
11313
|
var DEFAULT_MAX_CELLS = 8;
|
|
11309
11314
|
var DEFAULT_SEGMENT_GAP = 3e5;
|
|
11310
11315
|
/**
|
|
@@ -11330,9 +11335,11 @@ var SqliteTrackStore = class {
|
|
|
11330
11335
|
insert;
|
|
11331
11336
|
insertName;
|
|
11332
11337
|
selectName;
|
|
11338
|
+
selectImportedPoints;
|
|
11333
11339
|
maxCells;
|
|
11334
11340
|
segmentGap;
|
|
11335
11341
|
retention;
|
|
11342
|
+
binRetention;
|
|
11336
11343
|
resolution;
|
|
11337
11344
|
/** See CLIP_STRETCH_GAP; widened so a coarse write resolution does not split every fix. */
|
|
11338
11345
|
stretchGap;
|
|
@@ -11349,6 +11356,7 @@ var SqliteTrackStore = class {
|
|
|
11349
11356
|
this.maxCells = config.maxCells ?? DEFAULT_MAX_CELLS;
|
|
11350
11357
|
this.segmentGap = config.segmentGap ?? DEFAULT_SEGMENT_GAP;
|
|
11351
11358
|
this.retention = config.retention ?? 0;
|
|
11359
|
+
this.binRetention = config.binRetention ?? 0;
|
|
11352
11360
|
this.resolution = config.resolution ?? 0;
|
|
11353
11361
|
this.stretchGap = Math.max(CLIP_STRETCH_GAP, 2 * this.resolution);
|
|
11354
11362
|
this.debug = debug;
|
|
@@ -11374,12 +11382,85 @@ var SqliteTrackStore = class {
|
|
|
11374
11382
|
name TEXT NOT NULL,
|
|
11375
11383
|
timestamp INTEGER NOT NULL
|
|
11376
11384
|
);
|
|
11385
|
+
-- Imported tracks, apart from positions: they are never merged into a
|
|
11386
|
+
-- recording or pruned with one. The extent is kept per track so a query
|
|
11387
|
+
-- can skip an import without reading its points.
|
|
11388
|
+
CREATE TABLE IF NOT EXISTS imported_tracks (
|
|
11389
|
+
id TEXT PRIMARY KEY,
|
|
11390
|
+
context TEXT,
|
|
11391
|
+
name TEXT,
|
|
11392
|
+
metadata TEXT NOT NULL,
|
|
11393
|
+
first_time INTEGER,
|
|
11394
|
+
last_time INTEGER,
|
|
11395
|
+
west REAL NOT NULL,
|
|
11396
|
+
south REAL NOT NULL,
|
|
11397
|
+
east REAL NOT NULL,
|
|
11398
|
+
north REAL NOT NULL
|
|
11399
|
+
);
|
|
11400
|
+
CREATE INDEX IF NOT EXISTS idx_imported_context ON imported_tracks(context);
|
|
11401
|
+
CREATE TABLE IF NOT EXISTS imported_points (
|
|
11402
|
+
track_id TEXT NOT NULL,
|
|
11403
|
+
seq INTEGER NOT NULL,
|
|
11404
|
+
segment INTEGER NOT NULL,
|
|
11405
|
+
timestamp INTEGER,
|
|
11406
|
+
lat REAL NOT NULL,
|
|
11407
|
+
lon REAL NOT NULL,
|
|
11408
|
+
PRIMARY KEY (track_id, seq)
|
|
11409
|
+
) WITHOUT ROWID;
|
|
11410
|
+
-- The recycle bin. A delete moves rows here rather than removing them,
|
|
11411
|
+
-- so the tables every read uses stay as they are and a restore moves
|
|
11412
|
+
-- them back. A purged recorded span keeps its row: a history provider
|
|
11413
|
+
-- would otherwise fill the deleted window back in.
|
|
11414
|
+
CREATE TABLE IF NOT EXISTS recycle_bin (
|
|
11415
|
+
id INTEGER PRIMARY KEY,
|
|
11416
|
+
context TEXT,
|
|
11417
|
+
import_id TEXT,
|
|
11418
|
+
name TEXT,
|
|
11419
|
+
whole INTEGER NOT NULL,
|
|
11420
|
+
span_from INTEGER,
|
|
11421
|
+
span_to INTEGER,
|
|
11422
|
+
deleted_at INTEGER NOT NULL,
|
|
11423
|
+
purged INTEGER NOT NULL DEFAULT 0
|
|
11424
|
+
);
|
|
11425
|
+
CREATE TABLE IF NOT EXISTS binned_positions (
|
|
11426
|
+
bin_id INTEGER NOT NULL,
|
|
11427
|
+
context TEXT NOT NULL,
|
|
11428
|
+
timestamp INTEGER NOT NULL,
|
|
11429
|
+
lat REAL NOT NULL,
|
|
11430
|
+
lon REAL NOT NULL,
|
|
11431
|
+
s2cell INTEGER NOT NULL
|
|
11432
|
+
);
|
|
11433
|
+
CREATE INDEX IF NOT EXISTS idx_binned_positions ON binned_positions(bin_id);
|
|
11434
|
+
CREATE TABLE IF NOT EXISTS binned_imported_tracks (
|
|
11435
|
+
bin_id INTEGER PRIMARY KEY,
|
|
11436
|
+
id TEXT NOT NULL,
|
|
11437
|
+
context TEXT,
|
|
11438
|
+
name TEXT,
|
|
11439
|
+
metadata TEXT NOT NULL,
|
|
11440
|
+
first_time INTEGER,
|
|
11441
|
+
last_time INTEGER,
|
|
11442
|
+
west REAL NOT NULL,
|
|
11443
|
+
south REAL NOT NULL,
|
|
11444
|
+
east REAL NOT NULL,
|
|
11445
|
+
north REAL NOT NULL
|
|
11446
|
+
);
|
|
11447
|
+
CREATE TABLE IF NOT EXISTS binned_imported_points (
|
|
11448
|
+
bin_id INTEGER NOT NULL,
|
|
11449
|
+
track_id TEXT NOT NULL,
|
|
11450
|
+
seq INTEGER NOT NULL,
|
|
11451
|
+
segment INTEGER NOT NULL,
|
|
11452
|
+
timestamp INTEGER,
|
|
11453
|
+
lat REAL NOT NULL,
|
|
11454
|
+
lon REAL NOT NULL
|
|
11455
|
+
);
|
|
11456
|
+
CREATE INDEX IF NOT EXISTS idx_binned_imported_points ON binned_imported_points(bin_id);
|
|
11377
11457
|
`);
|
|
11378
11458
|
this.insert = this.db.prepare("INSERT INTO positions (context, timestamp, lat, lon, s2cell) VALUES (?, ?, ?, ?, ?)");
|
|
11379
11459
|
this.insertName = this.db.prepare(`INSERT INTO names (context, name, timestamp) VALUES (?, ?, ?)
|
|
11380
11460
|
ON CONFLICT(context) DO UPDATE SET name = excluded.name, timestamp = excluded.timestamp
|
|
11381
11461
|
WHERE excluded.timestamp >= names.timestamp`);
|
|
11382
11462
|
this.selectName = this.db.prepare("SELECT name FROM names WHERE context = ?");
|
|
11463
|
+
this.selectImportedPoints = this.db.prepare("SELECT segment, timestamp, lat, lon FROM imported_points WHERE track_id = ? ORDER BY seq");
|
|
11383
11464
|
}
|
|
11384
11465
|
newPosition(context, position, timestamp = Date.now()) {
|
|
11385
11466
|
if (this.resolution > 0) {
|
|
@@ -11594,12 +11675,244 @@ var SqliteTrackStore = class {
|
|
|
11594
11675
|
this.lastName.delete(context);
|
|
11595
11676
|
}
|
|
11596
11677
|
for (const { context } of dropped) this.lastStored.delete(context);
|
|
11678
|
+
if (this.binRetention > 0) {
|
|
11679
|
+
const expired = this.db.prepare("SELECT * FROM recycle_bin WHERE purged = 0 AND deleted_at < ? ORDER BY whole DESC").all(now - this.binRetention);
|
|
11680
|
+
for (const entry of expired) if (this.liveEntry(entry.id)) this.purge(entry);
|
|
11681
|
+
}
|
|
11597
11682
|
if (this.retention > 0) {
|
|
11598
11683
|
const oldest = now - this.retention;
|
|
11599
11684
|
if (keep === void 0) this.db.prepare("DELETE FROM positions WHERE timestamp < ?").run(oldest);
|
|
11600
11685
|
else this.db.prepare("DELETE FROM positions WHERE timestamp < ? AND context IS ?").run(oldest, keep);
|
|
11601
11686
|
}
|
|
11602
11687
|
}
|
|
11688
|
+
/**
|
|
11689
|
+
* Keep an imported track. The caller mints the id; a second track under the
|
|
11690
|
+
* same id fails rather than replacing the first.
|
|
11691
|
+
*/
|
|
11692
|
+
storeImport(track) {
|
|
11693
|
+
const points = track.segments.flat();
|
|
11694
|
+
if (points.length === 0) throw new Error("An imported track needs at least one point");
|
|
11695
|
+
let south = points[0].position[0];
|
|
11696
|
+
let north = south;
|
|
11697
|
+
let firstTime;
|
|
11698
|
+
let lastTime;
|
|
11699
|
+
for (const { position, timestamp } of points) {
|
|
11700
|
+
south = Math.min(south, position[0]);
|
|
11701
|
+
north = Math.max(north, position[0]);
|
|
11702
|
+
if (timestamp !== void 0) {
|
|
11703
|
+
firstTime = firstTime === void 0 ? timestamp : Math.min(firstTime, timestamp);
|
|
11704
|
+
lastTime = lastTime === void 0 ? timestamp : Math.max(lastTime, timestamp);
|
|
11705
|
+
}
|
|
11706
|
+
}
|
|
11707
|
+
const [west, east] = longitudeSpan(points.map(({ position }) => position[1]));
|
|
11708
|
+
this.db.prepare(`INSERT INTO imported_tracks (id, context, name, metadata, first_time, last_time, west, south, east, north)
|
|
11709
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(track.id, track.context ?? null, track.name ?? null, JSON.stringify(track.metadata), firstTime ?? null, lastTime ?? null, west, south, east, north);
|
|
11710
|
+
const insert = this.db.prepare("INSERT INTO imported_points (track_id, seq, segment, timestamp, lat, lon) VALUES (?, ?, ?, ?, ?, ?)");
|
|
11711
|
+
let seq = 0;
|
|
11712
|
+
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
|
+
}
|
|
11714
|
+
/**
|
|
11715
|
+
* Move a vessel's positions recorded within `[from, to]` into the recycle
|
|
11716
|
+
* bin, resolving to the entry's id and whether the store held the vessel at
|
|
11717
|
+
* all.
|
|
11718
|
+
*
|
|
11719
|
+
* An entry is made even when the store holds nothing there if `keepEmpty`
|
|
11720
|
+
* says another source does: the span still has to hide what a history
|
|
11721
|
+
* provider holds. Without one, an empty delete leaves no entry behind.
|
|
11722
|
+
*/
|
|
11723
|
+
binRecorded(context, from, to, deletedAt, keepEmpty) {
|
|
11724
|
+
const known = this.knows(context);
|
|
11725
|
+
const lower = from ?? Number.MIN_SAFE_INTEGER;
|
|
11726
|
+
const id = this.newBinEntry({
|
|
11727
|
+
context,
|
|
11728
|
+
...from === void 0 ? {} : { from },
|
|
11729
|
+
to,
|
|
11730
|
+
deletedAt
|
|
11731
|
+
});
|
|
11732
|
+
if (Number(this.db.prepare(`INSERT INTO binned_positions (bin_id, context, timestamp, lat, lon, s2cell)
|
|
11733
|
+
SELECT ?, context, timestamp, lat, lon, s2cell FROM positions
|
|
11734
|
+
WHERE context = ? AND timestamp >= ? AND timestamp <= ?`).run(id, context, lower, to).changes) === 0 && !keepEmpty) {
|
|
11735
|
+
this.db.prepare("DELETE FROM recycle_bin WHERE id = ?").run(id);
|
|
11736
|
+
return { known };
|
|
11737
|
+
}
|
|
11738
|
+
this.db.prepare("DELETE FROM positions WHERE context = ? AND timestamp >= ? AND timestamp <= ?").run(context, lower, to);
|
|
11739
|
+
return {
|
|
11740
|
+
id,
|
|
11741
|
+
known
|
|
11742
|
+
};
|
|
11743
|
+
}
|
|
11744
|
+
/**
|
|
11745
|
+
* Move an imported track into the recycle bin, or with a span only its
|
|
11746
|
+
* points within it, resolving to the entry's id; undefined when there is no
|
|
11747
|
+
* such import, or the span holds none of its points.
|
|
11748
|
+
*/
|
|
11749
|
+
binImport(importId, span, deletedAt) {
|
|
11750
|
+
const track = this.db.prepare("SELECT context, name FROM imported_tracks WHERE id = ?").get(importId);
|
|
11751
|
+
if (!track) return;
|
|
11752
|
+
const id = this.newBinEntry({
|
|
11753
|
+
...track.context === null ? {} : { context: track.context },
|
|
11754
|
+
importId,
|
|
11755
|
+
...track.name === null ? {} : { name: track.name },
|
|
11756
|
+
...span ?? {},
|
|
11757
|
+
deletedAt
|
|
11758
|
+
});
|
|
11759
|
+
const where = span ? "track_id = ? AND timestamp >= ? AND timestamp <= ?" : "track_id = ?";
|
|
11760
|
+
const params = span ? [
|
|
11761
|
+
importId,
|
|
11762
|
+
span.from ?? Number.MIN_SAFE_INTEGER,
|
|
11763
|
+
span.to ?? Number.MAX_SAFE_INTEGER
|
|
11764
|
+
] : [importId];
|
|
11765
|
+
const moved = Number(this.db.prepare(`INSERT INTO binned_imported_points (bin_id, track_id, seq, segment, timestamp, lat, lon)
|
|
11766
|
+
SELECT ?, track_id, seq, segment, timestamp, lat, lon FROM imported_points WHERE ${where}`).run(id, ...params).changes);
|
|
11767
|
+
if (span && moved === 0) {
|
|
11768
|
+
this.db.prepare("DELETE FROM recycle_bin WHERE id = ?").run(id);
|
|
11769
|
+
return;
|
|
11770
|
+
}
|
|
11771
|
+
this.db.prepare(`DELETE FROM imported_points WHERE ${where}`).run(...params);
|
|
11772
|
+
const emptied = span && !this.db.prepare("SELECT 1 AS found FROM imported_points WHERE track_id = ?").get(importId);
|
|
11773
|
+
if (emptied) this.db.prepare("UPDATE recycle_bin SET whole = 1, span_from = NULL, span_to = NULL WHERE id = ?").run(id);
|
|
11774
|
+
if (!span || emptied) {
|
|
11775
|
+
this.db.prepare(`INSERT INTO binned_imported_tracks
|
|
11776
|
+
SELECT ?, id, context, name, metadata, first_time, last_time, west, south, east, north
|
|
11777
|
+
FROM imported_tracks WHERE id = ?`).run(id, importId);
|
|
11778
|
+
this.db.prepare("DELETE FROM imported_tracks WHERE id = ?").run(importId);
|
|
11779
|
+
}
|
|
11780
|
+
return id;
|
|
11781
|
+
}
|
|
11782
|
+
newBinEntry(entry) {
|
|
11783
|
+
const whole = entry.importId !== void 0 && entry.from === void 0 && entry.to === void 0;
|
|
11784
|
+
return Number(this.db.prepare(`INSERT INTO recycle_bin (context, import_id, name, whole, span_from, span_to, deleted_at)
|
|
11785
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(entry.context ?? null, entry.importId ?? null, entry.name ?? null, whole ? 1 : 0, entry.from ?? null, entry.to ?? null, entry.deletedAt).lastInsertRowid);
|
|
11786
|
+
}
|
|
11787
|
+
/**
|
|
11788
|
+
* Put a recycle bin entry's rows back where they were deleted from.
|
|
11789
|
+
*
|
|
11790
|
+
* A span of an import whose whole track has since gone to the bin as well
|
|
11791
|
+
* is a conflict: its points have no track to return to until that is
|
|
11792
|
+
* restored first.
|
|
11793
|
+
*/
|
|
11794
|
+
restoreFromBin(id) {
|
|
11795
|
+
const entry = this.liveEntry(id);
|
|
11796
|
+
if (!entry) return "missing";
|
|
11797
|
+
if (entry.import_id === null) {
|
|
11798
|
+
this.db.prepare(`INSERT INTO positions (context, timestamp, lat, lon, s2cell)
|
|
11799
|
+
SELECT context, timestamp, lat, lon, s2cell FROM binned_positions WHERE bin_id = ?`).run(id);
|
|
11800
|
+
this.db.prepare("DELETE FROM binned_positions WHERE bin_id = ?").run(id);
|
|
11801
|
+
} else {
|
|
11802
|
+
if (entry.whole === 1) {
|
|
11803
|
+
this.db.prepare(`INSERT INTO imported_tracks (id, context, name, metadata, first_time, last_time, west, south, east, north)
|
|
11804
|
+
SELECT id, context, name, metadata, first_time, last_time, west, south, east, north
|
|
11805
|
+
FROM binned_imported_tracks WHERE bin_id = ?`).run(id);
|
|
11806
|
+
this.db.prepare("DELETE FROM binned_imported_tracks WHERE bin_id = ?").run(id);
|
|
11807
|
+
} else if (!this.db.prepare("SELECT 1 AS found FROM imported_tracks WHERE id = ?").get(entry.import_id)) return "conflict";
|
|
11808
|
+
this.db.prepare(`INSERT INTO imported_points (track_id, seq, segment, timestamp, lat, lon)
|
|
11809
|
+
SELECT track_id, seq, segment, timestamp, lat, lon FROM binned_imported_points WHERE bin_id = ?`).run(id);
|
|
11810
|
+
this.db.prepare("DELETE FROM binned_imported_points WHERE bin_id = ?").run(id);
|
|
11811
|
+
}
|
|
11812
|
+
this.db.prepare("DELETE FROM recycle_bin WHERE id = ?").run(id);
|
|
11813
|
+
return "restored";
|
|
11814
|
+
}
|
|
11815
|
+
/** Purge one recycle bin entry now, resolving whether it was in the bin. */
|
|
11816
|
+
purgeFromBin(id) {
|
|
11817
|
+
const entry = this.liveEntry(id);
|
|
11818
|
+
if (!entry) return false;
|
|
11819
|
+
this.purge(entry);
|
|
11820
|
+
return true;
|
|
11821
|
+
}
|
|
11822
|
+
/** Entries still in the recycle bin, newest delete first. */
|
|
11823
|
+
binEntries() {
|
|
11824
|
+
return this.db.prepare(`SELECT bin.*,
|
|
11825
|
+
(SELECT COUNT(*) FROM binned_positions WHERE bin_id = bin.id)
|
|
11826
|
+
+ (SELECT COUNT(*) FROM binned_imported_points WHERE bin_id = bin.id) AS point_count
|
|
11827
|
+
FROM recycle_bin AS bin WHERE purged = 0 ORDER BY deleted_at DESC, id DESC`).all().map((row) => ({
|
|
11828
|
+
id: row.id,
|
|
11829
|
+
...row.context === null ? {} : { context: row.context },
|
|
11830
|
+
...row.import_id === null ? {} : { importId: row.import_id },
|
|
11831
|
+
...row.name === null ? {} : { name: row.name },
|
|
11832
|
+
whole: row.whole === 1,
|
|
11833
|
+
...row.span_from === null ? {} : { from: row.span_from },
|
|
11834
|
+
...row.span_to === null ? {} : { to: row.span_to },
|
|
11835
|
+
deletedAt: row.deleted_at,
|
|
11836
|
+
pointCount: row.point_count
|
|
11837
|
+
}));
|
|
11838
|
+
}
|
|
11839
|
+
/** Every deleted span of a recorded track, purged or not: what history must not show. */
|
|
11840
|
+
deletedSpans() {
|
|
11841
|
+
return this.db.prepare("SELECT context, span_from, span_to FROM recycle_bin WHERE import_id IS NULL").all().map(({ context, span_from, span_to }) => ({
|
|
11842
|
+
context,
|
|
11843
|
+
...span_from === null ? {} : { from: span_from },
|
|
11844
|
+
to: span_to
|
|
11845
|
+
}));
|
|
11846
|
+
}
|
|
11847
|
+
liveEntry(id) {
|
|
11848
|
+
return this.db.prepare("SELECT * FROM recycle_bin WHERE id = ? AND purged = 0").get(id);
|
|
11849
|
+
}
|
|
11850
|
+
/**
|
|
11851
|
+
* Remove an entry's rows for good. A recorded span stays as a tombstone, so
|
|
11852
|
+
* history cannot refill it; a whole import takes the spans deleted from it
|
|
11853
|
+
* earlier with it, since they have no track left to be restored into.
|
|
11854
|
+
*/
|
|
11855
|
+
purge(entry) {
|
|
11856
|
+
const ids = entry.whole === 1 ? this.db.prepare("SELECT id FROM recycle_bin WHERE import_id = ? AND purged = 0").all(entry.import_id).map(({ id }) => id) : [entry.id];
|
|
11857
|
+
for (const id of ids) {
|
|
11858
|
+
this.db.prepare("DELETE FROM binned_positions WHERE bin_id = ?").run(id);
|
|
11859
|
+
this.db.prepare("DELETE FROM binned_imported_points WHERE bin_id = ?").run(id);
|
|
11860
|
+
this.db.prepare("DELETE FROM binned_imported_tracks WHERE bin_id = ?").run(id);
|
|
11861
|
+
if (entry.import_id === null) this.db.prepare("UPDATE recycle_bin SET purged = 1 WHERE id = ?").run(id);
|
|
11862
|
+
else this.db.prepare("DELETE FROM recycle_bin WHERE id = ?").run(id);
|
|
11863
|
+
}
|
|
11864
|
+
}
|
|
11865
|
+
getImport(id) {
|
|
11866
|
+
const row = this.db.prepare("SELECT * FROM imported_tracks WHERE id = ?").get(id);
|
|
11867
|
+
return row ? this.importFrom(row) : void 0;
|
|
11868
|
+
}
|
|
11869
|
+
/** Imported tracks the filter lets through, in the order they were stored. */
|
|
11870
|
+
findImports(filter = {}) {
|
|
11871
|
+
const clauses = [];
|
|
11872
|
+
const params = [];
|
|
11873
|
+
if (filter.contexts) {
|
|
11874
|
+
if (filter.contexts.length === 0) return [];
|
|
11875
|
+
clauses.push(`context IN (${filter.contexts.map(() => "?").join(", ")})`);
|
|
11876
|
+
params.push(...filter.contexts);
|
|
11877
|
+
}
|
|
11878
|
+
if (filter.window) {
|
|
11879
|
+
clauses.push("first_time IS NOT NULL", filter.window.inclusiveEnd ? "first_time <= ?" : "first_time < ?", "last_time >= ?");
|
|
11880
|
+
params.push(filter.window.to, filter.window.from);
|
|
11881
|
+
}
|
|
11882
|
+
if (filter.bbox) {
|
|
11883
|
+
clauses.push("south <= ?", "north >= ?");
|
|
11884
|
+
params.push(filter.bbox.ne[0], filter.bbox.sw[0]);
|
|
11885
|
+
}
|
|
11886
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
11887
|
+
let rows = this.db.prepare(`SELECT * FROM imported_tracks ${where} ORDER BY rowid`).all(...params);
|
|
11888
|
+
const box = filter.bbox;
|
|
11889
|
+
if (box) rows = rows.filter((row) => longitudesOverlap([row.west, row.east], [box.sw[1], box.ne[1]]));
|
|
11890
|
+
return rows.map((row) => this.importFrom(row));
|
|
11891
|
+
}
|
|
11892
|
+
importFrom(row) {
|
|
11893
|
+
const points = this.selectImportedPoints.all(row.id);
|
|
11894
|
+
const segments = [];
|
|
11895
|
+
let current;
|
|
11896
|
+
let currentIndex;
|
|
11897
|
+
for (const { segment, timestamp, lat, lon } of points) {
|
|
11898
|
+
if (segment !== currentIndex) {
|
|
11899
|
+
current = [];
|
|
11900
|
+
segments.push(current);
|
|
11901
|
+
currentIndex = segment;
|
|
11902
|
+
}
|
|
11903
|
+
current.push(timestamp === null ? { position: [lat, lon] } : {
|
|
11904
|
+
position: [lat, lon],
|
|
11905
|
+
timestamp
|
|
11906
|
+
});
|
|
11907
|
+
}
|
|
11908
|
+
return {
|
|
11909
|
+
id: row.id,
|
|
11910
|
+
...row.context === null ? {} : { context: row.context },
|
|
11911
|
+
...row.name === null ? {} : { name: row.name },
|
|
11912
|
+
metadata: JSON.parse(row.metadata),
|
|
11913
|
+
segments
|
|
11914
|
+
};
|
|
11915
|
+
}
|
|
11603
11916
|
/** Worker startup snapshot; queries and name lookups must not open a second database. */
|
|
11604
11917
|
storedNames() {
|
|
11605
11918
|
return this.db.prepare("SELECT context, name, timestamp FROM names").all();
|
|
@@ -11682,6 +11995,24 @@ function mutate(operation, target) {
|
|
|
11682
11995
|
default: return false;
|
|
11683
11996
|
}
|
|
11684
11997
|
}
|
|
11998
|
+
/** Writes whose caller waits for their own commit: imports and the recycle bin. */
|
|
11999
|
+
var ACKNOWLEDGED = /* @__PURE__ */ new Set([
|
|
12000
|
+
"storeImport",
|
|
12001
|
+
"binRecorded",
|
|
12002
|
+
"binImport",
|
|
12003
|
+
"restoreFromBin",
|
|
12004
|
+
"purgeFromBin"
|
|
12005
|
+
]);
|
|
12006
|
+
function acknowledge(operation, target) {
|
|
12007
|
+
switch (operation.method) {
|
|
12008
|
+
case "storeImport": return target.storeImport(...operation.args);
|
|
12009
|
+
case "binRecorded": return target.binRecorded(...operation.args);
|
|
12010
|
+
case "binImport": return target.binImport(...operation.args);
|
|
12011
|
+
case "restoreFromBin": return target.restoreFromBin(...operation.args);
|
|
12012
|
+
case "purgeFromBin": return target.purgeFromBin(...operation.args);
|
|
12013
|
+
default: throw new Error("Unknown track operation");
|
|
12014
|
+
}
|
|
12015
|
+
}
|
|
11685
12016
|
var isWrite = (operation) => [
|
|
11686
12017
|
"newPosition",
|
|
11687
12018
|
"recordName",
|
|
@@ -11709,6 +12040,33 @@ if (store) {
|
|
|
11709
12040
|
try {
|
|
11710
12041
|
for (let i = 0; i < operations.length;) {
|
|
11711
12042
|
const operation = operations[i];
|
|
12043
|
+
if (ACKNOWLEDGED.has(operation.method)) {
|
|
12044
|
+
i++;
|
|
12045
|
+
if (writeFailure) {
|
|
12046
|
+
results.push({
|
|
12047
|
+
id: operation.id,
|
|
12048
|
+
error: writeFailure
|
|
12049
|
+
});
|
|
12050
|
+
continue;
|
|
12051
|
+
}
|
|
12052
|
+
try {
|
|
12053
|
+
let value;
|
|
12054
|
+
target.transaction(() => {
|
|
12055
|
+
value = acknowledge(operation, target);
|
|
12056
|
+
});
|
|
12057
|
+
results.push({
|
|
12058
|
+
id: operation.id,
|
|
12059
|
+
value
|
|
12060
|
+
});
|
|
12061
|
+
} catch (error) {
|
|
12062
|
+
if (error instanceof TransactionStateError) throw error;
|
|
12063
|
+
results.push({
|
|
12064
|
+
id: operation.id,
|
|
12065
|
+
error: errorText(error)
|
|
12066
|
+
});
|
|
12067
|
+
}
|
|
12068
|
+
continue;
|
|
12069
|
+
}
|
|
11712
12070
|
if (isWrite(operation)) {
|
|
11713
12071
|
const first = i;
|
|
11714
12072
|
while (i < operations.length && isWrite(operations[i])) i++;
|
|
@@ -11769,6 +12127,18 @@ if (store) {
|
|
|
11769
12127
|
case "getFilteredTimedTracks":
|
|
11770
12128
|
value = await target.getFilteredTimedTracks(operation.args[0], operation.args[1], debug, operation.args[3]);
|
|
11771
12129
|
break;
|
|
12130
|
+
case "getImport":
|
|
12131
|
+
value = target.getImport(...operation.args);
|
|
12132
|
+
break;
|
|
12133
|
+
case "findImports":
|
|
12134
|
+
value = target.findImports(...operation.args);
|
|
12135
|
+
break;
|
|
12136
|
+
case "binEntries":
|
|
12137
|
+
value = target.binEntries();
|
|
12138
|
+
break;
|
|
12139
|
+
case "deletedSpans":
|
|
12140
|
+
value = target.deletedSpans();
|
|
12141
|
+
break;
|
|
11772
12142
|
default: throw new Error("Unknown track operation");
|
|
11773
12143
|
}
|
|
11774
12144
|
results.push({
|