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 +18 -8
- package/README.md +1 -0
- 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 +116 -0
- package/src/index.js +75 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [1.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
41
|
+
## [1.0.6] 2025-11-27
|
|
32
42
|
### Changes:
|
|
33
43
|
- Plugin description
|
|
34
44
|
|
|
35
|
-
## [1.0.5] 2025-
|
|
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-
|
|
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.
|
|
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
|
+
}
|
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
|
|
|
@@ -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 {
|