signalk-polar-performance-plugin 1.3.3 → 2.0.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/CHANGELOG.md +25 -1
- package/README.md +41 -86
- package/docs/developer-reference.md +22 -130
- package/openApi.json +12 -798
- package/package.json +13 -7
- package/plugin/index.js +178 -521
- package/public/app.js +53 -638
- package/public/index.html +0 -2
- package/public/plotter.html +20 -106
- package/test/lifecycle.test.js +6 -1
- package/docs/import.png +0 -0
- package/docs/polar.png +0 -0
- package/docs/polar_management.png +0 -0
- package/plugin/PolarFileStore.js +0 -133
- package/plugin/PolarTable.js +0 -897
- package/plugin/SI.js +0 -21
- package/plugin/import/ExpeditionImporter.js +0 -152
- package/plugin/import/FormatRegistry.js +0 -21
- package/plugin/import/ImportService.js +0 -199
- package/plugin/import/JieterImporter.js +0 -18
- package/plugin/import/OrcSource.js +0 -396
- package/plugin/import/SourceRegistry.js +0 -17
- package/plugin/import/canonical.js +0 -73
- package/plugin/import/matrixText.js +0 -133
- package/plugin/index.js.bak +0 -1003
- package/test/PolarFileStore.test.js +0 -216
- package/test/PolarTable.test.js +0 -349
- package/test/api.test.js +0 -502
- package/test/migration.test.js +0 -164
package/public/index.html
CHANGED
|
@@ -82,9 +82,7 @@
|
|
|
82
82
|
<ul id="main-nav" class="nav">
|
|
83
83
|
<li class="nav-item"><a class="nav-link active" href="#" data-page="overview">Overview</a></li>
|
|
84
84
|
<li class="nav-item"><a class="nav-link" href="#" data-page="inputs">Inputs</a></li>
|
|
85
|
-
<li class="nav-item"><a class="nav-link" href="#" data-page="settings">Polar</a></li>
|
|
86
85
|
<li class="nav-item"><a class="nav-link" href="#" data-page="outputs">Outputs</a></li>
|
|
87
|
-
<li class="nav-item"><a class="nav-link" href="#" data-page="polars">Polar management</a></li>
|
|
88
86
|
</ul>
|
|
89
87
|
</nav>
|
|
90
88
|
<button class="sidebar-minimizer brand-minimizer" type="button" id="sidebarMinimizer"></button>
|
package/public/plotter.html
CHANGED
|
@@ -126,19 +126,12 @@
|
|
|
126
126
|
|
|
127
127
|
<div class="settings-row">
|
|
128
128
|
<span class="settings-row-label">Active polar</span>
|
|
129
|
-
<
|
|
130
|
-
<span id="polar-name-display">—</span>
|
|
131
|
-
<span class="polar-arrow">›</span>
|
|
132
|
-
</button>
|
|
129
|
+
<span id="polar-name-display">—</span>
|
|
133
130
|
</div>
|
|
134
131
|
|
|
135
132
|
<div class="settings-row">
|
|
136
133
|
<span class="settings-row-label">Performance</span>
|
|
137
|
-
<
|
|
138
|
-
<button class="btn-step" id="btn-perf-minus" aria-label="Decrease">−</button>
|
|
139
|
-
<span class="stepper-value" id="perf-display">100%</span>
|
|
140
|
-
<button class="btn-step" id="btn-perf-plus" aria-label="Increase">+</button>
|
|
141
|
-
</div>
|
|
134
|
+
<span id="perf-display">100%</span>
|
|
142
135
|
</div>
|
|
143
136
|
|
|
144
137
|
<div class="settings-row">
|
|
@@ -147,15 +140,6 @@
|
|
|
147
140
|
</div>
|
|
148
141
|
</div>
|
|
149
142
|
|
|
150
|
-
<!-- Polar picker (opens on top of settings panel) -->
|
|
151
|
-
<div id="polar-list-panel" class="overlay" style="z-index:30" role="dialog" aria-label="Select polar">
|
|
152
|
-
<div class="panel-header">
|
|
153
|
-
<button class="btn-icon" id="btn-back-from-polar" aria-label="Back">‹</button>
|
|
154
|
-
<span class="panel-header-title">Select polar</span>
|
|
155
|
-
</div>
|
|
156
|
-
<div id="polar-list-items"></div>
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
143
|
<script src="polar-canvas.js"></script>
|
|
160
144
|
<script>
|
|
161
145
|
(function () {
|
|
@@ -189,10 +173,11 @@
|
|
|
189
173
|
let libraryVersion = '' // change detector
|
|
190
174
|
let liveTws = null // last fetched live TWS (m/s)
|
|
191
175
|
let liveCurve = null // interpolated curve for liveTws
|
|
192
|
-
let activePolarId = null // current active polar id
|
|
193
176
|
|
|
194
|
-
//
|
|
195
|
-
|
|
177
|
+
// Active polar / performance factor are read-only here — selected in a
|
|
178
|
+
// separate polar management webapp (e.g. signalk-polar-management).
|
|
179
|
+
let activePolarName = null
|
|
180
|
+
let performanceFactor = 1
|
|
196
181
|
let showAllTwsLines = true
|
|
197
182
|
|
|
198
183
|
// -----------------------------------------------------------------------
|
|
@@ -220,26 +205,16 @@
|
|
|
220
205
|
// -----------------------------------------------------------------------
|
|
221
206
|
|
|
222
207
|
async function refreshLibrary() {
|
|
223
|
-
// Resolve active polar ID first
|
|
224
|
-
let polarId
|
|
225
|
-
try {
|
|
226
|
-
const active = await fetchJSON(BASE + '/polars/active')
|
|
227
|
-
polarId = active.id
|
|
228
|
-
} catch (_) {
|
|
229
|
-
return // no active polar — try again on next cycle
|
|
230
|
-
}
|
|
231
|
-
|
|
232
208
|
let newList
|
|
233
209
|
try {
|
|
234
|
-
newList = await fetchJSON(BASE + '/
|
|
210
|
+
newList = await fetchJSON(BASE + '/polar/axes/tws')
|
|
235
211
|
} catch (_) {
|
|
236
|
-
return // polar
|
|
212
|
+
return // no active polar yet — try again on next cycle
|
|
237
213
|
}
|
|
238
214
|
|
|
239
|
-
const version =
|
|
215
|
+
const version = JSON.stringify(newList)
|
|
240
216
|
if (version === libraryVersion) return // no change
|
|
241
217
|
|
|
242
|
-
activePolarId = polarId
|
|
243
218
|
libraryVersion = version
|
|
244
219
|
twsList = newList
|
|
245
220
|
curves = {}
|
|
@@ -248,7 +223,7 @@
|
|
|
248
223
|
await Promise.all(twsList.map(async (tws) => {
|
|
249
224
|
try {
|
|
250
225
|
curves[tws] = await fetchJSON(
|
|
251
|
-
BASE + '/
|
|
226
|
+
BASE + '/polar/queries/curve?tws=' + tws + '&step=' + STEP
|
|
252
227
|
)
|
|
253
228
|
} catch (_) { /* skip individual failure */ }
|
|
254
229
|
}))
|
|
@@ -272,10 +247,10 @@
|
|
|
272
247
|
// -----------------------------------------------------------------------
|
|
273
248
|
|
|
274
249
|
async function fetchCurveForTws(tws) {
|
|
275
|
-
if (!Number.isFinite(tws) || tws <= 0
|
|
250
|
+
if (!Number.isFinite(tws) || tws <= 0) return null
|
|
276
251
|
try {
|
|
277
252
|
return await fetchJSON(
|
|
278
|
-
BASE + '/
|
|
253
|
+
BASE + '/polar/queries/curve?tws=' + tws.toFixed(5) + '&step=' + STEP
|
|
279
254
|
)
|
|
280
255
|
} catch (_) {
|
|
281
256
|
return null
|
|
@@ -311,12 +286,16 @@
|
|
|
311
286
|
async function loadSettings() {
|
|
312
287
|
try {
|
|
313
288
|
const s = await fetchJSON(BASE + '/settings')
|
|
314
|
-
perfAdjust = Number.isFinite(s.perfAdjust) ? s.perfAdjust : 1
|
|
315
289
|
showAllTwsLines = s.showAllTwsLines !== false
|
|
316
|
-
updatePerfDisplay()
|
|
317
290
|
updateTwsToggle()
|
|
318
291
|
polar.setShowAllTwsLines(showAllTwsLines)
|
|
319
292
|
} catch (_) { /* keep defaults */ }
|
|
293
|
+
try {
|
|
294
|
+
const m = await fetchJSON(BASE + '/meta')
|
|
295
|
+
activePolarName = m.activePolar ? (m.activePolar.name || m.activePolar.id) : null
|
|
296
|
+
performanceFactor = Number.isFinite(m.performanceFactor) ? m.performanceFactor : 1
|
|
297
|
+
updatePerfDisplay()
|
|
298
|
+
} catch (_) { /* keep defaults */ }
|
|
320
299
|
}
|
|
321
300
|
|
|
322
301
|
// -----------------------------------------------------------------------
|
|
@@ -324,14 +303,12 @@
|
|
|
324
303
|
// -----------------------------------------------------------------------
|
|
325
304
|
|
|
326
305
|
var settingsPanel = document.getElementById('settings-panel')
|
|
327
|
-
var polarListPanel = document.getElementById('polar-list-panel')
|
|
328
306
|
var perfDisplay = document.getElementById('perf-display')
|
|
329
307
|
var twsToggle = document.getElementById('btn-tws-toggle')
|
|
330
308
|
var polarNameEl = document.getElementById('polar-name-display')
|
|
331
|
-
var polarListEl = document.getElementById('polar-list-items')
|
|
332
309
|
|
|
333
310
|
function updatePerfDisplay() {
|
|
334
|
-
perfDisplay.textContent = Math.round(
|
|
311
|
+
perfDisplay.textContent = Math.round(performanceFactor * 100) + '%'
|
|
335
312
|
}
|
|
336
313
|
|
|
337
314
|
function updateTwsToggle() {
|
|
@@ -347,14 +324,7 @@
|
|
|
347
324
|
// Open settings: fetch fresh state first
|
|
348
325
|
document.getElementById('btn-settings').addEventListener('click', function () {
|
|
349
326
|
loadSettings().then(function () {
|
|
350
|
-
|
|
351
|
-
if (activePolarId) {
|
|
352
|
-
fetchJSON(BASE + '/polars/' + activePolarId)
|
|
353
|
-
.then(function (p) { polarNameEl.textContent = p.name || activePolarId })
|
|
354
|
-
.catch(function () { polarNameEl.textContent = activePolarId })
|
|
355
|
-
} else {
|
|
356
|
-
polarNameEl.textContent = '—'
|
|
357
|
-
}
|
|
327
|
+
polarNameEl.textContent = activePolarName || '—'
|
|
358
328
|
settingsPanel.classList.add('open')
|
|
359
329
|
})
|
|
360
330
|
})
|
|
@@ -363,25 +333,6 @@
|
|
|
363
333
|
settingsPanel.classList.remove('open')
|
|
364
334
|
})
|
|
365
335
|
|
|
366
|
-
// Performance stepper — ±5%, clamped 50%–200%
|
|
367
|
-
function applyPerfAdjust(next) {
|
|
368
|
-
if (next === perfAdjust) return
|
|
369
|
-
perfAdjust = next
|
|
370
|
-
updatePerfDisplay()
|
|
371
|
-
putJSON(BASE + '/settings', { perfAdjust: perfAdjust }).then(function () {
|
|
372
|
-
libraryVersion = '' // force full curve reload — perfAdjust changes speeds but not the TWS list
|
|
373
|
-
return refreshLibrary()
|
|
374
|
-
}).catch(function () {})
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
document.getElementById('btn-perf-minus').addEventListener('click', function () {
|
|
378
|
-
applyPerfAdjust(Math.max(0.5, Math.round((perfAdjust - 0.05) * 100) / 100))
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
document.getElementById('btn-perf-plus').addEventListener('click', function () {
|
|
382
|
-
applyPerfAdjust(Math.min(2.0, Math.round((perfAdjust + 0.05) * 100) / 100))
|
|
383
|
-
})
|
|
384
|
-
|
|
385
336
|
// TWS toggle
|
|
386
337
|
twsToggle.addEventListener('click', function () {
|
|
387
338
|
showAllTwsLines = !showAllTwsLines
|
|
@@ -390,43 +341,6 @@
|
|
|
390
341
|
putJSON(BASE + '/settings', { showAllTwsLines: showAllTwsLines }).catch(function () {})
|
|
391
342
|
})
|
|
392
343
|
|
|
393
|
-
// Polar picker
|
|
394
|
-
document.getElementById('btn-polar-current').addEventListener('click', function () {
|
|
395
|
-
polarListEl.innerHTML = ''
|
|
396
|
-
fetchJSON(BASE + '/polars').then(function (list) {
|
|
397
|
-
list.forEach(function (p) {
|
|
398
|
-
var btn = document.createElement('button')
|
|
399
|
-
btn.className = 'polar-item' + (p.id === activePolarId ? ' is-active' : '')
|
|
400
|
-
var nameSpan = document.createElement('span')
|
|
401
|
-
nameSpan.className = 'polar-item-name'
|
|
402
|
-
nameSpan.textContent = p.name || p.id
|
|
403
|
-
btn.appendChild(nameSpan)
|
|
404
|
-
if (p.id === activePolarId) {
|
|
405
|
-
var check = document.createElement('span')
|
|
406
|
-
check.className = 'polar-item-check'
|
|
407
|
-
check.textContent = '✓'
|
|
408
|
-
btn.appendChild(check)
|
|
409
|
-
}
|
|
410
|
-
btn.addEventListener('click', function () {
|
|
411
|
-
putJSON(BASE + '/polars/active', { id: p.id })
|
|
412
|
-
.then(function () {
|
|
413
|
-
polarNameEl.textContent = p.name || p.id
|
|
414
|
-
polarListPanel.classList.remove('open')
|
|
415
|
-
// Force library refresh on next cycle
|
|
416
|
-
libraryVersion = ''
|
|
417
|
-
})
|
|
418
|
-
.catch(function () {})
|
|
419
|
-
})
|
|
420
|
-
polarListEl.appendChild(btn)
|
|
421
|
-
})
|
|
422
|
-
polarListPanel.classList.add('open')
|
|
423
|
-
}).catch(function () {})
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
document.getElementById('btn-back-from-polar').addEventListener('click', function () {
|
|
427
|
-
polarListPanel.classList.remove('open')
|
|
428
|
-
})
|
|
429
|
-
|
|
430
344
|
// -----------------------------------------------------------------------
|
|
431
345
|
// Start
|
|
432
346
|
// -----------------------------------------------------------------------
|
package/test/lifecycle.test.js
CHANGED
|
@@ -16,7 +16,12 @@ function makeApp(dataDir) {
|
|
|
16
16
|
error: () => {},
|
|
17
17
|
setPluginStatus: () => {},
|
|
18
18
|
setPluginError: () => {},
|
|
19
|
-
savePluginOptions: () => {
|
|
19
|
+
savePluginOptions: (_options, callback) => {
|
|
20
|
+
if (typeof callback !== 'function') {
|
|
21
|
+
throw new TypeError('callback is not a function')
|
|
22
|
+
}
|
|
23
|
+
callback(null)
|
|
24
|
+
},
|
|
20
25
|
getDataDirPath: () => dataDir,
|
|
21
26
|
handleMessage: () => {},
|
|
22
27
|
config: { port: 3000 },
|
package/docs/import.png
DELETED
|
Binary file
|
package/docs/polar.png
DELETED
|
Binary file
|
|
Binary file
|
package/plugin/PolarFileStore.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const fs = require('fs')
|
|
4
|
-
const path = require('path')
|
|
5
|
-
const { PolarTable } = require('./PolarTable')
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* PolarFileStore — manages stored polar resources in the plugin data directory.
|
|
9
|
-
*
|
|
10
|
-
* All file names are bare names (no path, no extension).
|
|
11
|
-
* Files are stored as canonical <name>.json resources in the configured data directory.
|
|
12
|
-
*/
|
|
13
|
-
class PolarFileStore {
|
|
14
|
-
/**
|
|
15
|
-
* @param {string} dataDir - Absolute path to the directory where polar files are stored.
|
|
16
|
-
*/
|
|
17
|
-
constructor(dataDir) {
|
|
18
|
-
this.dataDir = dataDir
|
|
19
|
-
if (!fs.existsSync(dataDir)) {
|
|
20
|
-
fs.mkdirSync(dataDir, { recursive: true })
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Resolves a bare name to its full JSON file path. */
|
|
25
|
-
_jsonPath(name) {
|
|
26
|
-
const safe = path.basename(name)
|
|
27
|
-
return path.join(this.dataDir, `${safe}.json`)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Checks whether a canonical polar resource exists. */
|
|
31
|
-
exists(name) {
|
|
32
|
-
return fs.existsSync(this._jsonPath(name))
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* List all canonical polar resources in the data directory.
|
|
37
|
-
* @returns {string[]} Array of bare names, sorted alphabetically.
|
|
38
|
-
*/
|
|
39
|
-
list() {
|
|
40
|
-
return fs.readdirSync(this.dataDir)
|
|
41
|
-
.filter(fileName => fileName.endsWith('.json'))
|
|
42
|
-
.map(fileName => fileName.slice(0, -5))
|
|
43
|
-
.sort()
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Read the raw JSON content of a stored canonical polar resource.
|
|
48
|
-
* @param {string} name - Bare name of the polar.
|
|
49
|
-
* @returns {string} Raw file content.
|
|
50
|
-
* @throws {Error} If the file does not exist.
|
|
51
|
-
*/
|
|
52
|
-
read(name) {
|
|
53
|
-
const jp = this._jsonPath(name)
|
|
54
|
-
if (!fs.existsSync(jp)) throw new Error(`Polar not found: ${name}`)
|
|
55
|
-
return fs.readFileSync(jp, 'utf8')
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Load a canonical polar resource and return a populated PolarTable.
|
|
60
|
-
* @param {string} name - Bare name of the polar.
|
|
61
|
-
* @returns {PolarTable}
|
|
62
|
-
* @throws {Error} If the file does not exist or is not a canonical resource.
|
|
63
|
-
*/
|
|
64
|
-
load(name) {
|
|
65
|
-
const stored = this.readObject(name)
|
|
66
|
-
if (stored.kind !== 'polarTable') {
|
|
67
|
-
throw new Error(`Stored polar is not a canonical polarTable resource: ${name}`)
|
|
68
|
-
}
|
|
69
|
-
return new PolarTable().loadFromCanonical(stored)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Reads and parses a stored JSON polar resource. */
|
|
73
|
-
readObject(name) {
|
|
74
|
-
const jp = this._jsonPath(name)
|
|
75
|
-
if (!fs.existsSync(jp)) throw new Error(`Polar not found: ${name}`)
|
|
76
|
-
return JSON.parse(fs.readFileSync(jp, 'utf8'))
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Read metadata for a polar.
|
|
81
|
-
* Reads directly from the stored canonical resource.
|
|
82
|
-
*/
|
|
83
|
-
readMeta(name) {
|
|
84
|
-
try {
|
|
85
|
-
const stored = this.readObject(name)
|
|
86
|
-
return {
|
|
87
|
-
...(stored.name ? { boatName: stored.name } : {}),
|
|
88
|
-
...(stored.boatType ? { boatType: stored.boatType } : {}),
|
|
89
|
-
...(stored.sailnumber ? { sailnumber: stored.sailnumber } : {}),
|
|
90
|
-
...(Number.isInteger(stored.year) ? { year: stored.year } : {}),
|
|
91
|
-
...(stored.source ? { source: stored.source } : {}),
|
|
92
|
-
...(stored.notes ? { notes: stored.notes } : {}),
|
|
93
|
-
}
|
|
94
|
-
} catch (_) { return {} }
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* List all polar files with their stored metadata.
|
|
99
|
-
* @returns {{ name: string, boatName?: string, boatType?: string, sailnumber?: string }[]}
|
|
100
|
-
*/
|
|
101
|
-
listWithMeta() {
|
|
102
|
-
return this.list().map(name => ({ name, ...this.readMeta(name) }))
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Save a canonical polar resource as JSON. */
|
|
106
|
-
saveCanonical(name, resource) {
|
|
107
|
-
const stored = { ...resource, id: name, kind: 'polarTable' }
|
|
108
|
-
fs.writeFileSync(this._jsonPath(name), JSON.stringify(stored, null, 2), 'utf8')
|
|
109
|
-
return name
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Copy a stored canonical polar resource to a new name.
|
|
114
|
-
*/
|
|
115
|
-
copy(srcName, dstName) {
|
|
116
|
-
const srcJson = this._jsonPath(srcName)
|
|
117
|
-
if (!fs.existsSync(srcJson)) throw new Error(`Polar not found: ${srcName}`)
|
|
118
|
-
fs.copyFileSync(srcJson, this._jsonPath(dstName))
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Delete a stored canonical polar resource.
|
|
123
|
-
* @param {string} name - Bare name of the polar to delete.
|
|
124
|
-
* @throws {Error} If the file does not exist.
|
|
125
|
-
*/
|
|
126
|
-
delete(name) {
|
|
127
|
-
const jp = this._jsonPath(name)
|
|
128
|
-
if (!fs.existsSync(jp)) throw new Error(`Polar not found: ${name}`)
|
|
129
|
-
fs.unlinkSync(jp)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
module.exports = PolarFileStore
|