signalk-charts-provider-simple 1.2.0 → 1.3.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,16 @@
1
1
  # Changelog
2
2
 
3
- ## [1.2.0] 2025-01-28
3
+ ## [1.3.0] 2025-11-30
4
+ ### New:
5
+ - Chart Info button - View detailed MBTiles metadata directly in the Chart Manager
6
+ - Displays chart name, description, version, type, format
7
+ - Shows geographic bounds (SW/NE coordinates), zoom levels, center point
8
+ - Tile count with formatting
9
+ - Attribution, credits, tags, and legend information
10
+ - Beautiful modal dialog matching the existing Material Design 3 theme
11
+ - Works for both grid and list view
12
+
13
+ ## [1.2.0] 2025-11-28
4
14
  ### New:
5
15
  - 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
16
 
@@ -8,15 +18,15 @@
8
18
  - "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
19
  - Unnessecary server reload notifications removed
10
20
 
11
- ## [1.1.1] 2025-01-28
21
+ ## [1.1.1] 2025-11-28
12
22
  ### New:
13
23
  - [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
24
 
15
- ## [1.1.0] 2025-01-28
25
+ ## [1.1.0] 2025-11-28
16
26
  ### New:
17
27
  - Press and hold, than drag and drop charts to folder on touch and mobile devices - tested win11 & iOS 26
18
28
 
19
- ## [1.0.8] 2025-01-27
29
+ ## [1.0.8] 2025-11-27
20
30
  ### Fixes:
21
31
  - A newly generated folder in Chart Manager was not available in the Download area
22
32
 
@@ -24,22 +34,22 @@
24
34
  - No Scrolling for Chart Manager top buttons, breaks the design
25
35
  - CSS optimized between mobile and desktop
26
36
 
27
- ## [1.0.7] 2025-01-27
37
+ ## [1.0.7] 2025-11-27
28
38
  ### Changes:
29
39
  - to avoid conflicts use defaultChartsPath as charts-simple
30
40
 
31
- ## [1.0.6] 2025-01-27
41
+ ## [1.0.6] 2025-11-27
32
42
  ### Changes:
33
43
  - Plugin description
34
44
 
35
- ## [1.0.5] 2025-01-27
45
+ ## [1.0.5] 2025-11-27
36
46
  ### Fixes:
37
47
  - Github publishing - remove npm test
38
48
  - Github publisihing - Firx workflow
39
49
  ### Changes:
40
50
  - Typo in README.md
41
51
 
42
- ## [1.0.0] 2025-01-26
52
+ ## [1.0.0] 2025-11-26
43
53
  - Initial release
44
54
  - Material Design 3 UI
45
55
  - Optimized for touch and mobile
package/README.md CHANGED
@@ -48,6 +48,7 @@ The interface provides two main tabs:
48
48
 
49
49
  1. **Manage Charts**:
50
50
  - View all charts in your library
51
+ - View detailed chart information (metadata, bounds, zoom levels, tile count, etc.)
51
52
  - Enable/disable charts
52
53
  - Organize charts into folders
53
54
  - Upload new chart files
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.3.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
 
@@ -224,6 +224,9 @@ function renderChartCard(chart) {
224
224
  </div>
225
225
  </div>
226
226
  <div class="chart-card-footer">
227
+ <button class="btn btn-sm btn-info" onclick="showChartInfo('${chart.relativePath}')" title="View chart information">
228
+ Info
229
+ </button>
227
230
  <button class="btn btn-sm btn-secondary" onclick="showRenameDialog('${chart.relativePath}', '${chart.name}', '${chart.folder}')" title="Rename chart">
228
231
  Rename
229
232
  </button>
@@ -252,6 +255,9 @@ function renderChartCard(chart) {
252
255
  </div>
253
256
  </div>
254
257
  <div class="chart-list-actions">
258
+ <button class="btn btn-sm btn-info" onclick="showChartInfo('${chart.relativePath}')" title="View chart information">
259
+ Info
260
+ </button>
255
261
  <button class="btn btn-sm btn-secondary" onclick="showRenameDialog('${chart.relativePath}', '${chart.name}', '${chart.folder}')" title="Rename chart">
256
262
  Rename
257
263
  </button>
@@ -1288,3 +1294,113 @@ function showUploadNotification(fileCount) {
1288
1294
  }
1289
1295
  }, 5000);
1290
1296
  }
