signalk-charts-provider-simple 1.2.0 → 1.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/CHANGELOG.md +38 -8
- package/README.md +16 -2
- package/package.json +2 -1
- package/public/css/manage-charts.css +88 -0
- package/public/js/emoji-utils.js +5 -0
- package/public/js/manage-charts-enhanced.js +235 -0
- package/src/index.js +161 -0
- package/src/utils/file-scanner.js +39 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [1.
|
|
3
|
+
## [1.4.0] 2025-11-30
|
|
4
|
+
### New:
|
|
5
|
+
- Chart Metadata Editor - Edit chart names directly in MBTiles files
|
|
6
|
+
- "Info" button renamed to "Meta" for clarity
|
|
7
|
+
- Click "Edit" in metadata modal to modify chart name
|
|
8
|
+
- Legal warning displayed before editing
|
|
9
|
+
- Auto-updates description to "USER MODIFIED - DO NOT DISTRIBUTE - PERSONAL USE ONLY"
|
|
10
|
+
- Personal use only - helps identify charts for navigation vs distribution
|
|
11
|
+
- Real-time delta updates to Freeboard SK after metadata changes
|
|
12
|
+
- Chart Name Display - MBTiles chart names now shown in detail view
|
|
13
|
+
- Automatically reads chart name from MBTiles metadata
|
|
14
|
+
- Displayed in accent color between Size and Folder
|
|
15
|
+
- Fast single-query per chart (no performance impact)
|
|
16
|
+
- Updates automatically when chart name is edited
|
|
17
|
+
|
|
18
|
+
### Important:
|
|
19
|
+
- This feature is for personal use only
|
|
20
|
+
- Modified charts should not be distributed
|
|
21
|
+
- Signal K community is not responsible for any illegal use of this feature
|
|
22
|
+
|
|
23
|
+
## [1.3.0] 2025-11-30
|
|
24
|
+
### New:
|
|
25
|
+
- Chart Info button - View detailed MBTiles metadata directly in the Chart Manager
|
|
26
|
+
- Displays chart name, description, version, type, format
|
|
27
|
+
- Shows geographic bounds (SW/NE coordinates), zoom levels, center point
|
|
28
|
+
- Tile count with formatting
|
|
29
|
+
- Attribution, credits, tags, and legend information
|
|
30
|
+
- Beautiful modal dialog matching the existing Material Design 3 theme
|
|
31
|
+
- Works for both grid and list view
|
|
32
|
+
|
|
33
|
+
## [1.2.0] 2025-11-28
|
|
4
34
|
### New:
|
|
5
35
|
- Real-time delta notifications to all chart operations in the plugin, following the exact pattern used by SignalK Server's built-in resource provider, so server reloads not needed anymore.
|
|
6
36
|
|
|
@@ -8,15 +38,15 @@
|
|
|
8
38
|
- "RFC-Ressource-Change" changed to [RFC: Resource Provider Delta Notification Best Practice](https://github.com/dirkwa/signalk-charts-provider-simple/blob/main/docs/RFC-Resource-Change-Notifications.md) - thanks Adrian!
|
|
9
39
|
- Unnessecary server reload notifications removed
|
|
10
40
|
|
|
11
|
-
## [1.1.1] 2025-
|
|
41
|
+
## [1.1.1] 2025-11-28
|
|
12
42
|
### New:
|
|
13
43
|
- [RFC-Ressource-Change](https://github.com/dirkwa/signalk-charts-provider-simple/blob/main/docs/RFC-Resource-Change-Notifications.md) added to discuss with the community if we can make chart changes instant.
|
|
14
44
|
|
|
15
|
-
## [1.1.0] 2025-
|
|
45
|
+
## [1.1.0] 2025-11-28
|
|
16
46
|
### New:
|
|
17
47
|
- Press and hold, than drag and drop charts to folder on touch and mobile devices - tested win11 & iOS 26
|
|
18
48
|
|
|
19
|
-
## [1.0.8] 2025-
|
|
49
|
+
## [1.0.8] 2025-11-27
|
|
20
50
|
### Fixes:
|
|
21
51
|
- A newly generated folder in Chart Manager was not available in the Download area
|
|
22
52
|
|
|
@@ -24,22 +54,22 @@
|
|
|
24
54
|
- No Scrolling for Chart Manager top buttons, breaks the design
|
|
25
55
|
- CSS optimized between mobile and desktop
|
|
26
56
|
|
|
27
|
-
## [1.0.7] 2025-
|
|
57
|
+
## [1.0.7] 2025-11-27
|
|
28
58
|
### Changes:
|
|
29
59
|
- to avoid conflicts use defaultChartsPath as charts-simple
|
|
30
60
|
|
|
31
|
-
## [1.0.6] 2025-
|
|
61
|
+
## [1.0.6] 2025-11-27
|
|
32
62
|
### Changes:
|
|
33
63
|
- Plugin description
|
|
34
64
|
|
|
35
|
-
## [1.0.5] 2025-
|
|
65
|
+
## [1.0.5] 2025-11-27
|
|
36
66
|
### Fixes:
|
|
37
67
|
- Github publishing - remove npm test
|
|
38
68
|
- Github publisihing - Firx workflow
|
|
39
69
|
### Changes:
|
|
40
70
|
- Typo in README.md
|
|
41
71
|
|
|
42
|
-
## [1.0.0] 2025-
|
|
72
|
+
## [1.0.0] 2025-11-26
|
|
43
73
|
- Initial release
|
|
44
74
|
- Material Design 3 UI
|
|
45
75
|
- Optimized for touch and mobile
|
package/README.md
CHANGED
|
@@ -47,12 +47,14 @@ http://[your-server]:3000/plugins/signalk-charts-provider-simple/
|
|
|
47
47
|
The interface provides two main tabs:
|
|
48
48
|
|
|
49
49
|
1. **Manage Charts**:
|
|
50
|
-
- View all charts in your library
|
|
50
|
+
- View all charts in your library with chart names displayed from MBTiles metadata
|
|
51
|
+
- View chart metadata (name, description, bounds, zoom levels, tile count, etc.)
|
|
52
|
+
- Edit chart names directly in MBTiles files (personal use only)
|
|
51
53
|
- Enable/disable charts
|
|
52
54
|
- Organize charts into folders
|
|
53
55
|
- Upload new chart files
|
|
54
56
|
- Delete unwanted charts
|
|
55
|
-
- Rename
|
|
57
|
+
- Rename chart files
|
|
56
58
|
|
|
57
59
|
2. **Download from URL**:
|
|
58
60
|
- Download charts directly from any URL
|
|
@@ -73,6 +75,18 @@ Charts provided by this plugin work with:
|
|
|
73
75
|
- [Freeboard SK](https://www.npmjs.com/package/@signalk/freeboard-sk)
|
|
74
76
|
- [OpenCPN](https://opencpn.org/)
|
|
75
77
|
|
|
78
|
+
## Legal Notice
|
|
79
|
+
|
|
80
|
+
### Chart Metadata Editing
|
|
81
|
+
|
|
82
|
+
This plugin includes a feature to edit chart metadata (chart names) in MBTiles files. **This feature is intended for personal use only.**
|
|
83
|
+
|
|
84
|
+
**Important:**
|
|
85
|
+
- The Signal K community is **not responsible** for any illegal use of this feature
|
|
86
|
+
- Modified charts are automatically marked with "USER MODIFIED - DO NOT DISTRIBUTE - PERSONAL USE ONLY" in the description field
|
|
87
|
+
- **Do not distribute** modified charts - this may violate copyright laws
|
|
88
|
+
- Use this feature responsibly and only for organizing your personal chart library
|
|
89
|
+
|
|
76
90
|
## Acknowledgments
|
|
77
91
|
|
|
78
92
|
Inspired by [Signal K Charts Plugin](https://github.com/SignalK/charts-plugin) by Mikko Vesikkala.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-charts-provider-simple",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Simple Signal K chart provider for local MBTiles with web and download management",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"bluebird": "^3.5.1",
|
|
23
23
|
"busboy": "^1.6.0",
|
|
24
24
|
"lodash": "^4.17.11",
|
|
25
|
+
"sqlite3": "^5.1.7",
|
|
25
26
|
"unzipper": "^0.12.3",
|
|
26
27
|
"xml2js": "^0.6.2"
|
|
27
28
|
},
|
|
@@ -1432,3 +1432,91 @@
|
|
|
1432
1432
|
color: var(--accent-primary);
|
|
1433
1433
|
font-weight: 500;
|
|
1434
1434
|
}
|
|
1435
|
+
|
|
1436
|
+
/* Info Button */
|
|
1437
|
+
.btn-info {
|
|
1438
|
+
background: rgba(0, 212, 255, 0.1);
|
|
1439
|
+
color: var(--accent-primary);
|
|
1440
|
+
border: 1px solid var(--accent-primary);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
.btn-info::before {
|
|
1444
|
+
background: var(--accent-primary);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
.btn-info:hover:not(:disabled)::before {
|
|
1448
|
+
opacity: 0.08;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
.btn-info:active:not(:disabled)::before {
|
|
1452
|
+
opacity: 0.12;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
.btn-info svg {
|
|
1456
|
+
fill: var(--accent-primary);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/* Chart Info Modal */
|
|
1460
|
+
.chart-info-modal {
|
|
1461
|
+
max-width: 600px;
|
|
1462
|
+
max-height: 80vh;
|
|
1463
|
+
overflow: hidden;
|
|
1464
|
+
display: flex;
|
|
1465
|
+
flex-direction: column;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
.chart-info-modal .delete-modal-body {
|
|
1469
|
+
overflow-y: auto;
|
|
1470
|
+
max-height: calc(80vh - 200px);
|
|
1471
|
+
margin-bottom: 0;
|
|
1472
|
+
padding-bottom: 16px;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
.chart-info-container {
|
|
1476
|
+
display: flex;
|
|
1477
|
+
flex-direction: column;
|
|
1478
|
+
gap: 12px;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
.chart-info-row {
|
|
1482
|
+
display: grid;
|
|
1483
|
+
grid-template-columns: 140px 1fr;
|
|
1484
|
+
gap: 16px;
|
|
1485
|
+
padding: 12px;
|
|
1486
|
+
background: var(--bg-secondary);
|
|
1487
|
+
border-radius: 8px;
|
|
1488
|
+
align-items: start;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
.chart-info-label {
|
|
1492
|
+
font-weight: 600;
|
|
1493
|
+
color: var(--text-secondary);
|
|
1494
|
+
font-size: 0.9rem;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
.chart-info-value {
|
|
1498
|
+
color: var(--text-primary);
|
|
1499
|
+
font-size: 0.95rem;
|
|
1500
|
+
word-break: break-word;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/* Responsive adjustments for mobile */
|
|
1504
|
+
@media (max-width: 768px) {
|
|
1505
|
+
.chart-info-modal {
|
|
1506
|
+
max-width: 95%;
|
|
1507
|
+
max-height: 90vh;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
.chart-info-row {
|
|
1511
|
+
grid-template-columns: 1fr;
|
|
1512
|
+
gap: 4px;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
.chart-info-label {
|
|
1516
|
+
font-size: 0.85rem;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
.chart-info-value {
|
|
1520
|
+
font-size: 0.9rem;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
package/public/js/emoji-utils.js
CHANGED
|
@@ -96,6 +96,11 @@ const icons = {
|
|
|
96
96
|
emoji: '🕐',
|
|
97
97
|
html: '🕐',
|
|
98
98
|
svg: null
|
|
99
|
+
},
|
|
100
|
+
info: {
|
|
101
|
+
emoji: 'ℹ️',
|
|
102
|
+
html: 'ℹ',
|
|
103
|
+
svg: '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>'
|
|
99
104
|
}
|
|
100
105
|
};
|
|
101
106
|
|
|
@@ -210,6 +210,12 @@ function renderChartCard(chart) {
|
|
|
210
210
|
<span class="meta-label">${window.getIcon('size')} Size:</span>
|
|
211
211
|
<span class="meta-value">${displaySize}</span>
|
|
212
212
|
</div>
|
|
213
|
+
${chart.chartName ? `
|
|
214
|
+
<div class="chart-meta-row">
|
|
215
|
+
<span class="meta-label">📊 Chart:</span>
|
|
216
|
+
<span class="meta-value" style="font-weight: 500; color: var(--accent-primary);">${chart.chartName}</span>
|
|
217
|
+
</div>
|
|
218
|
+
` : ''}
|
|
213
219
|
<div class="chart-meta-row">
|
|
214
220
|
<span class="meta-label">${window.getIcon('folder')} Folder:</span>
|
|
215
221
|
<span class="meta-value">${folderDisplay}</span>
|
|
@@ -224,6 +230,9 @@ function renderChartCard(chart) {
|
|
|
224
230
|
</div>
|
|
225
231
|
</div>
|
|
226
232
|
<div class="chart-card-footer">
|
|
233
|
+
<button class="btn btn-sm btn-info" onclick="showChartInfo('${chart.relativePath}')" title="View chart metadata">
|
|
234
|
+
Meta
|
|
235
|
+
</button>
|
|
227
236
|
<button class="btn btn-sm btn-secondary" onclick="showRenameDialog('${chart.relativePath}', '${chart.name}', '${chart.folder}')" title="Rename chart">
|
|
228
237
|
Rename
|
|
229
238
|
</button>
|
|
@@ -252,6 +261,9 @@ function renderChartCard(chart) {
|
|
|
252
261
|
</div>
|
|
253
262
|
</div>
|
|
254
263
|
<div class="chart-list-actions">
|
|
264
|
+
<button class="btn btn-sm btn-info" onclick="showChartInfo('${chart.relativePath}')" title="View chart metadata">
|
|
265
|
+
Meta
|
|
266
|
+
</button>
|
|
255
267
|
<button class="btn btn-sm btn-secondary" onclick="showRenameDialog('${chart.relativePath}', '${chart.name}', '${chart.folder}')" title="Rename chart">
|
|
256
268
|
Rename
|
|
257
269
|
</button>
|
|
@@ -1288,3 +1300,226 @@ function showUploadNotification(fileCount) {
|
|
|
1288
1300
|
}
|
|
1289
1301
|
}, 5000);
|
|
1290
1302
|
}
|
|
1303
|
+
|
|
1304
|
+
// Chart info modal
|
|
1305
|
+
let currentChartPath = null;
|
|
1306
|
+
let currentMetadata = null;
|
|
1307
|
+
let isEditMode = false;
|
|
1308
|
+
|
|
1309
|
+
window.showChartInfo = async function(chartPath) {
|
|
1310
|
+
try {
|
|
1311
|
+
currentChartPath = chartPath;
|
|
1312
|
+
isEditMode = false;
|
|
1313
|
+
|
|
1314
|
+
const response = await fetch(`/signalk/chart-tiles/chart-metadata/${encodeURIComponent(chartPath)}`);
|
|
1315
|
+
|
|
1316
|
+
if (!response.ok) {
|
|
1317
|
+
const errorText = await response.text();
|
|
1318
|
+
showErrorNotification(`Failed to load chart information: ${errorText}`);
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const metadata = await response.json();
|
|
1323
|
+
currentMetadata = metadata;
|
|
1324
|
+
|
|
1325
|
+
renderMetadataModal(metadata);
|
|
1326
|
+
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
console.error('Error fetching chart info:', error);
|
|
1329
|
+
showErrorNotification('Error loading chart information: ' + error.message);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function renderMetadataModal(metadata) {
|
|
1334
|
+
const formatValue = (key, value, isEditable = false) => {
|
|
1335
|
+
if (value === null || value === undefined || value === '') {
|
|
1336
|
+
return '<span style="color: var(--text-secondary); font-style: italic;">Not specified</span>';
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// In edit mode, make Chart Name editable
|
|
1340
|
+
if (isEditMode && isEditable && key === 'name') {
|
|
1341
|
+
return `<input type="text" id="editChartName" class="text-input" value="${value}" style="width: 100%; padding: 8px;" />`;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
switch(key) {
|
|
1345
|
+
case 'bounds':
|
|
1346
|
+
try {
|
|
1347
|
+
const bounds = typeof value === 'string' ? value.split(',').map(v => parseFloat(v.trim())) : value;
|
|
1348
|
+
return `
|
|
1349
|
+
<div style="font-family: monospace; font-size: 0.9em;">
|
|
1350
|
+
SW: ${bounds[0].toFixed(4)}°, ${bounds[1].toFixed(4)}°<br>
|
|
1351
|
+
NE: ${bounds[2].toFixed(4)}°, ${bounds[3].toFixed(4)}°
|
|
1352
|
+
</div>
|
|
1353
|
+
`;
|
|
1354
|
+
} catch {
|
|
1355
|
+
return value;
|
|
1356
|
+
}
|
|
1357
|
+
case 'tileCount':
|
|
1358
|
+
return parseInt(value).toLocaleString();
|
|
1359
|
+
case 'minzoom':
|
|
1360
|
+
case 'maxzoom':
|
|
1361
|
+
return `Level ${value}`;
|
|
1362
|
+
case 'legend':
|
|
1363
|
+
return '<span style="color: var(--text-secondary); font-style: italic;">Available (not displayed)</span>';
|
|
1364
|
+
default:
|
|
1365
|
+
return value;
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
const metadataRows = [
|
|
1370
|
+
{ label: 'Chart Name', key: 'name', editable: true },
|
|
1371
|
+
{ label: 'Description', key: 'description' },
|
|
1372
|
+
{ label: 'Version', key: 'version' },
|
|
1373
|
+
{ label: 'Type', key: 'type' },
|
|
1374
|
+
{ label: 'Format', key: 'format' },
|
|
1375
|
+
{ label: 'Bounds', key: 'bounds' },
|
|
1376
|
+
{ label: 'Min Zoom', key: 'minzoom' },
|
|
1377
|
+
{ label: 'Max Zoom', key: 'maxzoom' },
|
|
1378
|
+
{ label: 'Center', key: 'center' },
|
|
1379
|
+
{ label: 'Tile Count', key: 'tileCount' },
|
|
1380
|
+
{ label: 'Attribution', key: 'attribution' },
|
|
1381
|
+
{ label: 'Credits', key: 'credits' },
|
|
1382
|
+
{ label: 'Tags', key: 'tags' },
|
|
1383
|
+
{ label: 'Legend', key: 'legend' }
|
|
1384
|
+
];
|
|
1385
|
+
|
|
1386
|
+
const metadataHTML = metadataRows
|
|
1387
|
+
.filter(row => metadata[row.key] !== undefined)
|
|
1388
|
+
.map(row => `
|
|
1389
|
+
<div class="chart-info-row">
|
|
1390
|
+
<div class="chart-info-label">${row.label}:</div>
|
|
1391
|
+
<div class="chart-info-value">${formatValue(row.key, metadata[row.key], row.editable)}</div>
|
|
1392
|
+
</div>
|
|
1393
|
+
`).join('');
|
|
1394
|
+
|
|
1395
|
+
const infoIcon = window.getIcon('info', true);
|
|
1396
|
+
|
|
1397
|
+
// Warning message for edit mode
|
|
1398
|
+
const warningHTML = isEditMode ? `
|
|
1399
|
+
<div class="delete-modal-warning" style="margin-bottom: 16px;">
|
|
1400
|
+
${window.getIcon('warning')} <strong>Legal Notice:</strong> You are about to modify chart metadata. The Signal K community is not responsible for any illegal use of this feature. Charts must only be modified for personal use. Distribution of modified charts may violate copyright laws.
|
|
1401
|
+
</div>
|
|
1402
|
+
` : '';
|
|
1403
|
+
|
|
1404
|
+
const modalHTML = `
|
|
1405
|
+
<div class="delete-modal-overlay" id="chartInfoModal" onclick="closeChartInfoModal(event)">
|
|
1406
|
+
<div class="delete-modal chart-info-modal" onclick="event.stopPropagation()">
|
|
1407
|
+
<div class="delete-modal-header">
|
|
1408
|
+
<div class="delete-modal-icon" style="color: var(--accent-primary);">${infoIcon}</div>
|
|
1409
|
+
<h3>Chart Metadata ${isEditMode ? '(Edit Mode)' : ''}</h3>
|
|
1410
|
+
</div>
|
|
1411
|
+
<div class="delete-modal-body">
|
|
1412
|
+
${warningHTML}
|
|
1413
|
+
<div class="chart-info-container">
|
|
1414
|
+
${metadataHTML}
|
|
1415
|
+
</div>
|
|
1416
|
+
</div>
|
|
1417
|
+
<div class="delete-modal-actions">
|
|
1418
|
+
${isEditMode ?
|
|
1419
|
+
'<button class="btn btn-secondary" onclick="cancelEditMetadata()">Cancel</button><button class="btn btn-primary" onclick="saveChartMetadata()">Save</button>' :
|
|
1420
|
+
'<button class="btn btn-secondary" onclick="editChartMetadata()">Edit</button><button class="btn btn-primary" onclick="closeChartInfoModal()">Close</button>'
|
|
1421
|
+
}
|
|
1422
|
+
</div>
|
|
1423
|
+
</div>
|
|
1424
|
+
</div>
|
|
1425
|
+
`;
|
|
1426
|
+
|
|
1427
|
+
// Remove existing modal if present
|
|
1428
|
+
const existingModal = document.getElementById('chartInfoModal');
|
|
1429
|
+
if (existingModal) {
|
|
1430
|
+
existingModal.remove();
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
window.editChartMetadata = function() {
|
|
1437
|
+
isEditMode = true;
|
|
1438
|
+
renderMetadataModal(currentMetadata);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
window.cancelEditMetadata = function() {
|
|
1442
|
+
isEditMode = false;
|
|
1443
|
+
renderMetadataModal(currentMetadata);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
window.saveChartMetadata = async function() {
|
|
1447
|
+
const newChartName = document.getElementById('editChartName')?.value.trim();
|
|
1448
|
+
|
|
1449
|
+
if (!newChartName) {
|
|
1450
|
+
showErrorNotification('Chart name cannot be empty');
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
try {
|
|
1455
|
+
const response = await fetch(`/signalk/chart-tiles/chart-metadata/${encodeURIComponent(currentChartPath)}`, {
|
|
1456
|
+
method: 'PUT',
|
|
1457
|
+
headers: {
|
|
1458
|
+
'Content-Type': 'application/json'
|
|
1459
|
+
},
|
|
1460
|
+
body: JSON.stringify({
|
|
1461
|
+
name: newChartName
|
|
1462
|
+
})
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
if (!response.ok) {
|
|
1466
|
+
const errorText = await response.text();
|
|
1467
|
+
showErrorNotification(`Failed to save metadata: ${errorText}`);
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// Update current metadata
|
|
1472
|
+
currentMetadata.name = newChartName;
|
|
1473
|
+
currentMetadata.description = 'USER MODIFIED - DO NOT DISTRIBUTE - PERSONAL USE ONLY';
|
|
1474
|
+
|
|
1475
|
+
// Close edit mode and show success
|
|
1476
|
+
isEditMode = false;
|
|
1477
|
+
renderMetadataModal(currentMetadata);
|
|
1478
|
+
|
|
1479
|
+
showSuccessNotification('Chart metadata updated successfully');
|
|
1480
|
+
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
console.error('Error saving chart metadata:', error);
|
|
1483
|
+
showErrorNotification('Error saving chart metadata: ' + error.message);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function showSuccessNotification(message) {
|
|
1488
|
+
const notificationHTML = `
|
|
1489
|
+
<div class="notification-toast" id="successNotification">
|
|
1490
|
+
<div class="notification-content">
|
|
1491
|
+
<div class="notification-icon success">
|
|
1492
|
+
${window.getIcon('checkmark')}
|
|
1493
|
+
</div>
|
|
1494
|
+
<div class="notification-text">
|
|
1495
|
+
<div class="notification-title">Success</div>
|
|
1496
|
+
<div class="notification-message">${message}</div>
|
|
1497
|
+
</div>
|
|
1498
|
+
</div>
|
|
1499
|
+
</div>
|
|
1500
|
+
`;
|
|
1501
|
+
|
|
1502
|
+
const existing = document.getElementById('successNotification');
|
|
1503
|
+
if (existing) {
|
|
1504
|
+
existing.remove();
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
document.body.insertAdjacentHTML('beforeend', notificationHTML);
|
|
1508
|
+
|
|
1509
|
+
setTimeout(() => {
|
|
1510
|
+
const notification = document.getElementById('successNotification');
|
|
1511
|
+
if (notification) {
|
|
1512
|
+
notification.classList.add('fade-out');
|
|
1513
|
+
setTimeout(() => notification.remove(), 300);
|
|
1514
|
+
}
|
|
1515
|
+
}, 5000);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
window.closeChartInfoModal = function(event) {
|
|
1519
|
+
if (event && event.target.id !== 'chartInfoModal') return;
|
|
1520
|
+
|
|
1521
|
+
const modal = document.getElementById('chartInfoModal');
|
|
1522
|
+
if (modal) {
|
|
1523
|
+
modal.remove();
|
|
1524
|
+
}
|
|
1525
|
+
}
|
package/src/index.js
CHANGED
|
@@ -648,6 +648,167 @@ module.exports = (app) => {
|
|
|
648
648
|
}
|
|
649
649
|
});
|
|
650
650
|
|
|
651
|
+
// Update chart metadata (MBTiles only)
|
|
652
|
+
app.put(`${chartTilesPath}/chart-metadata/:chartPath`, async (req, res) => {
|
|
653
|
+
const chartPathParam = decodeURIComponent(req.params.chartPath);
|
|
654
|
+
const { name } = req.body;
|
|
655
|
+
|
|
656
|
+
if (!name || typeof name !== 'string') {
|
|
657
|
+
return res.status(400).send('Chart name is required');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
try {
|
|
661
|
+
const basePath = props.chartPath || defaultChartsPath;
|
|
662
|
+
const fullPath = path.join(basePath, chartPathParam);
|
|
663
|
+
|
|
664
|
+
// Security check
|
|
665
|
+
const normalizedFullPath = path.normalize(fullPath);
|
|
666
|
+
const normalizedBasePath = path.normalize(basePath);
|
|
667
|
+
if (!normalizedFullPath.startsWith(normalizedBasePath)) {
|
|
668
|
+
return res.status(403).send('Access denied: Invalid path');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Check if file exists
|
|
672
|
+
if (!fs.existsSync(fullPath)) {
|
|
673
|
+
return res.status(404).send('Chart not found');
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Only support .mbtiles files
|
|
677
|
+
if (!fullPath.endsWith('.mbtiles')) {
|
|
678
|
+
return res.status(400).send('Metadata editing only available for MBTiles charts');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Update metadata in SQLite database
|
|
682
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
683
|
+
const db = new sqlite3.Database(fullPath, (err) => {
|
|
684
|
+
if (err) {
|
|
685
|
+
console.error('Error opening database:', err);
|
|
686
|
+
return res.status(500).send('Error opening chart database');
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// Update name and description
|
|
691
|
+
const description = 'USER MODIFIED - DO NOT DISTRIBUTE - PERSONAL USE ONLY';
|
|
692
|
+
|
|
693
|
+
db.serialize(() => {
|
|
694
|
+
db.run('UPDATE metadata SET value = ? WHERE name = ?', [name, 'name'], (err) => {
|
|
695
|
+
if (err) {
|
|
696
|
+
db.close();
|
|
697
|
+
console.error('Error updating chart name:', err);
|
|
698
|
+
return res.status(500).send('Error updating chart name');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Update description with warning
|
|
702
|
+
db.run('UPDATE metadata SET value = ? WHERE name = ?', [description, 'description'], async (err) => {
|
|
703
|
+
db.close();
|
|
704
|
+
|
|
705
|
+
if (err) {
|
|
706
|
+
console.error('Error updating description:', err);
|
|
707
|
+
return res.status(500).send('Error updating description');
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
app.debug(`Chart metadata updated: ${chartPathParam} - New name: ${name}`);
|
|
711
|
+
|
|
712
|
+
// Reload chart providers and emit delta notification
|
|
713
|
+
try {
|
|
714
|
+
await refreshChartProviders();
|
|
715
|
+
|
|
716
|
+
const chartId = path.basename(chartPathParam).replace(/\.mbtiles$/, '');
|
|
717
|
+
if (chartProviders[chartId]) {
|
|
718
|
+
const chartData = sanitizeProvider(chartProviders[chartId], serverMajorVersion);
|
|
719
|
+
emitChartDelta(chartId, chartData);
|
|
720
|
+
app.debug(`Delta emitted for metadata update: ${chartId}`);
|
|
721
|
+
}
|
|
722
|
+
} catch (refreshErr) {
|
|
723
|
+
app.error(`Error refreshing charts after metadata update: ${refreshErr.message}`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
res.json({ success: true, message: 'Chart metadata updated successfully' });
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
} catch (error) {
|
|
732
|
+
console.error('Error updating chart metadata:', error);
|
|
733
|
+
res.status(500).send('Error updating chart metadata');
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// Get chart metadata (MBTiles only)
|
|
738
|
+
app.get(`${chartTilesPath}/chart-metadata/:chartPath`, async (req, res) => {
|
|
739
|
+
const chartPathParam = decodeURIComponent(req.params.chartPath);
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
const basePath = props.chartPath || defaultChartsPath;
|
|
743
|
+
const fullPath = path.join(basePath, chartPathParam);
|
|
744
|
+
|
|
745
|
+
// Security check: ensure the path is within the base chart directory
|
|
746
|
+
const normalizedFullPath = path.normalize(fullPath);
|
|
747
|
+
const normalizedBasePath = path.normalize(basePath);
|
|
748
|
+
if (!normalizedFullPath.startsWith(normalizedBasePath)) {
|
|
749
|
+
return res.status(403).send('Access denied: Invalid path');
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Check if file exists
|
|
753
|
+
if (!fs.existsSync(fullPath)) {
|
|
754
|
+
return res.status(404).send('Chart not found');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Only support .mbtiles files
|
|
758
|
+
if (!fullPath.endsWith('.mbtiles')) {
|
|
759
|
+
return res.status(400).send('Metadata only available for MBTiles charts');
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Read metadata from SQLite database
|
|
763
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
764
|
+
const db = new sqlite3.Database(fullPath, sqlite3.OPEN_READONLY, (err) => {
|
|
765
|
+
if (err) {
|
|
766
|
+
console.error('Error opening database:', err);
|
|
767
|
+
return res.status(500).send('Error reading chart metadata');
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
db.all('SELECT name, value FROM metadata', [], (err, rows) => {
|
|
772
|
+
if (err) {
|
|
773
|
+
db.close();
|
|
774
|
+
console.error('Error querying metadata:', err);
|
|
775
|
+
return res.status(500).send('Error reading chart metadata');
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Convert rows to object
|
|
779
|
+
const metadata = {};
|
|
780
|
+
rows.forEach(row => {
|
|
781
|
+
metadata[row.name] = row.value;
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
// Try to get tile count - different MBTiles files use different table names
|
|
785
|
+
// Try 'map' first (standard), then 'tiles' (alternative)
|
|
786
|
+
db.get('SELECT COUNT(*) as count FROM map', [], (err, countRow) => {
|
|
787
|
+
if (err) {
|
|
788
|
+
// Try alternative table name
|
|
789
|
+
db.get('SELECT COUNT(*) as count FROM tiles', [], (err2, countRow2) => {
|
|
790
|
+
db.close();
|
|
791
|
+
|
|
792
|
+
if (!err2 && countRow2) {
|
|
793
|
+
metadata.tileCount = countRow2.count;
|
|
794
|
+
}
|
|
795
|
+
// If both fail, silently omit tile count - some MBTiles formats don't have this
|
|
796
|
+
return res.json(metadata);
|
|
797
|
+
});
|
|
798
|
+
} else {
|
|
799
|
+
db.close();
|
|
800
|
+
metadata.tileCount = countRow.count;
|
|
801
|
+
res.json(metadata);
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
} catch (error) {
|
|
807
|
+
console.error('Error fetching chart metadata:', error);
|
|
808
|
+
res.status(500).send('Error fetching chart metadata');
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
651
812
|
// Upload chart file
|
|
652
813
|
app.post(`${chartTilesPath}/upload`, async (req, res) => {
|
|
653
814
|
try {
|
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Get chart name from MBTiles metadata
|
|
6
|
+
* @param {string} filePath - Path to .mbtiles file
|
|
7
|
+
* @returns {Promise<string|null>} Chart name or null
|
|
8
|
+
*/
|
|
9
|
+
async function getChartName(filePath) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
try {
|
|
12
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
13
|
+
const db = new sqlite3.Database(filePath, sqlite3.OPEN_READONLY, (err) => {
|
|
14
|
+
if (err) {
|
|
15
|
+
resolve(null);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
db.get("SELECT value FROM metadata WHERE name = 'name'", [], (err, row) => {
|
|
21
|
+
db.close();
|
|
22
|
+
if (err || !row) {
|
|
23
|
+
resolve(null);
|
|
24
|
+
} else {
|
|
25
|
+
resolve(row.value);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
} catch (err) {
|
|
29
|
+
resolve(null);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
4
34
|
/**
|
|
5
35
|
* Recursively scan a directory for .mbtiles files
|
|
6
36
|
* @param {string} basePath - The base chart directory
|
|
@@ -32,8 +62,17 @@ async function scanChartsRecursively(basePath, currentPath = basePath) {
|
|
|
32
62
|
const relativePath = path.relative(basePath, fullPath);
|
|
33
63
|
const folder = path.dirname(relativePath) || '/';
|
|
34
64
|
|
|
65
|
+
// Try to read chart name from MBTiles metadata (fast query)
|
|
66
|
+
let chartName = null;
|
|
67
|
+
try {
|
|
68
|
+
chartName = await getChartName(fullPath);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// Silently fail - chart name is optional
|
|
71
|
+
}
|
|
72
|
+
|
|
35
73
|
charts.push({
|
|
36
74
|
name: entry.name,
|
|
75
|
+
chartName: chartName, // Chart name from metadata
|
|
37
76
|
size: stats.size,
|
|
38
77
|
path: fullPath,
|
|
39
78
|
relativePath: relativePath,
|