@signalk/charts-plugin 3.7.0 → 3.8.1

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.
Files changed (60) hide show
  1. package/.prettierignore +1 -0
  2. package/CHANGELOG.md +109 -0
  3. package/README.md +72 -0
  4. package/docs/screenshots/1_overview.png +0 -0
  5. package/docs/screenshots/2_region.png +0 -0
  6. package/docs/screenshots/3_jobs.png +0 -0
  7. package/package.json +16 -11
  8. package/plugin/cacheLayout.d.ts +72 -0
  9. package/plugin/cacheLayout.d.ts.map +1 -0
  10. package/plugin/cacheLayout.js +243 -0
  11. package/plugin/cacheLayout.js.map +1 -0
  12. package/plugin/chartDownloader.d.ts +122 -29
  13. package/plugin/chartDownloader.d.ts.map +1 -1
  14. package/plugin/chartDownloader.js +491 -314
  15. package/plugin/chartDownloader.js.map +1 -1
  16. package/plugin/chartDownloaderMBTilesHelpers.d.ts +10 -0
  17. package/plugin/chartDownloaderMBTilesHelpers.d.ts.map +1 -0
  18. package/plugin/chartDownloaderMBTilesHelpers.js +202 -0
  19. package/plugin/chartDownloaderMBTilesHelpers.js.map +1 -0
  20. package/plugin/chartDownloaderTileHelpers.d.ts +30 -0
  21. package/plugin/chartDownloaderTileHelpers.d.ts.map +1 -0
  22. package/plugin/chartDownloaderTileHelpers.js +277 -0
  23. package/plugin/chartDownloaderTileHelpers.js.map +1 -0
  24. package/plugin/charts.d.ts.map +1 -1
  25. package/plugin/charts.js +24 -39
  26. package/plugin/charts.js.map +1 -1
  27. package/plugin/constants.d.ts.map +1 -1
  28. package/plugin/index.d.ts.map +1 -1
  29. package/plugin/index.js +564 -47
  30. package/plugin/index.js.map +1 -1
  31. package/plugin/pluginStatus.js.map +1 -1
  32. package/plugin/projection.js.map +1 -1
  33. package/plugin/tileServer.d.ts +4 -2
  34. package/plugin/tileServer.d.ts.map +1 -1
  35. package/plugin/tileServer.js +37 -8
  36. package/plugin/tileServer.js.map +1 -1
  37. package/plugin/tokenProvider.d.ts +63 -0
  38. package/plugin/tokenProvider.d.ts.map +1 -0
  39. package/plugin/tokenProvider.js +258 -0
  40. package/plugin/tokenProvider.js.map +1 -0
  41. package/plugin/types.d.ts +37 -2
  42. package/plugin/types.d.ts.map +1 -1
  43. package/public/images/jobs.png +0 -0
  44. package/public/images/overview.png +0 -0
  45. package/public/images/region.png +0 -0
  46. package/public/index.html +392 -113
  47. package/public/js/index.js +620 -0
  48. package/public/leaflet/images/downloads.png +0 -0
  49. package/public/leaflet/images/layers-2x.png +0 -0
  50. package/public/leaflet/images/layers.png +0 -0
  51. package/public/leaflet/images/lifebuoy.png +0 -0
  52. package/public/leaflet/images/spritesheet-2x.png +0 -0
  53. package/public/leaflet/images/spritesheet.svg +156 -0
  54. package/public/leaflet/leaflet.css +677 -0
  55. package/public/leaflet/leaflet.draw.css +253 -0
  56. package/public/leaflet/leaflet.draw.js +2957 -0
  57. package/public/leaflet/leaflet.js +7467 -0
  58. package/public/style.css +351 -37
  59. package/docs/screenshots/1_placeholder.png +0 -0
  60. package/public/index.js +0 -349
