signalk-charts-provider-simple 2.0.2 → 2.0.3
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
|
@@ -107,9 +107,9 @@ The interface provides four tabs:
|
|
|
107
107
|
- **Node.js >= 22.5** — uses the built-in `node:sqlite` module, no native compilation needed
|
|
108
108
|
- **Not supported on Cerbo GX** — Venus OS ships Node.js 20, which lacks the `node:sqlite` module. Use v1.6.x if you need Cerbo support.
|
|
109
109
|
|
|
110
|
-
###
|
|
110
|
+
### Recommended for chart conversion: `signalk-container` plugin
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
Displaying charts (serving tiles from `.mbtiles`) needs nothing extra. The [`signalk-container`](https://github.com/dirkwa/signalk-container) plugin is only needed to **convert** charts (S-57 ENC, BSB raster, Pilot Charts, or basemaps), where this plugin delegates all container work to it. The Signal K App Store lists it as a recommended plugin on the Charts Provider Simple detail page and offers a one-click bulk install; without it the plugin still loads and serves charts — only the Convert tab is disabled.
|
|
113
113
|
|
|
114
114
|
`signalk-container` itself needs a Docker- or Podman-compatible runtime on the host. Both engines work the same way; pick whichever is easier:
|
|
115
115
|
|
|
@@ -133,7 +133,7 @@ The plugin uses one combined image that `signalk-container` pulls automatically
|
|
|
133
133
|
- **Signal K in Docker / Podman with a named volume** — the helper container mounts the same named volume. (1.x couldn't do this; named-volume deployments were unable to convert.)
|
|
134
134
|
- **Signal K in Docker / Podman with bind-mounted data** — the helper container gets the resolved host path, even when the in-container path doesn't match the host path. (1.x relied on host/container paths matching exactly; mismatches silently failed.)
|
|
135
135
|
|
|
136
|
-
**Signal K running in Docker?**
|
|
136
|
+
**Signal K running in Docker?** Runtime and socket setup is handled by `signalk-container` — see [docs/running-in-docker.md](docs/running-in-docker.md) for how conversion is delegated and where to configure the host.
|
|
137
137
|
|
|
138
138
|
Conversion concurrency is configurable — see the [CPU budget](#cpu-budget-for-chart-conversion) section. MBTiles charts (display only, no conversion) work without any container runtime, and without `signalk-container`.
|
|
139
139
|
|
package/package.json
CHANGED
|
@@ -382,6 +382,10 @@ select.catalog-folder-select:hover {
|
|
|
382
382
|
appearance: auto;
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
option.catalog-folder-option-new {
|
|
386
|
+
color: var(--md-sys-color-primary);
|
|
387
|
+
}
|
|
388
|
+
|
|
385
389
|
/* Source attribution note */
|
|
386
390
|
.catalog-source-note {
|
|
387
391
|
padding: var(--md-sys-spacing-3) var(--md-sys-spacing-4);
|
|
@@ -364,7 +364,7 @@ function renderCatalogCard(catalog) {
|
|
|
364
364
|
</div>
|
|
365
365
|
</div>
|
|
366
366
|
<div class="catalog-card-body" id="catalog-body-${catalogEscapeId(catalog.file)}">
|
|
367
|
-
${isExpanded && catalogChartData[catalog.file] ? renderChartList(catalog.file) : ''}
|
|
367
|
+
${isExpanded && catalogChartData[catalog.file] ? renderChartList(catalog.file, catalog.label) : ''}
|
|
368
368
|
</div>
|
|
369
369
|
</div>
|
|
370
370
|
`;
|
|
@@ -408,11 +408,12 @@ async function toggleCatalog(catalogFile) {
|
|
|
408
408
|
// Re-render to show charts
|
|
409
409
|
renderCatalogList();
|
|
410
410
|
}
|
|
411
|
-
function renderChartList(catalogFile) {
|
|
411
|
+
function renderChartList(catalogFile, catalogLabel) {
|
|
412
412
|
const data = catalogChartData[catalogFile];
|
|
413
413
|
if (!data?.charts || data.charts.length === 0) {
|
|
414
414
|
return `<div class="catalog-empty">No charts in this catalog.</div>`;
|
|
415
415
|
}
|
|
416
|
+
const defaultFolder = catalogLabelToFolder(catalogLabel);
|
|
416
417
|
return data.charts
|
|
417
418
|
.map((chart) => {
|
|
418
419
|
const cls = chart.urlClassification ?? {
|
|
@@ -495,7 +496,7 @@ function renderChartList(catalogFile) {
|
|
|
495
496
|
${podmanHint}
|
|
496
497
|
${zoomHtml}
|
|
497
498
|
<select class="catalog-folder-select" id="catalog-folder-${catalogEscapeId(chart.number)}">
|
|
498
|
-
${
|
|
499
|
+
${buildFolderOptions(defaultFolder)}
|
|
499
500
|
</select>
|
|
500
501
|
<button class="btn-catalog-download" ${btnDisabled}
|
|
501
502
|
data-catalog-download="${catalogEscapeAttr(chart.number)}"
|
|
@@ -926,6 +927,40 @@ function categoryLabel(category) {
|
|
|
926
927
|
};
|
|
927
928
|
return labels[category] ?? category;
|
|
928
929
|
}
|
|
930
|
+
/**
|
|
931
|
+
* Derive a filesystem-safe folder name from a catalog label.
|
|
932
|
+
* Strips path separators and illegal characters, collapses whitespace, removes
|
|
933
|
+
* leading dots/spaces, and rejects a result of '.' or '..' so a label can't
|
|
934
|
+
* resolve to a traversal or a hidden/relative folder. Falls back to '/' when
|
|
935
|
+
* nothing usable remains.
|
|
936
|
+
*/
|
|
937
|
+
function catalogLabelToFolder(label) {
|
|
938
|
+
if (!label) {
|
|
939
|
+
return '/';
|
|
940
|
+
}
|
|
941
|
+
const safe = label
|
|
942
|
+
.replace(/[/\\:*?"<>|]/g, '')
|
|
943
|
+
.replace(/\s+/g, ' ')
|
|
944
|
+
.trim()
|
|
945
|
+
.replace(/^[.\s]+/, '')
|
|
946
|
+
.trim();
|
|
947
|
+
return safe && safe !== '.' && safe !== '..' ? safe : '/';
|
|
948
|
+
}
|
|
949
|
+
function folderDisplayName(folder) {
|
|
950
|
+
return folder === '/' ? '/' : `/${folder}`;
|
|
951
|
+
}
|
|
952
|
+
function buildFolderOptions(defaultFolder) {
|
|
953
|
+
const isNew = defaultFolder !== '/' && !catalogFolders.includes(defaultFolder);
|
|
954
|
+
const options = [];
|
|
955
|
+
if (isNew) {
|
|
956
|
+
options.push(`<option value="${catalogEscapeAttr(defaultFolder)}" selected class="catalog-folder-option-new">${catalogEscapeHtml(folderDisplayName(defaultFolder))} (new)</option>`);
|
|
957
|
+
}
|
|
958
|
+
for (const folder of catalogFolders) {
|
|
959
|
+
const selected = !isNew && folder === defaultFolder ? ' selected' : '';
|
|
960
|
+
options.push(`<option value="${catalogEscapeAttr(folder)}"${selected}>${catalogEscapeHtml(folderDisplayName(folder))}</option>`);
|
|
961
|
+
}
|
|
962
|
+
return options.join('');
|
|
963
|
+
}
|
|
929
964
|
function catalogEscapeHtml(str) {
|
|
930
965
|
if (!str) {
|
|
931
966
|
return '';
|
|
@@ -481,7 +481,7 @@ function renderCatalogCard(catalog: CatalogRegistryEntry): string {
|
|
|
481
481
|
</div>
|
|
482
482
|
</div>
|
|
483
483
|
<div class="catalog-card-body" id="catalog-body-${catalogEscapeId(catalog.file)}">
|
|
484
|
-
${isExpanded && catalogChartData[catalog.file] ? renderChartList(catalog.file) : ''}
|
|
484
|
+
${isExpanded && catalogChartData[catalog.file] ? renderChartList(catalog.file, catalog.label) : ''}
|
|
485
485
|
</div>
|
|
486
486
|
</div>
|
|
487
487
|
`;
|
|
@@ -533,11 +533,12 @@ async function toggleCatalog(catalogFile: string): Promise<void> {
|
|
|
533
533
|
renderCatalogList();
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
-
function renderChartList(catalogFile: string): string {
|
|
536
|
+
function renderChartList(catalogFile: string, catalogLabel: string): string {
|
|
537
537
|
const data = catalogChartData[catalogFile];
|
|
538
538
|
if (!data?.charts || data.charts.length === 0) {
|
|
539
539
|
return `<div class="catalog-empty">No charts in this catalog.</div>`;
|
|
540
540
|
}
|
|
541
|
+
const defaultFolder = catalogLabelToFolder(catalogLabel);
|
|
541
542
|
|
|
542
543
|
return data.charts
|
|
543
544
|
.map((chart) => {
|
|
@@ -625,7 +626,7 @@ function renderChartList(catalogFile: string): string {
|
|
|
625
626
|
${podmanHint}
|
|
626
627
|
${zoomHtml}
|
|
627
628
|
<select class="catalog-folder-select" id="catalog-folder-${catalogEscapeId(chart.number)}">
|
|
628
|
-
${
|
|
629
|
+
${buildFolderOptions(defaultFolder)}
|
|
629
630
|
</select>
|
|
630
631
|
<button class="btn-catalog-download" ${btnDisabled}
|
|
631
632
|
data-catalog-download="${catalogEscapeAttr(chart.number)}"
|
|
@@ -1092,6 +1093,50 @@ function categoryLabel(category: CatalogCategory | string): string {
|
|
|
1092
1093
|
return labels[category] ?? category;
|
|
1093
1094
|
}
|
|
1094
1095
|
|
|
1096
|
+
/**
|
|
1097
|
+
* Derive a filesystem-safe folder name from a catalog label.
|
|
1098
|
+
* Strips path separators and illegal characters, collapses whitespace, removes
|
|
1099
|
+
* leading dots/spaces, and rejects a result of '.' or '..' so a label can't
|
|
1100
|
+
* resolve to a traversal or a hidden/relative folder. Falls back to '/' when
|
|
1101
|
+
* nothing usable remains.
|
|
1102
|
+
*/
|
|
1103
|
+
function catalogLabelToFolder(label: string | undefined | null): string {
|
|
1104
|
+
if (!label) {
|
|
1105
|
+
return '/';
|
|
1106
|
+
}
|
|
1107
|
+
const safe = label
|
|
1108
|
+
.replace(/[/\\:*?"<>|]/g, '')
|
|
1109
|
+
.replace(/\s+/g, ' ')
|
|
1110
|
+
.trim()
|
|
1111
|
+
.replace(/^[.\s]+/, '')
|
|
1112
|
+
.trim();
|
|
1113
|
+
return safe && safe !== '.' && safe !== '..' ? safe : '/';
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function folderDisplayName(folder: string): string {
|
|
1117
|
+
return folder === '/' ? '/' : `/${folder}`;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function buildFolderOptions(defaultFolder: string): string {
|
|
1121
|
+
const isNew = defaultFolder !== '/' && !catalogFolders.includes(defaultFolder);
|
|
1122
|
+
const options: string[] = [];
|
|
1123
|
+
|
|
1124
|
+
if (isNew) {
|
|
1125
|
+
options.push(
|
|
1126
|
+
`<option value="${catalogEscapeAttr(defaultFolder)}" selected class="catalog-folder-option-new">${catalogEscapeHtml(folderDisplayName(defaultFolder))} (new)</option>`
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
for (const folder of catalogFolders) {
|
|
1131
|
+
const selected = !isNew && folder === defaultFolder ? ' selected' : '';
|
|
1132
|
+
options.push(
|
|
1133
|
+
`<option value="${catalogEscapeAttr(folder)}"${selected}>${catalogEscapeHtml(folderDisplayName(folder))}</option>`
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return options.join('');
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1095
1140
|
function catalogEscapeHtml(str: string | undefined | null): string {
|
|
1096
1141
|
if (!str) {
|
|
1097
1142
|
return '';
|
|
@@ -36,8 +36,11 @@ async function initConvertTab() {
|
|
|
36
36
|
${!convertPodmanAvailable
|
|
37
37
|
? `
|
|
38
38
|
<div class="catalog-podman-warning">
|
|
39
|
-
<strong>
|
|
40
|
-
|
|
39
|
+
<strong>Chart conversion unavailable.</strong>
|
|
40
|
+
Converting charts needs the
|
|
41
|
+
<a href="https://github.com/dirkwa/signalk-container" target="_blank" rel="noopener">signalk-container</a>
|
|
42
|
+
plugin (install it from the App Store) plus a Docker- or Podman-compatible
|
|
43
|
+
runtime on the host. Chart display works without it.
|
|
41
44
|
<a href="https://github.com/dirkwa/signalk-charts-provider-simple/blob/main/docs/running-in-docker.md" target="_blank" rel="noopener">See setup notes</a>.
|
|
42
45
|
</div>
|
|
43
46
|
`
|
|
@@ -143,7 +146,7 @@ function setupDropZone(zoneId, inputId) {
|
|
|
143
146
|
}
|
|
144
147
|
zone.onclick = () => {
|
|
145
148
|
if (!convertPodmanAvailable) {
|
|
146
|
-
alert('Chart conversion needs a Docker- or Podman-compatible
|
|
149
|
+
alert('Chart conversion needs the signalk-container plugin and a Docker- or Podman-compatible runtime. See the warning above.');
|
|
147
150
|
return;
|
|
148
151
|
}
|
|
149
152
|
input.click();
|
|
@@ -159,7 +162,7 @@ function setupDropZone(zoneId, inputId) {
|
|
|
159
162
|
e.preventDefault();
|
|
160
163
|
zone.classList.remove('dragover');
|
|
161
164
|
if (!convertPodmanAvailable) {
|
|
162
|
-
alert('Chart conversion needs a Docker- or Podman-compatible
|
|
165
|
+
alert('Chart conversion needs the signalk-container plugin and a Docker- or Podman-compatible runtime. See the warning above.');
|
|
163
166
|
return;
|
|
164
167
|
}
|
|
165
168
|
const dt = e.dataTransfer;
|
|
@@ -61,8 +61,11 @@ async function initConvertTab(): Promise<void> {
|
|
|
61
61
|
!convertPodmanAvailable
|
|
62
62
|
? `
|
|
63
63
|
<div class="catalog-podman-warning">
|
|
64
|
-
<strong>
|
|
65
|
-
|
|
64
|
+
<strong>Chart conversion unavailable.</strong>
|
|
65
|
+
Converting charts needs the
|
|
66
|
+
<a href="https://github.com/dirkwa/signalk-container" target="_blank" rel="noopener">signalk-container</a>
|
|
67
|
+
plugin (install it from the App Store) plus a Docker- or Podman-compatible
|
|
68
|
+
runtime on the host. Chart display works without it.
|
|
66
69
|
<a href="https://github.com/dirkwa/signalk-charts-provider-simple/blob/main/docs/running-in-docker.md" target="_blank" rel="noopener">See setup notes</a>.
|
|
67
70
|
</div>
|
|
68
71
|
`
|
|
@@ -178,7 +181,7 @@ function setupDropZone(zoneId: string, inputId: string): void {
|
|
|
178
181
|
zone.onclick = () => {
|
|
179
182
|
if (!convertPodmanAvailable) {
|
|
180
183
|
alert(
|
|
181
|
-
'Chart conversion needs a Docker- or Podman-compatible
|
|
184
|
+
'Chart conversion needs the signalk-container plugin and a Docker- or Podman-compatible runtime. See the warning above.'
|
|
182
185
|
);
|
|
183
186
|
return;
|
|
184
187
|
}
|
|
@@ -199,7 +202,7 @@ function setupDropZone(zoneId: string, inputId: string): void {
|
|
|
199
202
|
zone.classList.remove('dragover');
|
|
200
203
|
if (!convertPodmanAvailable) {
|
|
201
204
|
alert(
|
|
202
|
-
'Chart conversion needs a Docker- or Podman-compatible
|
|
205
|
+
'Chart conversion needs the signalk-container plugin and a Docker- or Podman-compatible runtime. See the warning above.'
|
|
203
206
|
);
|
|
204
207
|
return;
|
|
205
208
|
}
|