@signalk/tracks-plugin 3.0.0-beta.5 → 3.0.0-beta.6
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/app-icon.svg +42 -0
- package/dist/index.js +64 -2
- package/dist/index.js.map +1 -1
- package/dist/sqliteStore.d.ts +4 -0
- package/dist/store.d.ts +15 -0
- package/dist/tracks.d.ts +10 -0
- package/dist/utils.d.ts +2 -0
- package/docs/history-and-storage.md +94 -0
- package/docs/screenshots/tracks_1.png +0 -0
- package/package.json +7 -2
package/app-icon.svg
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="Tracks">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="sea" x1="0" y1="0" x2="0" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#1b6ea8"/>
|
|
5
|
+
<stop offset="1" stop-color="#0b3a5e"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<clipPath id="card">
|
|
8
|
+
<rect x="0" y="0" width="128" height="128" rx="22"/>
|
|
9
|
+
</clipPath>
|
|
10
|
+
</defs>
|
|
11
|
+
|
|
12
|
+
<g clip-path="url(#card)">
|
|
13
|
+
<rect width="128" height="128" fill="url(#sea)"/>
|
|
14
|
+
|
|
15
|
+
<!-- Chart grid, as on the sibling plugins' icons. -->
|
|
16
|
+
<g stroke="#bee3f4" stroke-opacity="0.28" stroke-width="1" fill="none">
|
|
17
|
+
<path d="M0 32 H128 M0 64 H128 M0 96 H128"/>
|
|
18
|
+
<path d="M32 0 V128 M64 0 V128 M96 0 V128"/>
|
|
19
|
+
</g>
|
|
20
|
+
|
|
21
|
+
<!-- The track itself: the recorded path, drawn through its own points. -->
|
|
22
|
+
<path d="M24 102 C 40 94, 42 72, 58 64 S 84 58, 94 44"
|
|
23
|
+
fill="none" stroke="#0b3a5e" stroke-opacity="0.45"
|
|
24
|
+
stroke-width="9" stroke-linecap="round"/>
|
|
25
|
+
<path d="M24 102 C 40 94, 42 72, 58 64 S 84 58, 94 44"
|
|
26
|
+
fill="none" stroke="#ffd76a" stroke-width="5"
|
|
27
|
+
stroke-linecap="round" stroke-linejoin="round"/>
|
|
28
|
+
|
|
29
|
+
<!-- Position fixes. The track is a series of recorded points, not a
|
|
30
|
+
drawn line, and the icon should read that way at 32px too. -->
|
|
31
|
+
<g fill="#fffaf0" stroke="#0b3a5e" stroke-opacity="0.5" stroke-width="1.5">
|
|
32
|
+
<circle cx="24" cy="102" r="4.5"/>
|
|
33
|
+
<circle cx="49" cy="74" r="4"/>
|
|
34
|
+
<circle cx="72" cy="61" r="4"/>
|
|
35
|
+
</g>
|
|
36
|
+
|
|
37
|
+
<!-- The vessel, at the newest fix, heading along the track. -->
|
|
38
|
+
<path d="M94 34 L102 52 L94 47 L86 52 Z" fill="#fffaf0"
|
|
39
|
+
stroke="#0b3a5e" stroke-opacity="0.5" stroke-width="1.5"
|
|
40
|
+
stroke-linejoin="round"/>
|
|
41
|
+
</g>
|
|
42
|
+
</svg>
|
package/dist/index.js
CHANGED
|
@@ -12135,6 +12135,8 @@ var DEFAULT_SEGMENT_GAP = 3e5;
|
|
|
12135
12135
|
var SqliteTrackStore = class {
|
|
12136
12136
|
db;
|
|
12137
12137
|
insert;
|
|
12138
|
+
insertName;
|
|
12139
|
+
selectName;
|
|
12138
12140
|
maxCells;
|
|
12139
12141
|
segmentGap;
|
|
12140
12142
|
retention;
|
|
@@ -12160,8 +12162,20 @@ var SqliteTrackStore = class {
|
|
|
12160
12162
|
);
|
|
12161
12163
|
CREATE INDEX IF NOT EXISTS idx_s2cell ON positions(s2cell);
|
|
12162
12164
|
CREATE INDEX IF NOT EXISTS idx_context_timestamp ON positions(context, timestamp);
|
|
12165
|
+
-- A separate table rather than a column on positions: this store has no
|
|
12166
|
+
-- migration mechanism, so a new table appears harmlessly on a database
|
|
12167
|
+
-- written by an older version, where an ALTER TABLE would not.
|
|
12168
|
+
CREATE TABLE IF NOT EXISTS names (
|
|
12169
|
+
context TEXT PRIMARY KEY,
|
|
12170
|
+
name TEXT NOT NULL,
|
|
12171
|
+
timestamp INTEGER NOT NULL
|
|
12172
|
+
);
|
|
12163
12173
|
`);
|
|
12164
12174
|
this.insert = this.db.prepare("INSERT INTO positions (context, timestamp, lat, lon, s2cell) VALUES (?, ?, ?, ?, ?)");
|
|
12175
|
+
this.insertName = this.db.prepare(`INSERT INTO names (context, name, timestamp) VALUES (?, ?, ?)
|
|
12176
|
+
ON CONFLICT(context) DO UPDATE SET name = excluded.name, timestamp = excluded.timestamp
|
|
12177
|
+
WHERE excluded.timestamp >= names.timestamp`);
|
|
12178
|
+
this.selectName = this.db.prepare("SELECT name FROM names WHERE context = ?");
|
|
12165
12179
|
}
|
|
12166
12180
|
newPosition(context, position, timestamp = Date.now()) {
|
|
12167
12181
|
if (this.resolution > 0) {
|
|
@@ -12288,10 +12302,19 @@ var SqliteTrackStore = class {
|
|
|
12288
12302
|
if (current.length > 0) result.push(current);
|
|
12289
12303
|
return result;
|
|
12290
12304
|
}
|
|
12305
|
+
recordName(context, name, timestamp = Date.now()) {
|
|
12306
|
+
const trimmed = name.trim();
|
|
12307
|
+
if (trimmed === "") return;
|
|
12308
|
+
this.insertName.run(context, trimmed, timestamp);
|
|
12309
|
+
}
|
|
12310
|
+
nameFor(context) {
|
|
12311
|
+
return this.selectName.get(context)?.name;
|
|
12312
|
+
}
|
|
12291
12313
|
prune(maxAge, keep) {
|
|
12292
12314
|
const cutoff = Date.now() - maxAge;
|
|
12293
12315
|
const dropped = this.db.prepare("SELECT context FROM positions GROUP BY context HAVING MAX(timestamp) < ? AND context IS NOT ?").all(cutoff, keep ?? null);
|
|
12294
12316
|
this.db.prepare("DELETE FROM positions WHERE context IN (SELECT context FROM positions GROUP BY context HAVING MAX(timestamp) < ?) AND context IS NOT ?").run(cutoff, keep ?? null);
|
|
12317
|
+
for (const { context } of dropped) this.db.prepare("DELETE FROM names WHERE context = ?").run(context);
|
|
12295
12318
|
for (const { context } of dropped) this.lastStored.delete(context);
|
|
12296
12319
|
if (this.retention > 0) {
|
|
12297
12320
|
const oldest = Date.now() - this.retention;
|
|
@@ -13043,12 +13066,34 @@ var Tracks = class {
|
|
|
13043
13066
|
return acc;
|
|
13044
13067
|
}, {});
|
|
13045
13068
|
}
|
|
13069
|
+
/**
|
|
13070
|
+
* Remembered names, by context.
|
|
13071
|
+
*
|
|
13072
|
+
* In memory only: this store is the client-side accumulator, so there is no
|
|
13073
|
+
* restart for a stored name to survive. It implements the same contract so
|
|
13074
|
+
* the two stores stay interchangeable and the plugin needs no special case.
|
|
13075
|
+
*/
|
|
13076
|
+
names = /* @__PURE__ */ new Map();
|
|
13077
|
+
recordName(context, name, timestamp = Date.now()) {
|
|
13078
|
+
const trimmed = name.trim();
|
|
13079
|
+
if (trimmed === "") return;
|
|
13080
|
+
const seen = this.names.get(context);
|
|
13081
|
+
if (seen && seen.timestamp > timestamp) return;
|
|
13082
|
+
this.names.set(context, {
|
|
13083
|
+
name: trimmed,
|
|
13084
|
+
timestamp
|
|
13085
|
+
});
|
|
13086
|
+
}
|
|
13087
|
+
nameFor(context) {
|
|
13088
|
+
return this.names.get(context)?.name;
|
|
13089
|
+
}
|
|
13046
13090
|
prune(maxAge, keep) {
|
|
13047
13091
|
const cutoff = Date.now() - maxAge;
|
|
13048
13092
|
const deleted = [];
|
|
13049
13093
|
Object.entries(this.tracks).forEach(([key, value]) => {
|
|
13050
13094
|
if (key !== keep && value.latestLatLngTuple < cutoff) {
|
|
13051
13095
|
delete this.tracks[key];
|
|
13096
|
+
this.names.delete(key);
|
|
13052
13097
|
deleted.push(key);
|
|
13053
13098
|
}
|
|
13054
13099
|
});
|
|
@@ -13357,6 +13402,21 @@ var historyRowTimestamp = (row) => {
|
|
|
13357
13402
|
return Number.isNaN(parsed) ? 0 : parsed;
|
|
13358
13403
|
};
|
|
13359
13404
|
function ThePlugin(app) {
|
|
13405
|
+
/**
|
|
13406
|
+
* The server's own lookup, falling back to what the store remembers.
|
|
13407
|
+
*
|
|
13408
|
+
* Live wins whenever the data model has an answer, which keeps the rule the
|
|
13409
|
+
* resolvers were written for: an AIS static report arriving late must be
|
|
13410
|
+
* able to correct or replace what was known. The stored value only covers
|
|
13411
|
+
* the window where the model knows nothing -- after a restart, or for a
|
|
13412
|
+
* vessel that has since sailed out of range.
|
|
13413
|
+
*/
|
|
13414
|
+
const remembering = (store) => (path) => {
|
|
13415
|
+
const live = app.getPath?.(path);
|
|
13416
|
+
if (live !== void 0) return live;
|
|
13417
|
+
const match = /^(.*)\.name$/.exec(path);
|
|
13418
|
+
return match ? store()?.nameFor(match[1]) : void 0;
|
|
13419
|
+
};
|
|
13360
13420
|
let onStop = [];
|
|
13361
13421
|
let tracks = void 0;
|
|
13362
13422
|
let segmentGap = 0;
|
|
@@ -13420,6 +13480,8 @@ function ThePlugin(app) {
|
|
|
13420
13480
|
onStop.push(app.streambundle.getBus("navigation.position").onValue((update) => {
|
|
13421
13481
|
if (!update.value || update.value.latitude == null || update.value.longitude == null) return;
|
|
13422
13482
|
sourceWatch.add(update.context, update.$source);
|
|
13483
|
+
const live = unwrapString(app.getPath?.(`${update.context}.name`));
|
|
13484
|
+
if (live !== void 0) tracks?.recordName(update.context, live);
|
|
13423
13485
|
const recorded = update.timestamp === void 0 ? void 0 : Date.parse(update.timestamp);
|
|
13424
13486
|
const timestamp = recorded !== void 0 && !Number.isNaN(recorded) ? recorded : void 0;
|
|
13425
13487
|
const position = [update.value.latitude, update.value.longitude];
|
|
@@ -13449,7 +13511,7 @@ function ThePlugin(app) {
|
|
|
13449
13511
|
store: () => tracks,
|
|
13450
13512
|
selfContext: () => app.selfContext,
|
|
13451
13513
|
segmentGap: () => segmentGap,
|
|
13452
|
-
contextName: (context) => contextName(context, (
|
|
13514
|
+
contextName: (context) => contextName(context, remembering(() => tracks)),
|
|
13453
13515
|
reconcileWithHistory: async (context, stored, window, resolutionMs) => {
|
|
13454
13516
|
const effective = resolutionMs ?? storeResolution;
|
|
13455
13517
|
const history = await historyPositions(app, context, window ?? windowSpanning(stored, WINDOWLESS_HISTORY_SPAN_MS), effective, app.debug);
|
|
@@ -13479,7 +13541,7 @@ function ThePlugin(app) {
|
|
|
13479
13541
|
}
|
|
13480
13542
|
},
|
|
13481
13543
|
signalKApiRoutes: function(router) {
|
|
13482
|
-
const nameOf = (context) => trackLabel(context, app.selfContext, (
|
|
13544
|
+
const nameOf = (context) => trackLabel(context, app.selfContext, remembering(() => tracks));
|
|
13483
13545
|
/**
|
|
13484
13546
|
* The points of one context, reconciled with history and segmented.
|
|
13485
13547
|
*
|