signalk-charts-provider-simple 2.5.0 → 2.6.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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +130 -8
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/folder-state.d.ts +8 -0
- package/dist/utils/folder-state.d.ts.map +1 -0
- package/dist/utils/folder-state.js +93 -0
- package/dist/utils/folder-state.js.map +1 -0
- package/package.json +1 -1
- package/public/css/manage-charts.css +55 -0
- package/public/js/globals.d.ts +2 -0
- package/public/js/manage-charts-enhanced.js +71 -6
- package/public/js/manage-charts-enhanced.ts +87 -6
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ A lightweight Signal K server plugin for managing local nautical charts, written
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Local Chart Management**: MBTiles (raster and vector), with folder organization and enable/disable toggles
|
|
8
|
+
- **Folder Groups**: Enable/disable whole chart folders at once (OpenCPN-style groups, including nested folders); clients like Freeboard-SK see the change live via `resources.charts` deltas
|
|
8
9
|
- **Download Manager**: Built-in download queue with progress tracking and ZIP extraction
|
|
9
10
|
- **Chart Catalog**: Browse and download charts from [chartcatalogs.github.io](https://chartcatalogs.github.io/) with automatic update notifications
|
|
10
11
|
- **NOAA Charts**: Draw a coverage region on a map from NOAA band-4 chart footprints; the plugin bundles the overlapping band-3/4/5 ENCs and converts them into a single vector MBTiles named after your chart set
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,MAAM,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,MAAM,EAAE,MAAM,qBAAqB,CAAC;AA8HxD,OAAO,KAAK,EACV,aAAa,EAEb,iBAAiB,EAIjB,QAAQ,EAET,MAAM,YAAY,CAAC;AAwBpB,QAAA,MAAM,iBAAiB,GAAI,KAAK,iBAAiB,KAAG,MAujInD,CAAC;AA4GF,eAAO,MAAM,oBAAoB,GAC/B,KAAK,QAAQ,EACb,UAAU,aAAa,EACvB,GAAG,MAAM,EACT,GAAG,MAAM,EACT,GAAG,MAAM,KACR,IAiDF,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { findCharts, findRepairableCharts } from './charts-loader.js';
|
|
|
9
9
|
import { checkForUpdates, classifyUrl, fetchCatalog, fetchCatalogRegistry, getCachedCatalog, getCatalogRegistry, getCatalogsWithInstalledCharts, getConvertingCharts, getConvertingCount, getInstalledCatalogCharts, getRegistryStatus, initCatalogManager, pruneStaleInstalls, removeInstall, removeInstallByFilename, renameInstallFilename, rollbackInstall, setConvertingState, setInstallFilename, trackInstall } from './utils/catalog-manager.js';
|
|
10
10
|
import { cleanCatalogTitle } from './utils/catalog-title.js';
|
|
11
11
|
import { initChartState, isChartEnabled, setChartEnabled } from './utils/chart-state.js';
|
|
12
|
+
import { initFolderState, isFolderEnabled, isFolderPathEnabled, setFolderEnabled, removeFolderState } from './utils/folder-state.js';
|
|
12
13
|
import { getCpuBudget, setCpuBudget } from './utils/concurrency.js';
|
|
13
14
|
import { PLUGIN_OWNER_ID } from './utils/container-jobs.js';
|
|
14
15
|
import { getContainerManager, waitForContainerManager } from './utils/container-manager.js';
|
|
@@ -147,6 +148,11 @@ const pluginConstructor = (app) => {
|
|
|
147
148
|
clearInterval(catalogUpdateInterval);
|
|
148
149
|
catalogUpdateInterval = null;
|
|
149
150
|
}
|
|
151
|
+
// Release sqlite handles: on Windows an open handle keeps the
|
|
152
|
+
// .mbtiles file locked, and Signal K restarts the plugin on every
|
|
153
|
+
// config save, which would otherwise leak the whole map each time.
|
|
154
|
+
closeProviderHandles(Object.values(chartProviders));
|
|
155
|
+
chartProviders = {};
|
|
150
156
|
app.setPluginStatus('stopped');
|
|
151
157
|
},
|
|
152
158
|
registerWithRouter: (router) => {
|
|
@@ -265,6 +271,7 @@ const pluginConstructor = (app) => {
|
|
|
265
271
|
console.log(`[charts-provider] chartPath resolved to ${chartPath} (marker write failed)`);
|
|
266
272
|
}
|
|
267
273
|
initChartState(pluginDataDir);
|
|
274
|
+
initFolderState(pluginDataDir);
|
|
268
275
|
const dataDir = pluginDataDir;
|
|
269
276
|
initCatalogManager(dataDir, app.debug.bind(app));
|
|
270
277
|
initNoaaEncFootprints(dataDir, app.debug.bind(app));
|
|
@@ -442,6 +449,43 @@ const pluginConstructor = (app) => {
|
|
|
442
449
|
// a stale snapshot would delete them as "invalid".
|
|
443
450
|
void cleanupChartDirectory(chartPath);
|
|
444
451
|
};
|
|
452
|
+
// A chart is served only when its own flag AND every ancestor folder's
|
|
453
|
+
// flag are enabled; folder state is a separate layer that never rewrites
|
|
454
|
+
// per-chart state.
|
|
455
|
+
const isChartVisible = (chartPath, filePath) => {
|
|
456
|
+
const relativePath = path.relative(chartPath, filePath);
|
|
457
|
+
return isChartEnabled(relativePath) && isFolderPathEnabled(path.dirname(relativePath));
|
|
458
|
+
};
|
|
459
|
+
// findCharts opens a sqlite handle for every chart it finds; any provider
|
|
460
|
+
// that doesn't make it into (or drops out of) `chartProviders` must be
|
|
461
|
+
// closed explicitly or the handle leaks — and on Windows keeps the
|
|
462
|
+
// .mbtiles file locked. Safe to call on live objects being replaced:
|
|
463
|
+
// tile serving is synchronous from map lookup to handle use, so no
|
|
464
|
+
// in-flight request can hold a closed handle across an await.
|
|
465
|
+
const closeProviderHandles = (providers) => {
|
|
466
|
+
for (const provider of providers) {
|
|
467
|
+
try {
|
|
468
|
+
provider._mbtilesHandle?.close();
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// Already closed — nothing to release.
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
const partitionVisibleCharts = (chartPath, charts) => {
|
|
476
|
+
const visible = {};
|
|
477
|
+
const dropped = [];
|
|
478
|
+
for (const [identifier, chart] of Object.entries(charts)) {
|
|
479
|
+
if (isChartVisible(chartPath, chart._filePath || '')) {
|
|
480
|
+
visible[identifier] = chart;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
dropped.push(chart);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
closeProviderHandles(dropped);
|
|
487
|
+
return visible;
|
|
488
|
+
};
|
|
445
489
|
// Load enabled charts into `chartProviders` and prune stale install
|
|
446
490
|
// records. Display-critical: must run (and resolve) before the resource
|
|
447
491
|
// provider is registered so `listResources` has charts to return. Returns
|
|
@@ -449,10 +493,7 @@ const pluginConstructor = (app) => {
|
|
|
449
493
|
const loadChartProviders = async (chartPath) => {
|
|
450
494
|
try {
|
|
451
495
|
const charts = await findCharts(chartPath);
|
|
452
|
-
const enabledCharts =
|
|
453
|
-
const relativePath = path.relative(chartPath, chart._filePath || '');
|
|
454
|
-
return isChartEnabled(relativePath);
|
|
455
|
-
}));
|
|
496
|
+
const enabledCharts = partitionVisibleCharts(chartPath, charts);
|
|
456
497
|
app.debug(`Chart provider: Found ${Object.keys(charts).length} charts (${Object.keys(enabledCharts).length} enabled) from ${chartPath}.`);
|
|
457
498
|
chartProviders = enabledCharts;
|
|
458
499
|
pruneStaleInstalls(Object.keys(charts));
|
|
@@ -615,6 +656,12 @@ const pluginConstructor = (app) => {
|
|
|
615
656
|
const ChartToggleBody = Type.Object({
|
|
616
657
|
enabled: Type.Boolean()
|
|
617
658
|
});
|
|
659
|
+
// folderPath travels in the body (not a URL param): nested folders contain
|
|
660
|
+
// '/' and Express 5 rejects encoded %2F in path segments.
|
|
661
|
+
const FolderToggleBody = Type.Object({
|
|
662
|
+
folderPath: Type.String({ minLength: 1 }),
|
|
663
|
+
enabled: Type.Boolean()
|
|
664
|
+
});
|
|
618
665
|
const MoveChartBody = Type.Object({
|
|
619
666
|
chartPath: Type.String({ minLength: 1 }),
|
|
620
667
|
targetFolder: Type.String({ minLength: 1 })
|
|
@@ -915,13 +962,22 @@ const pluginConstructor = (app) => {
|
|
|
915
962
|
return {
|
|
916
963
|
...chart,
|
|
917
964
|
enabled: isChartEnabled(chart.relativePath),
|
|
965
|
+
folderEnabled: isFolderPathEnabled(chart.folder),
|
|
918
966
|
downloading: downloadingFiles.has(chart.name),
|
|
919
967
|
converting
|
|
920
968
|
};
|
|
921
969
|
});
|
|
970
|
+
const folderStates = Object.fromEntries(folders.map((folder) => [
|
|
971
|
+
folder,
|
|
972
|
+
{
|
|
973
|
+
enabled: isFolderEnabled(folder),
|
|
974
|
+
effectiveEnabled: isFolderPathEnabled(folder)
|
|
975
|
+
}
|
|
976
|
+
]));
|
|
922
977
|
res.json({
|
|
923
978
|
charts: chartsWithState,
|
|
924
979
|
folders: folders,
|
|
980
|
+
folderStates,
|
|
925
981
|
basePath: chartPath
|
|
926
982
|
});
|
|
927
983
|
}
|
|
@@ -1051,6 +1107,7 @@ const pluginConstructor = (app) => {
|
|
|
1051
1107
|
return;
|
|
1052
1108
|
}
|
|
1053
1109
|
await fs.promises.rmdir(fullPath);
|
|
1110
|
+
removeFolderState(folderPathParam);
|
|
1054
1111
|
res.status(200).json({ success: true, message: 'Folder deleted successfully' });
|
|
1055
1112
|
}
|
|
1056
1113
|
catch (error) {
|
|
@@ -1058,6 +1115,64 @@ const pluginConstructor = (app) => {
|
|
|
1058
1115
|
res.status(500).send('Error deleting folder');
|
|
1059
1116
|
}
|
|
1060
1117
|
});
|
|
1118
|
+
router.post('/folders/toggle', async (req, res) => {
|
|
1119
|
+
const body = parseBody(FolderToggleBody, req, res);
|
|
1120
|
+
if (!body) {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const { folderPath, enabled } = body;
|
|
1124
|
+
try {
|
|
1125
|
+
const basePath = props.chartPath || defaultChartsPath;
|
|
1126
|
+
const fullPath = path.join(basePath, folderPath);
|
|
1127
|
+
if (!isWithinBase(fullPath, basePath)) {
|
|
1128
|
+
res.status(403).send('Access denied: Invalid path');
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (path.normalize(fullPath) === path.normalize(basePath)) {
|
|
1132
|
+
res.status(400).send('Cannot toggle the root folder');
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (!fs.existsSync(fullPath)) {
|
|
1136
|
+
res.status(404).send('Folder not found');
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (!fs.statSync(fullPath).isDirectory()) {
|
|
1140
|
+
res.status(400).send('Not a folder');
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
// Diff providers before/after the refresh: a folder toggle can affect
|
|
1144
|
+
// many charts (including nested folders), while charts that are
|
|
1145
|
+
// individually disabled stay absent on both sides and emit no delta.
|
|
1146
|
+
const before = chartProviders;
|
|
1147
|
+
setFolderEnabled(folderPath, enabled);
|
|
1148
|
+
await refreshChartProviders();
|
|
1149
|
+
let chartsAffected = 0;
|
|
1150
|
+
for (const id of Object.keys(before)) {
|
|
1151
|
+
if (!chartProviders[id]) {
|
|
1152
|
+
emitChartDelta(id, null);
|
|
1153
|
+
chartsAffected++;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
for (const id of Object.keys(chartProviders)) {
|
|
1157
|
+
if (!before[id]) {
|
|
1158
|
+
emitChartDelta(id, sanitizeProvider(chartProviders[id], 2));
|
|
1159
|
+
chartsAffected++;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
app.debug(`Folder ${folderPath} set to ${enabled ? 'enabled' : 'disabled'} (${chartsAffected} chart delta(s))`);
|
|
1163
|
+
res.status(200).json({
|
|
1164
|
+
success: true,
|
|
1165
|
+
message: `Folder ${enabled ? 'enabled' : 'disabled'}`,
|
|
1166
|
+
folderPath,
|
|
1167
|
+
enabled,
|
|
1168
|
+
chartsAffected
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
catch (error) {
|
|
1172
|
+
console.error('Error toggling folder state:', error);
|
|
1173
|
+
res.status(500).send('Error toggling folder state');
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1061
1176
|
router.post('/charts/:chartPath/toggle', async (req, res) => {
|
|
1062
1177
|
const chartPathParam = decodeURIComponent(req.params.chartPath);
|
|
1063
1178
|
const body = parseBody(ChartToggleBody, req, res);
|
|
@@ -1141,6 +1256,12 @@ const pluginConstructor = (app) => {
|
|
|
1141
1256
|
const chartData = sanitizeProvider(chartProviders[chartId], 2);
|
|
1142
1257
|
emitChartDelta(chartId, chartData);
|
|
1143
1258
|
}
|
|
1259
|
+
else {
|
|
1260
|
+
// The move can hide the chart from serving (target folder disabled)
|
|
1261
|
+
// — retract it so v2 clients don't keep a stale entry whose tiles
|
|
1262
|
+
// now 404.
|
|
1263
|
+
emitChartDelta(chartId, null);
|
|
1264
|
+
}
|
|
1144
1265
|
res.status(200).json({ success: true, message: 'Chart moved successfully' });
|
|
1145
1266
|
}
|
|
1146
1267
|
catch (error) {
|
|
@@ -3300,10 +3421,11 @@ const pluginConstructor = (app) => {
|
|
|
3300
3421
|
try {
|
|
3301
3422
|
const chartPath = props.chartPath || defaultChartsPath;
|
|
3302
3423
|
const charts = await findCharts(chartPath);
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3424
|
+
// Close the handles of the map being replaced — findCharts reopened
|
|
3425
|
+
// every chart, so the old provider objects would otherwise leak.
|
|
3426
|
+
const previous = chartProviders;
|
|
3427
|
+
chartProviders = partitionVisibleCharts(chartPath, charts);
|
|
3428
|
+
closeProviderHandles(Object.values(previous));
|
|
3307
3429
|
app.debug(`Chart providers refreshed: ${Object.keys(chartProviders).length} enabled charts`);
|
|
3308
3430
|
}
|
|
3309
3431
|
catch (error) {
|