package/public/index.js DELETED
@@ -1,349 +0,0 @@
1
- // POSTs a seed-job request and returns the parsed job info. Extracted so the
2
- // fetch + response-validation logic can be unit-tested in node without jsdom.
3
- // The browser still calls this from the click handler below.
4
- async function submitSeedJob(chart, body, fetchImpl) {
5
- const fetchFn = fetchImpl || globalThis.fetch
6
- const resp = await fetchFn(
7
- `/signalk/chart-tiles/cache/${encodeURIComponent(chart)}`,
8
- {
9
- method: 'POST',
10
- headers: { 'Content-Type': 'application/json' },
11
- body: JSON.stringify(body)
12
- }
13
- )
14
- if (!resp.ok) {
15
- let text = ''
16
- try {
17
- text = await resp.text()
18
- } catch (_readErr) {
19
- // Ignore read errors when composing the error message; the status code
20
- // and our surrounding context are enough to diagnose.
21
- }
22
- throw new Error(
23
- `Failed to seed charts: ${resp.status}${text ? ` - ${text}` : ''}`
24
- )
25
- }
26
- const data = await resp.json()
27
- if (!data || typeof data.id !== 'number') {
28
- throw new Error('Job response is missing a numeric id')
29
- }
30
- return data
31
- }
32
-
33
- // Node-only export for the unit test. The browser script tag ignores this
34
- // branch because `module` is undefined in a plain <script>.
35
- if (typeof module !== 'undefined' && module.exports) {
36
- module.exports = { submitSeedJob }
37
- }
38
-
39
- // The browser attaches the DOM handlers; node (test context) skips them.
40
- if (typeof document !== 'undefined') registerDomHandlers()
41
-
42
- function registerDomHandlers() {
43
- document.addEventListener('DOMContentLoaded', async () => {
44
- const regionSelect = document.getElementById('region')
45
- const bboxInputs = document.querySelectorAll(
46
- '#bboxMinLon, #bboxMinLat, #bboxMaxLon, #bboxMaxLat'
47
- )
48
- const tileInputs = document.querySelectorAll('#tileX, #tileY, #tileZ')
49
-
50
- function updateInputs() {
51
- const type = document.querySelector(
52
- 'input[name="inputType"]:checked'
53
- ).value
54
-
55
- regionSelect.disabled = type !== 'region'
56
- bboxInputs.forEach((i) => (i.disabled = type !== 'bbox'))
57
- tileInputs.forEach((i) => (i.disabled = type !== 'tile'))
58
- }
59
-
60
- // Attach listeners
61
- document.querySelectorAll('input[name="inputType"]').forEach((r) => {
62
- r.addEventListener('change', updateInputs)
63
- })
64
-
65
- updateInputs() // initial load
66
-
67
- if (!regionSelect) return
68
-
69
- try {
70
- const resp = await fetch('/signalk/v2/api/resources/regions')
71
- if (!resp.ok) throw new Error(`Failed to fetch regions: ${resp.status}`)
72
- const data = await resp.json()
73
- // clear existing options
74
- regionSelect.innerHTML = ''
75
-
76
- const entries = Object.entries(data || {})
77
- entries
78
- .sort(([, a], [, b]) => {
79
- const na = a && a.name ? String(a.name) : ''
80
- const nb = b && b.name ? String(b.name) : ''
81
- return na.localeCompare(nb)
82
- })
83
- .forEach(([id, info]) => {
84
- const opt = document.createElement('option')
85
- opt.value = id
86
- opt.textContent = info && info.name ? info.name : id
87
- regionSelect.appendChild(opt)
88
- })
89
-
90
- // if no regions returned, add a fallback option
91
- if (!regionSelect.options.length) {
92
- const opt = document.createElement('option')
93
- opt.value = ''
94
- opt.textContent = '-- No regions available --'
95
- regionSelect.appendChild(opt)
96
- }
97
- } catch (err) {
98
- console.error('Error loading regions:', err)
99
- }
100
-
101
- //Fetch available maps and populate the chart select box
102
- const chartSelect = document.getElementById('chart')
103
- if (!chartSelect) return
104
-
105
- try {
106
- const resp = await fetch('/signalk/v2/api/resources/charts')
107
- if (!resp.ok) throw new Error(`Failed to fetch charts: ${resp.status}`)
108
- const data = await resp.json()
109
- // clear existing options
110
- chartSelect.innerHTML = ''
111
-
112
- const entries = Object.entries(data || {})
113
- entries
114
- .sort(([, a], [, b]) => {
115
- const na = a && a.name ? String(a.name) : ''
116
- const nb = b && b.name ? String(b.name) : ''
117
- return na.localeCompare(nb)
118
- })
119
- .forEach(([id, info]) => {
120
- if (info.proxy === true) {
121
- const opt = document.createElement('option')
122
- opt.value = id
123
- opt.textContent = info && info.name ? info.name : id
124
- chartSelect.appendChild(opt)
125
- }
126
- })
127
-
128
- // if no maps returned, add a fallback option
129
- if (!chartSelect.options.length) {
130
- const opt = document.createElement('option')
131
- opt.value = ''
132
- opt.textContent = '-- No maps available --'
133
- chartSelect.appendChild(opt)
134
- }
135
- } catch (err) {
136
- console.error('Error loading maps:', err)
137
- }
138
-
139
- document
140
- .getElementById('createJobButton')
141
- .addEventListener('click', async () => {
142
- try {
143
- const chart = document.getElementById('chart').value
144
- const regionGUID = document.getElementById('region').value
145
- const maxZoom = document.getElementById('maxZoom').value
146
- const bbox = {
147
- minLon: parseFloat(document.getElementById('bboxMinLon').value),
148
- minLat: parseFloat(document.getElementById('bboxMinLat').value),
149
- maxLon: parseFloat(document.getElementById('bboxMaxLon').value),
150
- maxLat: parseFloat(document.getElementById('bboxMaxLat').value)
151
- }
152
- // const tile = {
153
- // z: parseInt(document.getElementById('tileZ').value),
154
- // x: parseInt(document.getElementById('tileX').value),
155
- // y: parseInt(document.getElementById('tileY').value),
156
-
157
- // };
158
- // console.log(tile);
159
- const type = document.querySelector(
160
- 'input[name="inputType"]:checked'
161
- ).value
162
- const body = {}
163
- if (type === 'region') {
164
- body.regionGUID = regionGUID
165
- } else if (type === 'bbox') {
166
- body.bbox = bbox
167
- }
168
- // else if (type === 'tile') {
169
- // body.tile = tile;
170
- // }
171
- body.maxZoom = maxZoom
172
- await submitSeedJob(chart, body)
173
- // Trigger an immediate refresh so the new job appears without waiting
174
- // for the 2s polling interval.
175
- fetchActiveJobs()
176
- } catch (err) {
177
- console.error('Error seeding charts:', err)
178
- alert(`Failed to start seeding job: ${err.message}`)
179
- }
180
- })
181
-
182
- const fmt = (n) => (typeof n === 'number' ? n.toLocaleString() : '-')
183
- const statusText = (s) => {
184
- switch (s) {
185
- case 0:
186
- return 'Stopped'
187
- case 1:
188
- return 'Running'
189
- default:
190
- return String(s)
191
- }
192
- }
193
-
194
- function renderJobs(items) {
195
- const tbody = document.querySelector('#activeJobsTable tbody')
196
- if (!tbody) return
197
- tbody.innerHTML = ''
198
- if (!Array.isArray(items) || items.length === 0) {
199
- const tr = document.createElement('tr')
200
- tr.innerHTML = '<td colspan="10" class="muted">No active jobs</td>'
201
- tbody.appendChild(tr)
202
- return
203
- }
204
-
205
- items.forEach((it, idx) => {
206
- const tr = document.createElement('tr')
207
-
208
- const pct =
209
- typeof it.progress === 'number' && isFinite(it.progress)
210
- ? Math.max(0, Math.min(100, it.progress * 100))
211
- : 0
212
- const pctText = `${pct.toFixed(1)}%`
213
- tr.innerHTML = [
214
- `<td>${idx + 1}</td>`,
215
- `<td>${it.chartName || '-'}</td>`,
216
- `<td>${it.regionName || '-'}</td>`,
217
- `<td>${fmt(it.totalTiles)}</td>`,
218
- `<td>${fmt(it.downloadedTiles)}</td>`,
219
- `<td>${fmt(it.cachedTiles)}</td>`,
220
- `<td>${fmt(it.failedTiles)}</td>`,
221
- `<td>
222
- <span class="progress-bar" aria-hidden="true">
223
- <span class="progress-fill" style="width:${pct}%"></span>
224
- </span>
225
- <span class="percent">${pctText}</span>
226
- </td>`,
227
- `<td>${statusText(it.status)}</td>`,
228
- `
229
- <td class="action-buttons">
230
- <button class="btn startstop-btn" data-id="${it.id}" data-totalTiles="${it.totalTiles}" title="Start">
231
- <svg class="icon-play ${it.status === 1 ? 'hidden' : ''}" viewBox="0 0 24 24" width="16" height="16" fill="white">
232
- <path d="M8 5v14l11-7z"/>
233
- </svg>
234
- <svg class="icon-pause ${it.status === 1 ? '' : 'hidden'}" viewBox="0 0 24 24" width="16" height="16" fill="white">
235
- <path d="M6 19h4V5H6zm8-14v14h4V5z"/>
236
- </svg>
237
- </button>
238
- <button class="btn delete-btn" data-id="${it.id}" title="Delete">
239
- <svg viewBox="0 0 24 24" width="16" height="16" fill="white">
240
- <path d="M3 6h18v2H3zm3 3h12v12H6zM8 2h8v2H8z"/>
241
- </svg>
242
- </button>
243
- <button class="btn remove-btn" data-id="${it.id}" title="Remove">
244
- <svg viewBox="0 0 24 24" width="16" height="16" fill="white">
245
- <path d="M18.3 5.71L12 12l6.3 6.29-1.41 1.42L10.59 13.4 4.29 19.71 2.88 18.3 9.17 12 2.88 5.71 4.29 4.3 10.59 10.6l6.3-6.3z"/>
246
- </svg>
247
- </button>
248
- </td>
249
- `
250
- ].join('')
251
-
252
- tr.querySelectorAll('.startstop-btn').forEach((button) => {
253
- button.addEventListener('click', () => {
254
- const playIcon = button.querySelector('.icon-play')
255
- const pauseIcon = button.querySelector('.icon-pause')
256
- const jobId = button.getAttribute('data-id')
257
- const totalTiles = button.getAttribute('data-totalTiles')
258
- const isRunning = pauseIcon.classList.contains('hidden')
259
- console.log(isRunning)
260
- if (isRunning) {
261
- // Switch to stop
262
- if (
263
- totalTiles > 100000 &&
264
- !confirm(
265
- `This job has more than ${totalTiles} tiles to download. Starting it may take a long time and put high load on the server. Are you sure you want to start it?`
266
- )
267
- ) {
268
- return
269
- }
270
- takeAction(jobId, 'start')
271
- button.title = 'Stop'
272
- } else {
273
- // Switch to start
274
- takeAction(jobId, 'stop')
275
- button.title = 'Start'
276
- }
277
- })
278
- })
279
-
280
- tr.querySelectorAll('.delete-btn').forEach((button) => {
281
- button.addEventListener('click', () => {
282
- if (
283
- confirm(
284
- 'This will delete all cached tiles for this job. Are you sure?'
285
- )
286
- ) {
287
- const jobId = button.getAttribute('data-id')
288
- if (jobId) {
289
- takeAction(jobId, 'delete')
290
- }
291
- }
292
- })
293
- })
294
-
295
- tr.querySelectorAll('.remove-btn').forEach((button) => {
296
- button.addEventListener('click', () => {
297
- const jobId = button.getAttribute('data-id')
298
- if (jobId) {
299
- takeAction(jobId, 'remove')
300
- }
301
- })
302
- })
303
-
304
- tbody.appendChild(tr)
305
- })
306
-
307
- function takeAction(jobId, action) {
308
- fetch(`/signalk/chart-tiles/cache/jobs/${jobId}`, {
309
- method: 'POST',
310
- headers: {
311
- 'Content-Type': 'application/json'
312
- },
313
- body: JSON.stringify({
314
- action: action
315
- })
316
- })
317
- .then((response) => {
318
- if (!response.ok) {
319
- throw new Error(
320
- `Failed to ${action} job ${jobId}: ${response.status}`
321
- )
322
- }
323
- fetchActiveJobs()
324
- })
325
- .catch((error) => {
326
- console.error(`Error ${action} job:`, error)
327
- })
328
- }
329
- }
330
-
331
- async function fetchActiveJobs() {
332
- const tbody = document.querySelector('#activeJobsTable tbody')
333
- try {
334
- const resp = await fetch('/signalk/chart-tiles/cache/jobs')
335
- if (!resp.ok) throw new Error('HTTP ' + resp.status)
336
- const data = await resp.json()
337
- renderJobs(data)
338
- } catch (err) {
339
- console.error('Error fetching active jobs:', err)
340
- tbody.innerHTML =
341
- '<tr><td colspan="10" class="muted">Error loading active jobs</td></tr>'
342
- }
343
- }
344
-
345
- fetchActiveJobs()
346
- // refresh every 2 seconds
347
- setInterval(fetchActiveJobs, 2000)
348
- })
349
- }