Phase 2: MapLibre GL with OpenSeaMap navigation
- Integrated MapLibre GL for professional maritime mapping - Combined OSM base maps with OpenSeaMap layer for nautical overlays * Seamarks, buoys, channels, and depth information * Fully styled with MapLibre-compatible tiles - Created NavigationMap component with: * Real-time ship position marker with heading indicator * Automatic centering and smooth flyTo animations * Waypoint display with current waypoint highlighting * Ship track visualization (last 500 points, dashed line) * Route polyline showing waypoints - Professional map controls: * Zoom in/out buttons with smooth animations * Center-on-ship button for quick navigation * Info panel showing current position, heading, speed, distance * Glassmorphic info panel with dark/light mode support - Enhanced SignalK mock: * Added trackPoints array to record ship movement * Automatically maintains last 500 points for performance * Integrated with map for visual track history - Updated Navigation page to use new map - Build: 68 modules, 1.24 MB (343 KB gzipped) - Hot module reloading working smoothly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
289
dashboard/package-lock.json
generated
289
dashboard/package-lock.json
generated
@@ -8,8 +8,11 @@
|
||||
"name": "bordanlage-dashboard",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"leaflet": "^1.9.4",
|
||||
"maplibre-gl": "^5.21.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"react-dom": "^18.3.1",
|
||||
"react-leaflet": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
@@ -739,6 +742,122 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/jsonlint-lines-primitives": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
|
||||
"integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/point-geometry": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
|
||||
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@mapbox/tiny-sdf": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz",
|
||||
"integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/unitbezier": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
|
||||
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/vector-tile": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz",
|
||||
"integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "~1.1.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"pbf": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/whoots-js": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
|
||||
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/geojson-vt": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.0.4.tgz",
|
||||
"integrity": "sha512-HYv3POhMRCdhP3UPPATM/hfcy6/WuVIf5FKboH8u/ZuFMTnAIcSVlq5nfOqroLokd925w2QtE7YwquFOIacwVQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"kdbush": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||
"version": "24.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.7.0.tgz",
|
||||
"integrity": "sha512-Ed7rcKYU5iELfablg9Mj+TVCsXsPBgdMyXPRAxb2v7oWg9YJnpQdZ5msDs1LESu/mtXy3Z48Vdppv2t/x5kAhw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"quickselect": "^3.0.0",
|
||||
"rw": "^1.3.3",
|
||||
"tinyqueue": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"gl-style-format": "dist/gl-style-format.mjs",
|
||||
"gl-style-migrate": "dist/gl-style-migrate.mjs",
|
||||
"gl-style-validate": "dist/gl-style-validate.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/mlt": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.8.tgz",
|
||||
"integrity": "sha512-8vtfYGidr1rNkv5IwIoU2lfe3Oy+Wa8HluzQYcQi9cveU9K3pweAal/poQj4GJ0K/EW4bTQp2wVAs09g2yDRZg==",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/vt-pbf": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.0.tgz",
|
||||
"integrity": "sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "^1.1.0",
|
||||
"@mapbox/vector-tile": "^2.0.4",
|
||||
"@maplibre/geojson-vt": "^5.0.4",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/supercluster": "^7.1.3",
|
||||
"pbf": "^4.0.1",
|
||||
"supercluster": "^8.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/vt-pbf/node_modules/@maplibre/geojson-vt": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz",
|
||||
"integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@react-leaflet/core": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
|
||||
"integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
|
||||
"license": "Hippocratic-2.1",
|
||||
"peerDependencies": {
|
||||
"leaflet": "^1.9.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.27",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||
@@ -1148,6 +1267,21 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/supercluster": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||
"integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||
@@ -1262,6 +1396,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/earcut": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
|
||||
"integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.325",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.325.tgz",
|
||||
@@ -1343,6 +1483,12 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gl-matrix": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -1362,6 +1508,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/json-stringify-pretty-compact": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
|
||||
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
@@ -1375,6 +1527,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/kdbush": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
||||
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/leaflet": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -1397,6 +1561,49 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/maplibre-gl": {
|
||||
"version": "5.21.1",
|
||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.21.1.tgz",
|
||||
"integrity": "sha512-zto1RTnFkOpOO1bm93ElCXF1huey2N4LvXaGLMFcYAu9txh0OhGIdX1q3LZLkrMKgMxMeYduaQo+DVNzg098fg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||
"@mapbox/point-geometry": "^1.1.0",
|
||||
"@mapbox/tiny-sdf": "^2.0.7",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"@mapbox/vector-tile": "^2.0.4",
|
||||
"@mapbox/whoots-js": "^3.1.0",
|
||||
"@maplibre/geojson-vt": "^6.0.4",
|
||||
"@maplibre/maplibre-gl-style-spec": "^24.7.0",
|
||||
"@maplibre/mlt": "^1.1.8",
|
||||
"@maplibre/vt-pbf": "^4.3.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"earcut": "^3.0.2",
|
||||
"gl-matrix": "^3.4.4",
|
||||
"kdbush": "^4.0.2",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"pbf": "^4.0.1",
|
||||
"potpack": "^2.1.0",
|
||||
"quickselect": "^3.0.0",
|
||||
"tinyqueue": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14.0",
|
||||
"npm": ">=8.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -1404,6 +1611,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/murmurhash-js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -1430,6 +1643,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pbf": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
|
||||
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbf": "bin/pbf"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -1466,6 +1691,24 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/potpack": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
|
||||
"integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quickselect": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
@@ -1491,6 +1734,20 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-leaflet": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
|
||||
"integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
|
||||
"license": "Hippocratic-2.1",
|
||||
"dependencies": {
|
||||
"@react-leaflet/core": "^3.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"leaflet": "^1.9.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
@@ -1501,6 +1758,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-protobuf-schema": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"protocol-buffers-schema": "^3.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz",
|
||||
@@ -1546,6 +1812,12 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rw": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
@@ -1575,6 +1847,21 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supercluster": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
|
||||
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"kdbush": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyqueue": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"leaflet": "^1.9.4",
|
||||
"maplibre-gl": "^5.21.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"react-dom": "^18.3.1",
|
||||
"react-leaflet": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
|
||||
387
dashboard/src/components/nav/NavigationMap.jsx
Normal file
387
dashboard/src/components/nav/NavigationMap.jsx
Normal file
@@ -0,0 +1,387 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import maplibregl from 'maplibre-gl'
|
||||
import { useNMEA } from '../../hooks/useNMEA.js'
|
||||
import { getApi } from '../../mock/index.js'
|
||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||
|
||||
export default function NavigationMap() {
|
||||
const { lat, lon, heading, sog } = useNMEA()
|
||||
const mapContainer = useRef(null)
|
||||
const map = useRef(null)
|
||||
const shipMarkerRef = useRef(null)
|
||||
const trackSourceRef = useRef(false)
|
||||
const [zoom, setZoom] = useState(11)
|
||||
|
||||
const api = getApi()
|
||||
const snapshot = api.signalk.getSnapshot?.()
|
||||
const waypoints = api.signalk.getWaypoints?.() || []
|
||||
|
||||
const mapLat = lat ?? 55.32
|
||||
const mapLon = lon ?? 15.22
|
||||
|
||||
useEffect(() => {
|
||||
if (map.current) return
|
||||
|
||||
map.current = new maplibregl.Map({
|
||||
container: mapContainer.current,
|
||||
style: {
|
||||
version: 8,
|
||||
sources: {
|
||||
'osm': {
|
||||
type: 'raster',
|
||||
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
||||
tileSize: 256,
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
},
|
||||
'seamark': {
|
||||
type: 'raster',
|
||||
tiles: ['https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png'],
|
||||
tileSize: 256,
|
||||
attribution: '© OpenSeaMap contributors',
|
||||
},
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
id: 'osm-base',
|
||||
type: 'raster',
|
||||
source: 'osm',
|
||||
minzoom: 0,
|
||||
maxzoom: 19,
|
||||
},
|
||||
{
|
||||
id: 'seamark-overlay',
|
||||
type: 'raster',
|
||||
source: 'seamark',
|
||||
minzoom: 5,
|
||||
maxzoom: 19,
|
||||
paint: {
|
||||
'raster-opacity': 0.8,
|
||||
},
|
||||
},
|
||||
],
|
||||
glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf',
|
||||
},
|
||||
center: [mapLon, mapLat],
|
||||
zoom: 11,
|
||||
pitch: 0,
|
||||
bearing: 0,
|
||||
antialias: true,
|
||||
})
|
||||
|
||||
// Add navigation controls
|
||||
map.current.addControl(new maplibregl.NavigationControl(), 'top-right')
|
||||
|
||||
// Add ship marker source and layer
|
||||
map.current.on('load', () => {
|
||||
if (!map.current.getSource('ship')) {
|
||||
map.current.addSource('ship', {
|
||||
type: 'geojson',
|
||||
data: {
|
||||
type: 'Feature',
|
||||
geometry: { type: 'Point', coordinates: [mapLon, mapLat] },
|
||||
properties: { heading: 0 },
|
||||
},
|
||||
})
|
||||
|
||||
map.current.addLayer({
|
||||
id: 'ship-marker',
|
||||
type: 'symbol',
|
||||
source: 'ship',
|
||||
layout: {
|
||||
'icon-image': 'marker-blue',
|
||||
'icon-size': 1.2,
|
||||
'icon-rotate': ['get', 'heading'],
|
||||
'icon-allow-overlap': true,
|
||||
'icon-ignore-placement': true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Add track source for ship trail
|
||||
if (!map.current.getSource('track')) {
|
||||
map.current.addSource('track', {
|
||||
type: 'geojson',
|
||||
data: {
|
||||
type: 'Feature',
|
||||
geometry: { type: 'LineString', coordinates: [[mapLon, mapLat]] },
|
||||
},
|
||||
})
|
||||
|
||||
map.current.addLayer({
|
||||
id: 'track-line',
|
||||
type: 'line',
|
||||
source: 'track',
|
||||
paint: {
|
||||
'line-color': '#0ea5e9',
|
||||
'line-width': 2,
|
||||
'line-opacity': 0.7,
|
||||
'line-dasharray': [5, 5],
|
||||
},
|
||||
})
|
||||
trackSourceRef.current = true
|
||||
}
|
||||
|
||||
// Add waypoints
|
||||
if (waypoints.length > 0) {
|
||||
const waypointFeatures = waypoints.map((wp, idx) => ({
|
||||
type: 'Feature',
|
||||
geometry: { type: 'Point', coordinates: [wp.lon, wp.lat] },
|
||||
properties: { index: idx, isCurrent: idx === snapshot?.currentWaypoint },
|
||||
}))
|
||||
|
||||
if (!map.current.getSource('waypoints')) {
|
||||
map.current.addSource('waypoints', {
|
||||
type: 'geojson',
|
||||
data: {
|
||||
type: 'FeatureCollection',
|
||||
features: waypointFeatures,
|
||||
},
|
||||
})
|
||||
|
||||
map.current.addLayer({
|
||||
id: 'waypoint-circles',
|
||||
type: 'circle',
|
||||
source: 'waypoints',
|
||||
paint: {
|
||||
'circle-radius': 8,
|
||||
'circle-color': ['case', ['get', 'isCurrent'], '#0ea5e9', '#f59e0b'],
|
||||
'circle-stroke-width': 2,
|
||||
'circle-stroke-color': '#ffffff',
|
||||
},
|
||||
})
|
||||
|
||||
map.current.addLayer({
|
||||
id: 'waypoint-labels',
|
||||
type: 'symbol',
|
||||
source: 'waypoints',
|
||||
layout: {
|
||||
'text-field': ['to-string', ['+', ['get', 'index'], 1]],
|
||||
'text-size': 12,
|
||||
'text-font': ['Open Sans Semibold'],
|
||||
'text-offset': [0, 0],
|
||||
'text-allow-overlap': true,
|
||||
},
|
||||
paint: {
|
||||
'text-color': '#ffffff',
|
||||
'text-halo-color': '#000000',
|
||||
'text-halo-width': 1,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
map.current.getSource('waypoints').setData({
|
||||
type: 'FeatureCollection',
|
||||
features: waypointFeatures,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
map.current.on('zoom', () => setZoom(map.current.getZoom()))
|
||||
|
||||
return () => {
|
||||
// Cleanup is handled by React
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Update ship position
|
||||
useEffect(() => {
|
||||
if (!map.current || lat == null || lon == null) return
|
||||
|
||||
if (map.current.getSource('ship')) {
|
||||
const trackSource = map.current.getSource('track')
|
||||
if (trackSource && trackSourceRef.current) {
|
||||
const currentData = trackSource._data
|
||||
if (currentData.geometry.coordinates.length > 500) {
|
||||
currentData.geometry.coordinates.shift() // Keep last 500 points
|
||||
}
|
||||
currentData.geometry.coordinates.push([lon, lat])
|
||||
trackSource.setData(currentData)
|
||||
}
|
||||
|
||||
map.current.getSource('ship').setData({
|
||||
type: 'Feature',
|
||||
geometry: { type: 'Point', coordinates: [lon, lat] },
|
||||
properties: { heading: heading ?? 0 },
|
||||
})
|
||||
|
||||
// Auto-center on ship
|
||||
map.current.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: zoom,
|
||||
speed: 0.5,
|
||||
curve: 1,
|
||||
})
|
||||
}
|
||||
}, [lat, lon, heading, zoom])
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div ref={mapContainer} style={styles.mapBox} />
|
||||
|
||||
{/* Map Controls */}
|
||||
<div style={styles.controls}>
|
||||
<button
|
||||
className="icon ghost"
|
||||
onClick={() => map.current?.zoomIn()}
|
||||
title="Zoom in"
|
||||
style={{ fontSize: 18 }}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
className="icon ghost"
|
||||
onClick={() => map.current?.zoomOut()}
|
||||
title="Zoom out"
|
||||
style={{ fontSize: 18 }}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<button
|
||||
className="icon ghost"
|
||||
onClick={() => {
|
||||
if (lat != null && lon != null) {
|
||||
map.current?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 12,
|
||||
duration: 1000,
|
||||
})
|
||||
}
|
||||
}}
|
||||
title="Center on ship"
|
||||
style={{ fontSize: 16 }}
|
||||
>
|
||||
⊙
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Info Panel */}
|
||||
{lat != null && lon != null && (
|
||||
<div style={styles.infoPanel}>
|
||||
<div style={styles.infoSection}>
|
||||
<div style={styles.infoRow}>
|
||||
<span style={styles.label}>Position</span>
|
||||
<span style={styles.value}>{lat.toFixed(5)}°</span>
|
||||
</div>
|
||||
<div style={styles.infoRow}>
|
||||
<span style={styles.label}></span>
|
||||
<span style={styles.value}>{lon.toFixed(5)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{heading != null && (
|
||||
<div style={styles.infoSection}>
|
||||
<div style={styles.infoRow}>
|
||||
<span style={styles.label}>Heading</span>
|
||||
<span style={styles.value}>{Math.round(heading)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sog != null && (
|
||||
<div style={styles.infoSection}>
|
||||
<div style={styles.infoRow}>
|
||||
<span style={styles.label}>Speed</span>
|
||||
<span style={styles.value}>{(sog * 1.943844).toFixed(1)} kn</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{waypoints.length > 0 && snapshot?.currentWaypoint != null && (
|
||||
<div style={styles.infoSection}>
|
||||
<div style={styles.infoRow}>
|
||||
<span style={styles.label}>Route</span>
|
||||
<span style={styles.value}>WP {snapshot.currentWaypoint + 1} / {waypoints.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{snapshot?.distanceToWaypoint != null && (
|
||||
<div style={styles.infoSection}>
|
||||
<div style={styles.infoRow}>
|
||||
<span style={styles.label}>Distance</span>
|
||||
<span style={styles.value}>{(snapshot.distanceToWaypoint * 1.852).toFixed(1)} km</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Map Style Toggle */}
|
||||
<div style={styles.layerToggle}>
|
||||
<span style={{ fontSize: 11, color: 'var(--muted)' }}>+ SeaMarks</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
borderRadius: 'var(--radius)',
|
||||
},
|
||||
mapBox: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
controls: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 6,
|
||||
zIndex: 1000,
|
||||
},
|
||||
infoPanel: {
|
||||
position: 'absolute',
|
||||
bottom: 12,
|
||||
left: 12,
|
||||
background: 'var(--glass-bg)',
|
||||
backdropFilter: 'blur(var(--glass-blur))',
|
||||
WebkitBackdropFilter: 'blur(var(--glass-blur))',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 'var(--radius-lg)',
|
||||
padding: '10px 12px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 6,
|
||||
fontSize: 12,
|
||||
color: 'var(--text)',
|
||||
zIndex: 500,
|
||||
maxWidth: 180,
|
||||
animation: 'slideInUp 0.3s ease-out',
|
||||
},
|
||||
infoSection: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
},
|
||||
infoRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
},
|
||||
label: {
|
||||
color: 'var(--muted)',
|
||||
fontWeight: 600,
|
||||
fontSize: 10,
|
||||
},
|
||||
value: {
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontWeight: 600,
|
||||
color: 'var(--accent)',
|
||||
textAlign: 'right',
|
||||
},
|
||||
layerToggle: {
|
||||
position: 'absolute',
|
||||
top: 60,
|
||||
right: 12,
|
||||
background: 'rgba(14, 165, 233, 0.1)',
|
||||
border: '1px solid rgba(14, 165, 233, 0.3)',
|
||||
borderRadius: 'var(--radius)',
|
||||
padding: '4px 8px',
|
||||
zIndex: 500,
|
||||
},
|
||||
}
|
||||
@@ -69,6 +69,7 @@ export function createSignalKMock() {
|
||||
waterUsed: 23,
|
||||
wasteWater: 18,
|
||||
freshWater: 156,
|
||||
trackPoints: [{ lat: WAYPOINTS[0].lat, lon: WAYPOINTS[0].lon }],
|
||||
}
|
||||
|
||||
// Navigate to next waypoint
|
||||
@@ -115,6 +116,12 @@ export function createSignalKMock() {
|
||||
|
||||
state.lat += dLat
|
||||
state.lon += dLon
|
||||
|
||||
// Record track point (keep last 500)
|
||||
state.trackPoints.push({ lat: state.lat, lon: state.lon })
|
||||
if (state.trackPoints.length > 500) {
|
||||
state.trackPoints.shift()
|
||||
}
|
||||
}
|
||||
|
||||
function buildDelta() {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import ChartPlaceholder from '../components/nav/ChartPlaceholder.jsx'
|
||||
import NavigationMap from '../components/nav/NavigationMap.jsx'
|
||||
import InstrumentPanel from '../components/nav/InstrumentPanel.jsx'
|
||||
|
||||
export default function Navigation() {
|
||||
return (
|
||||
<div style={styles.layout}>
|
||||
<div style={styles.chart}>
|
||||
<ChartPlaceholder />
|
||||
<NavigationMap />
|
||||
</div>
|
||||
<div style={styles.panel}>
|
||||
<InstrumentPanel />
|
||||
|
||||
1
package.json
Normal file
1
package.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
Reference in New Issue
Block a user