@signalk/tracks-plugin 3.2.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 CHANGED
@@ -49,29 +49,51 @@ other track, filtered by vessel, time window and area, but as a feature of its o
49
49
  it names a vessel the plugin also records. It is never merged with a history provider's data,
50
50
  never removed by the retention settings, and never returned by the v1 routes, which answer
51
51
  where vessels are now. Its own name and any other properties it was posted with come back as
52
- posted.
53
-
54
- A track needs times (`coordTimes`) or a vessel (`context`). One with neither is refused:
55
- it would match no time window, and a query across every vessel needs one, so it could
56
- only be found again by its id.
52
+ posted. It carries `imported`, the time it was stored, which a recorded track does not, so a
53
+ client drawing a vessel's recording can leave the import out of it. Imports stored by a version
54
+ before that time was kept are dated to the first start of a version that keeps it.
55
+
56
+ A timed track of the own vessel is the exception: it is the boat's own passage, logged before
57
+ the plugin was installed or by another plotter, so it is written into the own vessel's
58
+ recording instead of being kept as an import, and the answer is the recording's id,
59
+ `tracks:recorded:<context>`. By default it only fills what the recording is missing: its
60
+ points are used where the vessel has no position, from this plugin or a history provider, for
61
+ at least five minutes, and left out elsewhere, so nothing recorded is overwritten and two
62
+ sources are never interleaved minute by minute; while a history provider cannot be read, such an
63
+ import is refused with a 503 rather than drawn over what it holds. Asked to replace instead, it takes the place of
64
+ what was recorded over the same time: the positions this plugin stored there go to the recycle
65
+ bin first, one entry per segment, and a history provider's data over that time is hidden while
66
+ the entry is there. Restoring the entry brings both back. An untimed track of the own vessel
67
+ stays an import, since it cannot be placed in time.
68
+
69
+ A track with neither times (`coordTimes`) nor a vessel (`context`) is kept too. No time
70
+ window or vessel query matches it, but `GET /plugins/tracks/imports` lists it with every other
71
+ import, and `GET /signalk/v2/api/tracks/<id>` returns it.
57
72
 
58
73
  ### Importing a GPX file
59
74
 
60
75
  The Tracks webapp imports a GPX file from the **Import** form, and a client can do the same