1297
+
1298
+ // Chart info modal
1299
+ window.showChartInfo = async function(chartPath) {
1300
+ try {
1301
+ const response = await fetch(`/signalk/chart-tiles/chart-metadata/${encodeURIComponent(chartPath)}`);
1302
+
1303
+ if (!response.ok) {
1304
+ const errorText = await response.text();
1305
+ showErrorNotification(`Failed to load chart information: ${errorText}`);
1306
+ return;
1307
+ }
1308
+
1309
+ const metadata = await response.json();
1310
+
1311
+ // Format the metadata nicely
1312
+ const formatValue = (key, value) => {
1313
+ if (value === null || value === undefined || value === '') {
1314
+ return '<span style="color: var(--text-secondary); font-style: italic;">Not specified</span>';
1315
+ }
1316
+
1317
+ switch(key) {
1318
+ case 'bounds':
1319
+ // Format: [minLon, minLat, maxLon, maxLat]
1320
+ try {
1321
+ const bounds = typeof value === 'string' ? value.split(',').map(v => parseFloat(v.trim())) : value;
1322
+ return `
1323
+ <div style="font-family: monospace; font-size: 0.9em;">
1324
+ SW: ${bounds[0].toFixed(4)}°, ${bounds[1].toFixed(4)}°<br>
1325
+ NE: ${bounds[2].toFixed(4)}°, ${bounds[3].toFixed(4)}°
1326
+ </div>
1327
+ `;
1328
+ } catch {
1329
+ return value;
1330
+ }
1331
+ case 'tileCount':
1332
+ return parseInt(value).toLocaleString();
1333
+ case 'minzoom':
1334
+ case 'maxzoom':
1335
+ return `Level ${value}`;
1336
+ case 'legend':
1337
+ return '<span style="color: var(--text-secondary); font-style: italic;">Available (not displayed)</span>';
1338
+ default:
1339
+ return value;
1340
+ }
1341
+ };
1342
+
1343
+ // Build metadata rows
1344
+ const metadataRows = [
1345
+ { label: 'Chart Name', key: 'name' },
1346
+ { label: 'Description', key: 'description' },
1347
+ { label: 'Version', key: 'version' },
1348
+ { label: 'Type', key: 'type' },
1349
+ { label: 'Format', key: 'format' },
1350
+ { label: 'Bounds', key: 'bounds' },
1351
+ { label: 'Min Zoom', key: 'minzoom' },
1352
+ { label: 'Max Zoom', key: 'maxzoom' },
1353
+ { label: 'Center', key: 'center' },
1354
+ { label: 'Tile Count', key: 'tileCount' },
1355
+ { label: 'Attribution', key: 'attribution' },
1356
+ { label: 'Credits', key: 'credits' },
1357
+ { label: 'Tags', key: 'tags' },
1358
+ { label: 'Legend', key: 'legend' }
1359
+ ];
1360
+
1361
+ const metadataHTML = metadataRows
1362
+ .filter(row => metadata[row.key] !== undefined)
1363
+ .map(row => `
1364
+ <div class="chart-info-row">
1365
+ <div class="chart-info-label">${row.label}:</div>
1366
+ <div class="chart-info-value">${formatValue(row.key, metadata[row.key])}</div>
1367
+ </div>
1368
+ `).join('');
1369
+
1370
+ const infoIcon = window.getIcon('info', true);
1371
+
1372
+ const modalHTML = `
1373
+ <div class="delete-modal-overlay" id="chartInfoModal" onclick="closeChartInfoModal(event)">
1374
+ <div class="delete-modal chart-info-modal" onclick="event.stopPropagation()">
1375
+ <div class="delete-modal-header">
1376
+ <div class="delete-modal-icon" style="color: var(--accent-primary);">${infoIcon}</div>
1377
+ <h3>Chart Information</h3>
1378
+ </div>
1379
+ <div class="delete-modal-body">
1380
+ <div class="chart-info-container">
1381
+ ${metadataHTML}
1382
+ </div>
1383
+ </div>
1384
+ <div class="delete-modal-actions">
1385
+ <button class="btn btn-primary" onclick="closeChartInfoModal()">Close</button>
1386
+ </div>
1387
+ </div>
1388
+ </div>
1389
+ `;
1390
+
1391
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
1392
+
1393
+ } catch (error) {
1394
+ console.error('Error fetching chart info:', error);
1395
+ showErrorNotification('Error loading chart information: ' + error.message);
1396
+ }
1397
+ }
1398
+
1399
+ window.closeChartInfoModal = function(event) {
1400
+ if (event && event.target.id !== 'chartInfoModal') return;
1401
+
1402
+ const modal = document.getElementById('chartInfoModal');
1403
+ if (modal) {
1404
+ modal.remove();
1405
+ }
1406
+ }
package/src/index.js CHANGED
@@ -648,6 +648,81 @@ module.exports = (app) => {
648
648
  }
