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 CHANGED
@@ -1,6 +1,36 @@
1
1
  # Changelog
2
2
 
3
- ## [1.2.0] 2025-01-28
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-01-28
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-01-28
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-01-27
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-01-27
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-01-27
61
+ ## [1.0.6] 2025-11-27
32
62
  ### Changes:
33
63
  - Plugin description
34
64
 
35
- ## [1.0.5] 2025-01-27
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-01-26
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 charts
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.2.0",
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
+ }
@@ -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,