@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/public/tracks.js
CHANGED
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
const WINDOW_V2 = 'P30D'
|
|
26
26
|
const WINDOW_V1 = '30d'
|
|
27
27
|
|
|
28
|
+
/** This plugin's id, which the server stamps on its tracks as `providerId`. */
|
|
29
|
+
const PROVIDER_ID = 'tracks'
|
|
30
|
+
|
|
28
31
|
/**
|
|
29
32
|
* Ceiling on the list request.
|
|
30
33
|
*
|
|
@@ -36,6 +39,10 @@ const REQUEST_TIMEOUT_MS = 30_000
|
|
|
36
39
|
|
|
37
40
|
/** Relative, because the server mounts this under /@signalk/tracks-plugin. */
|
|
38
41
|
const TRACKS_URL = `../../signalk/v2/api/tracks?geometry=false&duration=${WINDOW_V2}`
|
|
42
|
+
/** One track by its id, on a server that serves them so. */
|
|
43
|
+
const TRACK_URL = '../../signalk/v2/api/tracks/'
|
|
44
|
+
/** Every import, in the shape of the list's rows; the window above misses old and untimed ones. */
|
|
45
|
+
const IMPORTS_URL = '../../plugins/tracks/imports'
|
|
39
46
|
|
|
40
47
|
const status = document.getElementById('status')
|
|
41
48
|
const table = document.getElementById('tracks')
|
|
@@ -56,8 +63,8 @@ function label({ context, contextName, isSelf }) {
|
|
|
56
63
|
if (contextName) {
|
|
57
64
|
return `AIS ${contextName}`
|
|
58
65
|
}
|
|
59
|
-
const mmsi = /urn:mrn:imo:mmsi:(\d+)$/.exec(context)
|
|
60
|
-
return mmsi ? `AIS ${mmsi[1]}` : context
|
|
66
|
+
const mmsi = /urn:mrn:imo:mmsi:(\d+)$/.exec(context ?? '')
|
|
67
|
+
return mmsi ? `AIS ${mmsi[1]}` : (context ?? 'No vessel')
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
/** ISO-8601 from the API, rendered in the viewer's own locale and zone. */
|
|
@@ -73,16 +80,48 @@ function row(properties) {
|
|
|
73
80
|
if (properties.isSelf) {
|
|
74
81
|
name.className = 'self'
|
|
75
82
|
}
|
|
83
|
+
// An imported track names the vessel it belongs to and has a name of its
|
|
84
|
+
// own; without the second it reads exactly like that vessel's recording.
|
|
85
|
+
if (typeof properties.name === 'string') {
|
|
86
|
+
const imported = document.createElement('span')
|
|
87
|
+
imported.className = 'imported'
|
|
88
|
+
imported.textContent = ` (imported: ${properties.name})`
|
|
89
|
+
name.append(imported)
|
|
90
|
+
}
|
|
76
91
|
tr.append(
|
|
77
92
|
name,
|
|
78
93
|
cell(when(properties.from)),
|
|
79
94
|
cell(when(properties.to)),
|
|
80
95
|
cell(properties.pointCount, 'num'),
|
|
81
96
|
exportCell(properties),
|
|
97
|
+
deleteCell(properties),
|
|
82
98
|
)
|
|
83
99
|
return tr
|
|
84
100
|
}
|
|
85
101
|
|
|
102
|
+
/**
|
|
103
|
+
* A button asking `bin.js` to delete this track.
|
|
104
|
+
*
|
|
105
|
+
* Handed over as an event rather than handled here, so the list stays a page
|
|
106
|
+
* that only reads; a track that cannot be addressed gets none.
|
|
107
|
+
*/
|
|
108
|
+
function deleteCell(properties) {
|
|
109
|
+
const td = document.createElement('td')
|
|
110
|
+
// The server names a track `providerId:trackId`; one that hands out the
|
|
111
|
+
// provider's own ids has no route to delete them by.
|
|
112
|
+
const { id, providerId } = properties
|
|
113
|
+
if (typeof id === 'string' && typeof providerId === 'string' && id.startsWith(`${providerId}:`)) {
|
|
114
|
+
const button = document.createElement('button')
|
|
115
|
+
button.type = 'button'
|
|
116
|
+
button.textContent = 'Delete…'
|
|
117
|
+
button.addEventListener('click', () => {
|
|
118
|
+
document.dispatchEvent(new CustomEvent('tracks:delete', { detail: { ...properties, label: label(properties) } }))
|
|
119
|
+
})
|
|
120
|
+
td.append(button)
|
|
121
|
+
}
|
|
122
|
+
return td
|
|
123
|
+
}
|
|
124
|
+
|
|
86
125
|
/**
|
|
87
126
|
* A link that downloads this track as GPX.
|
|
88
127
|
*
|
|
@@ -90,13 +129,27 @@ function row(properties) {
|
|
|
90
129
|
* response with a Content-Disposition on it, and the server sets one. Doing it
|
|
91
130
|
* by hand would mean holding a whole track in memory to build a blob.
|
|
92
131
|
*/
|
|
93
|
-
function exportCell(
|
|
132
|
+
function exportCell(properties) {
|
|
133
|
+
const { providerId, context, isSelf } = properties
|
|
94
134
|
const td = document.createElement('td')
|
|
95
135
|
const link = document.createElement('a')
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
136
|
+
const trackId = localId(properties)
|
|
137
|
+
// An import is exported as it was imported, never as the recording of the
|
|
138
|
+
// vessel it names; v1 does not know imports at all. Only this plugin's own:
|
|
139
|
+
// another provider's ids mean nothing to its route.
|
|
140
|
+
if (typeof trackId === 'string' && trackId.startsWith('imported:')) {
|
|
141
|
+
if (providerId !== undefined && providerId !== PROVIDER_ID) {
|
|
142
|
+
return td
|
|
143
|
+
}
|
|
144
|
+
link.href = `../../plugins/tracks/imports/${encodeURIComponent(trackId)}/track.gpx`
|
|
145
|
+
} else if (typeof context === 'string') {
|
|
146
|
+
// `self` resolves server-side; using it keeps the URL short and avoids
|
|
147
|
+
// guessing how the own vessel's context should be spelled.
|
|
148
|
+
const vessel = isSelf ? 'self/track.gpx' : `vessels/${encodeURIComponent(context)}/track.gpx`
|
|
149
|
+
link.href = `../../signalk/v1/api/${vessel}?duration=${WINDOW_V1}`
|
|
150
|
+
} else {
|
|
151
|
+
return td
|
|
152
|
+
}
|
|
100
153
|
link.textContent = 'GPX'
|
|
101
154
|
link.rel = 'nofollow'
|
|
102
155
|
td.append(link)
|
|
@@ -123,14 +176,68 @@ function show(message, state) {
|
|
|
123
176
|
}
|
|
124
177
|
}
|
|
125
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Every imported track, or none when the server cannot list them: one that
|
|
181
|
+
* predates the route, or keeps it to administrators. The list then shows what
|
|
182
|
+
* the Track API returns, as it always has.
|
|
183
|
+
*/
|
|
184
|
+
async function allImports(signal) {
|
|
185
|
+
try {
|
|
186
|
+
const response = await fetch(IMPORTS_URL, { credentials: 'include', signal })
|
|
187
|
+
const body = response.ok ? await response.json() : undefined
|
|
188
|
+
return Array.isArray(body) ? body : []
|
|
189
|
+
} catch {
|
|
190
|
+
return []
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A track's id without the provider prefix, which a server older than the
|
|
196
|
+
* Track API's write routes leaves off; the import listing always carries it.
|
|
197
|
+
*/
|
|
198
|
+
function localId(properties) {
|
|
199
|
+
const { id, providerId } = properties ?? {}
|
|
200
|
+
return typeof id === 'string' && typeof providerId === 'string' && id.startsWith(`${providerId}:`)
|
|
201
|
+
? id.slice(providerId.length + 1)
|
|
202
|
+
: id
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Whether the server names tracks `providerId:trackId`. Its listing shows it;
|
|
207
|
+
* with nothing listed, asking it for one import by that name does.
|
|
208
|
+
*/
|
|
209
|
+
async function namesByProvider(listed, unlisted, signal) {
|
|
210
|
+
if (listed.length > 0) {
|
|
211
|
+
return listed.some(
|
|
212
|
+
(properties) =>
|
|
213
|
+
typeof properties?.providerId === 'string' &&
|
|
214
|
+
typeof properties.id === 'string' &&
|
|
215
|
+
properties.id.startsWith(`${properties.providerId}:`),
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
if (unlisted.length === 0) {
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const response = await fetch(`${TRACK_URL}${encodeURIComponent(unlisted[0].id)}`, {
|
|
223
|
+
credentials: 'include',
|
|
224
|
+
signal,
|
|
225
|
+
})
|
|
226
|
+
return response.ok
|
|
227
|
+
} catch {
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
126
232
|
async function load() {
|
|
127
|
-
let response
|
|
128
233
|
// Bounded, because an unsettled fetch is the one failure the page cannot
|
|
129
234
|
// recover from: every other path ends in an error message, while a stalled
|
|
130
235
|
// server or proxy leaves it on "Loading…" indefinitely, which reads as a
|
|
131
236
|
// hang rather than a failure. The signal stays armed through json() below,
|
|
132
237
|
// so a response that stops mid-body is caught too.
|
|
133
238
|
const deadline = AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
239
|
+
const imports = allImports(deadline)
|
|
240
|
+
let response
|
|
134
241
|
try {
|
|
135
242
|
response = await fetch(TRACKS_URL, { credentials: 'include', signal: deadline })
|
|
136
243
|
} catch {
|
|
@@ -171,13 +278,24 @@ async function load() {
|
|
|
171
278
|
}
|
|
172
279
|
// Individual entries are still skipped rather than thrown on: one malformed
|
|
173
280
|
// feature should not blank a list that is otherwise fine.
|
|
174
|
-
const
|
|
281
|
+
const listed = body.features.map((feature) => feature?.properties)
|
|
282
|
+
const ids = new Set(listed.map(localId))
|
|
283
|
+
const unlisted = (await imports).filter((properties) => !ids.has(localId(properties)))
|
|
284
|
+
// A server older than the Track API's write routes names tracks without
|
|
285
|
+
// their provider and cannot delete by id, so the imports added here follow
|
|
286
|
+
// suit there rather than offer a delete that cannot work.
|
|
287
|
+
const older = (await namesByProvider(listed, unlisted, deadline))
|
|
288
|
+
? unlisted
|
|
289
|
+
: unlisted.map((properties) => ({ ...properties, id: localId(properties), providerId: undefined }))
|
|
175
290
|
// Newest first: the track someone came to look at is almost always the one
|
|
176
291
|
// that just finished.
|
|
177
|
-
const sorted =
|
|
178
|
-
|
|
292
|
+
const sorted = [...listed, ...older]
|
|
293
|
+
// An import may name no vessel; it is still listed, so it can be deleted.
|
|
179
294
|
.filter(
|
|
180
|
-
(properties) =>
|
|
295
|
+
(properties) =>
|
|
296
|
+
properties &&
|
|
297
|
+
(typeof properties.context === 'string' || typeof properties.name === 'string') &&
|
|
298
|
+
Number.isFinite(properties.pointCount),
|
|
181
299
|
)
|
|
182
300
|
.sort((a, b) => String(b.to ?? '').localeCompare(String(a.to ?? '')))
|
|
183
301
|
|
|
@@ -190,7 +308,8 @@ async function load() {
|
|
|
190
308
|
|
|
191
309
|
tbody.replaceChildren(...sorted.map(row))
|
|
192
310
|
table.hidden = false
|
|
193
|
-
|
|
311
|
+
const tracks = `${sorted.length} track${sorted.length === 1 ? '' : 's'}`
|
|
312
|
+
show(older.length > 0 ? `${tracks}: the last 30 days, and every import.` : `${tracks} in the last 30 days.`)
|
|
194
313
|
}
|
|
195
314
|
|
|
196
315
|
void load()
|