61
76
  with `POST /plugins/tracks/imports`, the file as the request body with `Content-Type:
62
- application/gpx+xml`. Each track in the file
63
- becomes an imported track as above. Ticking **Tracks of my own vessel** (`?self=true`) files
64
- them under the own vessel; otherwise a track names a vessel only if the file says which, as
65
- the plugin's own GPX export does.
66
-
67
- A file with a track that cannot be kept is refused whole: if any track in it has neither times
68
- nor a vessel, nothing is stored and the reason is shown. In a timed track, points without a time are
69
- skipped, and the answer says how many. The route needs write access on a Signal K server that
70
- lets a plugin say so, and administrator rights on an older one.
77
+ application/gpx+xml`. Each track in the file becomes an imported track as above. Ticking
78
+ **Tracks of my own vessel** (`?self=true`) files them under the own vessel, which writes a
79
+ timed one into the recording; otherwise a track names a vessel only if the file says which, as
80
+ the plugin's own GPX export does. Ticking **Replace what my vessel recorded over the same time**
81
+ as well (`onOverlap=replace`; the default is `keep`) replaces the recording rather than filling
82
+ it.
83
+
84
+ In a timed track, points without a time are skipped, and the answer says how many
85
+ (`skippedPoints`), as it does for the points left out where the recording already has positions
86
+ (`overlappingPoints`). A track posted through the Track API's `POST /signalk/v2/api/tracks` is
87
+ always kept beside the recording this way, since that API has no such choice yet. The route
88
+ needs write access on a Signal K server that lets a plugin say so, and administrator rights on
89
+ an older one.
71
90
 
72
91
  The webapp lists every imported track, however old and whether or not it has times, next to
73
92
  the last 30 days of recorded ones, so a passage imported from years ago can be found there. It
74
- can be deleted there on a Signal K server that deletes tracks by id. `GET /plugins/tracks/imports` returns every imported track, and no recorded ones.
93
+ can be deleted there on a Signal K server that deletes tracks by id.
94
+
95
+ `GET /plugins/tracks/imports` returns every imported track, and no recorded ones, each with
96
+ `imported`, the time it was stored.
75
97
 
76
98
  The Tracks webapp exports an imported track as GPX from its row, as it was imported. A client
77
99
  can download the same file from `GET /plugins/tracks/imports/<id>/track.gpx`, with the id less
@@ -1,6 +1,6 @@
1
1
  import { TrackStore } from './store.js';
2
2
  import { SqliteStoreConfig } from './sqliteStore.js';
3
- import { Debug, Context, ImportedTrack, ImportFilter, LatLngTuple, TimeWindow, TrackParams } from './types.js';
3
+ import { Debug, Context, ImportedTrack, ImportFilter, LatLngTuple, TimedPosition, TimeWindow, TrackParams } from './types.js';
4
4
  import { TrackQuery } from './timeWindow.js';
5
5
  interface Limits {
6
6
  maxItems?: number;
@@ -58,7 +58,7 @@ export declare class AsyncTrackStore implements TrackStore {
58
58
  initialTrack(context: Context, track: LatLngTuple[], timestamps?: number[]): void;
59
59
  prune(maxAge: number, keep?: Context): void;
60
60
  get(context: Context, window?: TimeWindow): Promise<LatLngTuple[]>;
61
- getTimed(context: Context, window?: TimeWindow): Promise<import('./types.js').TimedPosition[]>;
61
+ getTimed(context: Context, window?: TimeWindow): Promise<TimedPosition[]>;
62
62
  getAllTracks(query?: TrackQuery): Promise<{
63
63
  context: string;
64
64
  track: LatLngTuple[];
@@ -70,6 +70,7 @@ export declare class AsyncTrackStore implements TrackStore {
70
70
  id?: number;
71
71
  known: boolean;
72
72
  }>;
73
+ recordTrack(context: Context, segments: TimedPosition[][], deletedAt: number, keepEmpty: boolean): Promise<number[]>;
73
74
  binImport(importId: string, span: {
74
75
  from?: number;
75
76
  to?: number;
@@ -78,8 +79,8 @@ export declare class AsyncTrackStore implements TrackStore {
78
79
  purgeFromBin(id: number): Promise<boolean>;
79
80
  binEntries(): Promise<import('./types.js').BinEntry[]>;
80
81
  deletedSpans(): Promise<import('./types.js').RecordedSpan[]>;
81
- getImport(id: string): Promise<ImportedTrack | undefined>;
82
- findImports(filter?: ImportFilter): Promise<ImportedTrack[]>;
82
+ getImport(id: string): Promise<import('./types.js').StoredImport | undefined>;
83
+ findImports(filter?: ImportFilter): Promise<import('./types.js').StoredImport[]>;
83
84
  close(): Promise<void>;
84
85
  }
85
86
  export {};
package/dist/import.js CHANGED
@@ -10,12 +10,26 @@ import { refusal } from './bin.js'
10
10
  /** Relative, because the server mounts this page under /@signalk/tracks-plugin. */
11
11
  const IMPORT_URL = '../../plugins/tracks/imports'
12
12
 
13
+ const points = (count) => `${count} point${count === 1 ? '' : 's'}`
14
+
13
15
  /** What an import came to, in words. */
14
- export function imported({ ids, skippedPoints }) {
15
- const count = `Imported ${ids.length} track${ids.length === 1 ? '' : 's'}.`
16
- return skippedPoints > 0
17
- ? `${count} ${skippedPoints} point${skippedPoints === 1 ? '' : 's'} without a time left out.`
18
- : count
16
+ export function imported({ ids, skippedPoints, overlappingPoints = 0 }) {
17
+ return [
18
+ `Imported ${ids.length} track${ids.length === 1 ? '' : 's'}.`,
19
+ ...(skippedPoints > 0 ? [`${points(skippedPoints)} without a time left out.`] : []),
20
+ ...(overlappingPoints > 0 ? [`${points(overlappingPoints)} left out where your vessel already has a track.`] : []),
21
+ ].join(' ')
22
+ }
23
+
24
+ /**
25
+ * The upload's query. Only the own vessel's tracks go into a recording, so
26
+ * replacing is offered for those alone; the route keeps by default.
27
+ */
28
+ export function importQuery({ self, replace }) {
29
+ if (!self) {
30
+ return ''
31
+ }
32
+ return replace ? '?self=true&onOverlap=replace' : '?self=true'
19
33
  }
20
34
 
21
35
  async function messageOf(response) {
@@ -31,6 +45,13 @@ export function start() {
31
45
  const form = document.getElementById('import-form')
32
46
  const status = document.getElementById('import-status')
33
47
  const button = form.querySelector('button[type="submit"]')
48
+ const { self, replace } = form.elements
49
+ // Only the own vessel has a recording to replace.
50
+ const allowReplace = () => {
51
+ replace.disabled = !self.checked
52
+ }
53
+ allowReplace()
54
+ self.addEventListener('change', allowReplace)
34
55
 
35
56
  function show(message, state) {
36
57
  // A failed upload can be tried again; a successful one reloads the page.
@@ -58,7 +79,7 @@ export function start() {
58
79
  show('Choose a GPX file first.', 'error')
59
80
  return
60
81
  }
61
- const query = form.elements.self.checked ? '?self=true' : ''
82
+ const query = importQuery({ self: self.checked, replace: replace.checked })
62
83
  button.disabled = true
63
84
  show('Importing…')
64
85
  let response
@@ -14,9 +14,8 @@ export declare const isImportedId: (id: string) => boolean;
14
14
  * thinning and window code assume time order, and a segment boundary is a gap
15
15
  * in recording, so segments are never merged or reordered.
16
16
  *
17
- * Refuses a track with neither times nor a vessel. Such a track answers no
18
- * time window, and the server refuses a query across all vessels without one,
19
- * so it could only ever be found again by an id the client would have to keep.
17
+ * A track with neither times nor a vessel answers no time window and no
18
+ * vessel query; the plugin's import list and its id are what find it again.
20
19
  */
21
20
  export declare function toImportedTrack(track: TrackImport, id?: string): ImportedTrack;
22
21
  /**
@@ -27,7 +26,6 @@ export declare function toImportedTrack(track: TrackImport, id?: string): Import
27
26
  * becomes an untimed import; a file with some keeps only its timed points,
28
27
  * because an import is timed throughout or not at all, and its times are what
29
28
  * let a query find it. `context` names the vessel in place of the file's own.
30
- * Refused, like any import, when it ends up with neither times nor a vessel.
31
29
  */
32
30
  export declare function fromGpxTrack(gpx: GpxTrack, context?: string | undefined, id?: string): {
33
31
  track: ImportedTrack;
package/dist/index.html CHANGED
@@ -31,6 +31,10 @@
31
31
  <form id="import-form">
32
32
  <label>GPX file <input type="file" name="file" accept=".gpx,application/gpx+xml" required /></label>
33
33
  <label><input type="checkbox" name="self" /> Tracks of my own vessel</label>
34
+ <label
35
+ ><input type="checkbox" name="replace" /> Replace what my vessel recorded over the same time (it goes to the
36
+ recycle bin)</label
37
+ >
34
38
  <button type="submit">Import</button>
35
39
  </form>
36
40
  <p id="import-status" role="status" hidden></p>