649
649
  });
650
650
 
651
+ // Get chart metadata (MBTiles only)
652
+ app.get(`${chartTilesPath}/chart-metadata/:chartPath`, async (req, res) => {
653
+ const chartPathParam = decodeURIComponent(req.params.chartPath);
654
+
655
+ try {
656
+ const basePath = props.chartPath || defaultChartsPath;
657
+ const fullPath = path.join(basePath, chartPathParam);
658
+
659
+ // Security check: ensure the path is within the base chart directory
660
+ const normalizedFullPath = path.normalize(fullPath);
661
+ const normalizedBasePath = path.normalize(basePath);
662
+ if (!normalizedFullPath.startsWith(normalizedBasePath)) {
663
+ return res.status(403).send('Access denied: Invalid path');
664
+ }
665
+
666
+ // Check if file exists
667
+ if (!fs.existsSync(fullPath)) {
668
+ return res.status(404).send('Chart not found');
669
+ }
670
+
671
+ // Only support .mbtiles files
672
+ if (!fullPath.endsWith('.mbtiles')) {
673
+ return res.status(400).send('Metadata only available for MBTiles charts');
674
+ }
675
+
676
+ // Read metadata from SQLite database
677
+ const sqlite3 = require('sqlite3').verbose();
678
+ const db = new sqlite3.Database(fullPath, sqlite3.OPEN_READONLY, (err) => {
679
+ if (err) {
680
+ console.error('Error opening database:', err);
681
+ return res.status(500).send('Error reading chart metadata');
682
+ }
683
+ });
684
+
685
+ db.all('SELECT name, value FROM metadata', [], (err, rows) => {
686
+ if (err) {
687
+ db.close();
688
+ console.error('Error querying metadata:', err);
689
+ return res.status(500).send('Error reading chart metadata');
690
+ }
691
+
692
+ // Convert rows to object
693
+ const metadata = {};
694
+ rows.forEach(row => {
695
+ metadata[row.name] = row.value;
696
+ });
697
+
698
+ // Try to get tile count - different MBTiles files use different table names
699
+ // Try 'map' first (standard), then 'tiles' (alternative)
700
+ db.get('SELECT COUNT(*) as count FROM map', [], (err, countRow) => {
701
+ if (err) {
702
+ // Try alternative table name
703
+ db.get('SELECT COUNT(*) as count FROM tiles', [], (err2, countRow2) => {
704
+ db.close();
705
+
706
+ if (!err2 && countRow2) {
707
+ metadata.tileCount = countRow2.count;
708
+ }
709
+ // If both fail, silently omit tile count - some MBTiles formats don't have this
710
+ return res.json(metadata);
711
+ });
712
+ } else {
713
+ db.close();
714
+ metadata.tileCount = countRow.count;
715
+ res.json(metadata);
716
+ }
717
+ });
718
+ });
719
+
720
+ } catch (error) {
721
+ console.error('Error fetching chart metadata:', error);
722
+ res.status(500).send('Error fetching chart metadata');
723
+ }
724
+ });
725
+
651
726
  // Upload chart file
652
727
  app.post(`${chartTilesPath}/upload`, async (req, res) => {
653
728
  try {