add vector mbtiles
This commit is contained in:
parent
c3fe22fb4a
commit
60fa1eec06
10 changed files with 628 additions and 18 deletions
|
|
@ -14,6 +14,7 @@
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "^2",
|
"@tauri-apps/plugin-dialog": "^2",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"maplibre-gl": "^5.7.2",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-leaflet": "^4.2.1"
|
"react-leaflet": "^4.2.1"
|
||||||
|
|
|
||||||
214
frontend/pnpm-lock.yaml
generated
214
frontend/pnpm-lock.yaml
generated
|
|
@ -20,6 +20,9 @@ importers:
|
||||||
leaflet:
|
leaflet:
|
||||||
specifier: ^1.9.4
|
specifier: ^1.9.4
|
||||||
version: 1.9.4
|
version: 1.9.4
|
||||||
|
maplibre-gl:
|
||||||
|
specifier: ^5.7.2
|
||||||
|
version: 5.15.0
|
||||||
react:
|
react:
|
||||||
specifier: ^19.1.0
|
specifier: ^19.1.0
|
||||||
version: 19.2.3
|
version: 19.2.3
|
||||||
|
|
@ -312,6 +315,40 @@ packages:
|
||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
|
'@mapbox/geojson-rewind@0.5.2':
|
||||||
|
resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
'@mapbox/jsonlint-lines-primitives@2.0.2':
|
||||||
|
resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
'@mapbox/point-geometry@1.1.0':
|
||||||
|
resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==}
|
||||||
|
|
||||||
|
'@mapbox/tiny-sdf@2.0.7':
|
||||||
|
resolution: {integrity: sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==}
|
||||||
|
|
||||||
|
'@mapbox/unitbezier@0.0.1':
|
||||||
|
resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==}
|
||||||
|
|
||||||
|
'@mapbox/vector-tile@2.0.4':
|
||||||
|
resolution: {integrity: sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==}
|
||||||
|
|
||||||
|
'@mapbox/whoots-js@3.1.0':
|
||||||
|
resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
'@maplibre/maplibre-gl-style-spec@24.4.1':
|
||||||
|
resolution: {integrity: sha512-UKhA4qv1h30XT768ccSv5NjNCX+dgfoq2qlLVmKejspPcSQTYD4SrVucgqegmYcKcmwf06wcNAa/kRd0NHWbUg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
'@maplibre/mlt@1.1.2':
|
||||||
|
resolution: {integrity: sha512-SQKdJ909VGROkA6ovJgtHNs9YXV4YXUPS+VaZ50I2Mt951SLlUm2Cv34x5Xwc1HiFlsd3h2Yrs5cn7xzqBmENw==}
|
||||||
|
|
||||||
|
'@maplibre/vt-pbf@4.2.0':
|
||||||
|
resolution: {integrity: sha512-bxrk/kQUwWXZgmqYgwOCnZCMONCRi3MJMqJdza4T3E4AeR5i+VyMnaJ8iDWtWxdfEAJRtrzIOeJtxZSy5mFrFA==}
|
||||||
|
|
||||||
'@react-leaflet/core@2.1.0':
|
'@react-leaflet/core@2.1.0':
|
||||||
resolution: {integrity: sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==}
|
resolution: {integrity: sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -527,6 +564,9 @@ packages:
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
|
'@types/geojson-vt@3.2.5':
|
||||||
|
resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==}
|
||||||
|
|
||||||
'@types/geojson@7946.0.16':
|
'@types/geojson@7946.0.16':
|
||||||
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
||||||
|
|
||||||
|
|
@ -544,6 +584,9 @@ packages:
|
||||||
'@types/react@19.2.7':
|
'@types/react@19.2.7':
|
||||||
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
||||||
|
|
||||||
|
'@types/supercluster@7.1.3':
|
||||||
|
resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==}
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.7.0':
|
'@vitejs/plugin-react@4.7.0':
|
||||||
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
|
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
|
@ -577,6 +620,9 @@ packages:
|
||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
earcut@3.0.2:
|
||||||
|
resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.267:
|
electron-to-chromium@1.5.267:
|
||||||
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
||||||
|
|
||||||
|
|
@ -607,6 +653,16 @@ packages:
|
||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
geojson-vt@4.0.2:
|
||||||
|
resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==}
|
||||||
|
|
||||||
|
get-stream@6.0.1:
|
||||||
|
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
gl-matrix@3.4.4:
|
||||||
|
resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==}
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
|
|
@ -615,20 +671,36 @@ packages:
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
json-stringify-pretty-compact@4.0.0:
|
||||||
|
resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==}
|
||||||
|
|
||||||
json5@2.2.3:
|
json5@2.2.3:
|
||||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
kdbush@4.0.2:
|
||||||
|
resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==}
|
||||||
|
|
||||||
leaflet@1.9.4:
|
leaflet@1.9.4:
|
||||||
resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==}
|
resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
|
maplibre-gl@5.15.0:
|
||||||
|
resolution: {integrity: sha512-pPeu/t4yPDX/+Uf9ibLUdmaKbNMlGxMAX+tBednYukol2qNk2TZXAlhdohWxjVvTO3is8crrUYv3Ok02oAaKzA==}
|
||||||
|
engines: {node: '>=16.14.0', npm: '>=8.1.0'}
|
||||||
|
|
||||||
|
minimist@1.2.8:
|
||||||
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
murmurhash-js@1.0.0:
|
||||||
|
resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==}
|
||||||
|
|
||||||
nanoid@3.3.11:
|
nanoid@3.3.11:
|
||||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
|
|
@ -637,6 +709,10 @@ packages:
|
||||||
node-releases@2.0.27:
|
node-releases@2.0.27:
|
||||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||||
|
|
||||||
|
pbf@4.0.1:
|
||||||
|
resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
|
|
@ -648,6 +724,15 @@ packages:
|
||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
potpack@2.1.0:
|
||||||
|
resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==}
|
||||||
|
|
||||||
|
protocol-buffers-schema@3.6.0:
|
||||||
|
resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==}
|
||||||
|
|
||||||
|
quickselect@3.0.0:
|
||||||
|
resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==}
|
||||||
|
|
||||||
react-dom@19.2.3:
|
react-dom@19.2.3:
|
||||||
resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==}
|
resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -668,11 +753,17 @@ packages:
|
||||||
resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
|
resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
resolve-protobuf-schema@2.1.0:
|
||||||
|
resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==}
|
||||||
|
|
||||||
rollup@4.53.5:
|
rollup@4.53.5:
|
||||||
resolution: {integrity: sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==}
|
resolution: {integrity: sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
rw@1.3.3:
|
||||||
|
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
|
||||||
|
|
||||||
scheduler@0.27.0:
|
scheduler@0.27.0:
|
||||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||||
|
|
||||||
|
|
@ -684,10 +775,16 @@ packages:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
supercluster@8.0.1:
|
||||||
|
resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==}
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tinyqueue@3.0.0:
|
||||||
|
resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==}
|
||||||
|
|
||||||
typescript@5.8.3:
|
typescript@5.8.3:
|
||||||
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
|
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
|
|
@ -953,6 +1050,51 @@ snapshots:
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@mapbox/geojson-rewind@0.5.2':
|
||||||
|
dependencies:
|
||||||
|
get-stream: 6.0.1
|
||||||
|
minimist: 1.2.8
|
||||||
|
|
||||||
|
'@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':
|
||||||
|
dependencies:
|
||||||
|
'@mapbox/point-geometry': 1.1.0
|
||||||
|
'@types/geojson': 7946.0.16
|
||||||
|
pbf: 4.0.1
|
||||||
|
|
||||||
|
'@mapbox/whoots-js@3.1.0': {}
|
||||||
|
|
||||||
|
'@maplibre/maplibre-gl-style-spec@24.4.1':
|
||||||
|
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
|
||||||
|
|
||||||
|
'@maplibre/mlt@1.1.2':
|
||||||
|
dependencies:
|
||||||
|
'@mapbox/point-geometry': 1.1.0
|
||||||
|
|
||||||
|
'@maplibre/vt-pbf@4.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@mapbox/point-geometry': 1.1.0
|
||||||
|
'@mapbox/vector-tile': 2.0.4
|
||||||
|
'@types/geojson-vt': 3.2.5
|
||||||
|
'@types/supercluster': 7.1.3
|
||||||
|
geojson-vt: 4.0.2
|
||||||
|
pbf: 4.0.1
|
||||||
|
supercluster: 8.0.1
|
||||||
|
|
||||||
'@react-leaflet/core@2.1.0(leaflet@1.9.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
'@react-leaflet/core@2.1.0(leaflet@1.9.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
leaflet: 1.9.4
|
leaflet: 1.9.4
|
||||||
|
|
@ -1107,6 +1249,10 @@ snapshots:
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
|
'@types/geojson-vt@3.2.5':
|
||||||
|
dependencies:
|
||||||
|
'@types/geojson': 7946.0.16
|
||||||
|
|
||||||
'@types/geojson@7946.0.16': {}
|
'@types/geojson@7946.0.16': {}
|
||||||
|
|
||||||
'@types/leaflet.markercluster@1.5.6':
|
'@types/leaflet.markercluster@1.5.6':
|
||||||
|
|
@ -1125,6 +1271,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
|
'@types/supercluster@7.1.3':
|
||||||
|
dependencies:
|
||||||
|
'@types/geojson': 7946.0.16
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.7.0(vite@7.3.0)':
|
'@vitejs/plugin-react@4.7.0(vite@7.3.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
|
|
@ -1157,6 +1307,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
earcut@3.0.2: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.267: {}
|
electron-to-chromium@1.5.267: {}
|
||||||
|
|
||||||
esbuild@0.27.1:
|
esbuild@0.27.1:
|
||||||
|
|
@ -1199,24 +1351,68 @@ snapshots:
|
||||||
|
|
||||||
gensync@1.0.0-beta.2: {}
|
gensync@1.0.0-beta.2: {}
|
||||||
|
|
||||||
|
geojson-vt@4.0.2: {}
|
||||||
|
|
||||||
|
get-stream@6.0.1: {}
|
||||||
|
|
||||||
|
gl-matrix@3.4.4: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
|
|
||||||
|
json-stringify-pretty-compact@4.0.0: {}
|
||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
||||||
|
kdbush@4.0.2: {}
|
||||||
|
|
||||||
leaflet@1.9.4: {}
|
leaflet@1.9.4: {}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
|
|
||||||
|
maplibre-gl@5.15.0:
|
||||||
|
dependencies:
|
||||||
|
'@mapbox/geojson-rewind': 0.5.2
|
||||||
|
'@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/maplibre-gl-style-spec': 24.4.1
|
||||||
|
'@maplibre/mlt': 1.1.2
|
||||||
|
'@maplibre/vt-pbf': 4.2.0
|
||||||
|
'@types/geojson': 7946.0.16
|
||||||
|
'@types/geojson-vt': 3.2.5
|
||||||
|
'@types/supercluster': 7.1.3
|
||||||
|
earcut: 3.0.2
|
||||||
|
geojson-vt: 4.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
|
||||||
|
supercluster: 8.0.1
|
||||||
|
tinyqueue: 3.0.0
|
||||||
|
|
||||||
|
minimist@1.2.8: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
murmurhash-js@1.0.0: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
node-releases@2.0.27: {}
|
node-releases@2.0.27: {}
|
||||||
|
|
||||||
|
pbf@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
resolve-protobuf-schema: 2.1.0
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.3: {}
|
picomatch@4.0.3: {}
|
||||||
|
|
@ -1227,6 +1423,12 @@ snapshots:
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
potpack@2.1.0: {}
|
||||||
|
|
||||||
|
protocol-buffers-schema@3.6.0: {}
|
||||||
|
|
||||||
|
quickselect@3.0.0: {}
|
||||||
|
|
||||||
react-dom@19.2.3(react@19.2.3):
|
react-dom@19.2.3(react@19.2.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
|
|
@ -1243,6 +1445,10 @@ snapshots:
|
||||||
|
|
||||||
react@19.2.3: {}
|
react@19.2.3: {}
|
||||||
|
|
||||||
|
resolve-protobuf-schema@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
protocol-buffers-schema: 3.6.0
|
||||||
|
|
||||||
rollup@4.53.5:
|
rollup@4.53.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
|
|
@ -1271,17 +1477,25 @@ snapshots:
|
||||||
'@rollup/rollup-win32-x64-msvc': 4.53.5
|
'@rollup/rollup-win32-x64-msvc': 4.53.5
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
rw@1.3.3: {}
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
supercluster@8.0.1:
|
||||||
|
dependencies:
|
||||||
|
kdbush: 4.0.2
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
tinyqueue@3.0.0: {}
|
||||||
|
|
||||||
typescript@5.8.3: {}
|
typescript@5.8.3: {}
|
||||||
|
|
||||||
update-browserslist-db@1.2.2(browserslist@4.28.1):
|
update-browserslist-db@1.2.2(browserslist@4.28.1):
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,18 @@ body, #root {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maplibre {
|
||||||
|
height: 70vh;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-switch {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { MapContainer, Marker, Polyline, TileLayer, useMap } from "react-leaflet
|
||||||
import L, { LatLngExpression } from "leaflet";
|
import L, { LatLngExpression } from "leaflet";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
|
|
||||||
type FixMessage = {
|
type FixMessage = {
|
||||||
type: "fix";
|
type: "fix";
|
||||||
|
|
@ -27,6 +28,8 @@ type IncomingMessage = FixMessage | StatusMessage;
|
||||||
type AppConfig = {
|
type AppConfig = {
|
||||||
tiles_url: string;
|
tiles_url: string;
|
||||||
tiles_url_tms?: string;
|
tiles_url_tms?: string;
|
||||||
|
vector_tiles_url?: string;
|
||||||
|
vector_metadata_url?: string;
|
||||||
ws_url: string;
|
ws_url: string;
|
||||||
gnss_source: string;
|
gnss_source: string;
|
||||||
};
|
};
|
||||||
|
|
@ -73,6 +76,7 @@ export default function App() {
|
||||||
const [status, setStatus] = useState<string>("connecting");
|
const [status, setStatus] = useState<string>("connecting");
|
||||||
const [track, setTrack] = useState<TrackPoint[]>([]);
|
const [track, setTrack] = useState<TrackPoint[]>([]);
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
|
const [mapMode, setMapMode] = useState<"raster" | "vector">("raster");
|
||||||
const [selectedFileName, setSelectedFileName] = useState<string>("");
|
const [selectedFileName, setSelectedFileName] = useState<string>("");
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [sliceParams, setSliceParams] = useState({
|
const [sliceParams, setSliceParams] = useState({
|
||||||
|
|
@ -85,6 +89,8 @@ export default function App() {
|
||||||
source_format: "geotiff" as "geotiff" | "mbtiles" | "tms" | "xyz",
|
source_format: "geotiff" as "geotiff" | "mbtiles" | "tms" | "xyz",
|
||||||
});
|
});
|
||||||
const [sliceStatus, setSliceStatus] = useState<string>("idle");
|
const [sliceStatus, setSliceStatus] = useState<string>("idle");
|
||||||
|
const maplibreContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const maplibreRef = useRef<any>(null);
|
||||||
|
|
||||||
const uploadFileBlob = async (file: File) => {
|
const uploadFileBlob = async (file: File) => {
|
||||||
setSliceStatus("uploading");
|
setSliceStatus("uploading");
|
||||||
|
|
@ -100,6 +106,11 @@ export default function App() {
|
||||||
setSelectedFileName(res.filename);
|
setSelectedFileName(res.filename);
|
||||||
setSliceStatus("idle");
|
setSliceStatus("idle");
|
||||||
sendFrontLog(`uploaded source ${res.filename}`, "info");
|
sendFrontLog(`uploaded source ${res.filename}`, "info");
|
||||||
|
if (res.filename?.toLowerCase?.().endsWith(".mbtiles")) {
|
||||||
|
activateVectorMbtiles(res.path).catch((err) => {
|
||||||
|
sendFrontLog(`vector activate failed: ${err}`, "warn");
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setSliceStatus("error");
|
setSliceStatus("error");
|
||||||
|
|
@ -201,6 +212,181 @@ export default function App() {
|
||||||
: config.tiles_url || "/tiles/{z}/{x}/{y}.png"
|
: config.tiles_url || "/tiles/{z}/{x}/{y}.png"
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
const vectorTileUrl =
|
||||||
|
config && `${backendBase.replace(/\/+$/, "")}${config.vector_tiles_url || "/tiles/vector/{z}/{x}/{y}.pbf"}`;
|
||||||
|
|
||||||
|
const vectorMetaUrl =
|
||||||
|
config && `${backendBase.replace(/\/+$/, "")}${config.vector_metadata_url || "/tiles/vector/metadata"}`;
|
||||||
|
|
||||||
|
const activateVectorMbtiles = async (path: string) => {
|
||||||
|
const res = await fetch(`${backendBase}/tiles/vector/activate`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ path }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data?.detail || `status ${res.status}`);
|
||||||
|
sendFrontLog(`vector mbtiles activated: ${path}`, "info");
|
||||||
|
setMapMode("vector");
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mapMode !== "vector") return;
|
||||||
|
if (!maplibreContainerRef.current) return;
|
||||||
|
if (!vectorMetaUrl || !vectorTileUrl) return;
|
||||||
|
|
||||||
|
let canceled = false;
|
||||||
|
(async () => {
|
||||||
|
const metaRes = await fetch(vectorMetaUrl, { cache: "no-store" });
|
||||||
|
if (!metaRes.ok) return;
|
||||||
|
const meta = await metaRes.json();
|
||||||
|
const vectorLayersRaw = meta.vector_layers || meta["json"] || null;
|
||||||
|
let sourceLayers: string[] = [];
|
||||||
|
try {
|
||||||
|
const parsed = typeof vectorLayersRaw === "string" ? JSON.parse(vectorLayersRaw) : vectorLayersRaw;
|
||||||
|
const layers = parsed?.vector_layers ?? parsed?.layers ?? parsed ?? [];
|
||||||
|
if (Array.isArray(layers)) {
|
||||||
|
sourceLayers = layers.map((l: any) => l.id).filter(Boolean);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
sourceLayers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { default: maplibregl } = await import("maplibre-gl");
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
if (maplibreRef.current) {
|
||||||
|
try {
|
||||||
|
maplibreRef.current.remove();
|
||||||
|
} catch {}
|
||||||
|
maplibreRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style: any = {
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
osm: {
|
||||||
|
type: "vector",
|
||||||
|
tiles: [vectorTileUrl],
|
||||||
|
minzoom: Number(meta.minzoom ?? 0),
|
||||||
|
maxzoom: Number(meta.maxzoom ?? 14),
|
||||||
|
},
|
||||||
|
track: {
|
||||||
|
type: "geojson",
|
||||||
|
data: { type: "FeatureCollection", features: [] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{ id: "bg", type: "background", paint: { "background-color": "#050816" } },
|
||||||
|
],
|
||||||
|
glyphs: "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const id of sourceLayers) {
|
||||||
|
style.layers.push({
|
||||||
|
id: `fill-${id}`,
|
||||||
|
type: "fill",
|
||||||
|
source: "osm",
|
||||||
|
"source-layer": id,
|
||||||
|
filter: ["==", ["geometry-type"], "Polygon"],
|
||||||
|
paint: { "fill-color": "#1f2937", "fill-opacity": 0.35 },
|
||||||
|
});
|
||||||
|
style.layers.push({
|
||||||
|
id: `linepoly-${id}`,
|
||||||
|
type: "line",
|
||||||
|
source: "osm",
|
||||||
|
"source-layer": id,
|
||||||
|
filter: ["==", ["geometry-type"], "Polygon"],
|
||||||
|
paint: { "line-color": "#334155", "line-width": 1 },
|
||||||
|
});
|
||||||
|
style.layers.push({
|
||||||
|
id: `line-${id}`,
|
||||||
|
type: "line",
|
||||||
|
source: "osm",
|
||||||
|
"source-layer": id,
|
||||||
|
filter: ["==", ["geometry-type"], "LineString"],
|
||||||
|
paint: { "line-color": "#475569", "line-width": 1.2 },
|
||||||
|
});
|
||||||
|
style.layers.push({
|
||||||
|
id: `point-${id}`,
|
||||||
|
type: "circle",
|
||||||
|
source: "osm",
|
||||||
|
"source-layer": id,
|
||||||
|
filter: ["==", ["geometry-type"], "Point"],
|
||||||
|
paint: { "circle-radius": 2, "circle-color": "#94a3b8", "circle-opacity": 0.8 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
style.layers.push(
|
||||||
|
{
|
||||||
|
id: "track-line",
|
||||||
|
type: "line",
|
||||||
|
source: "track",
|
||||||
|
paint: { "line-color": "#7dd3fc", "line-width": 4, "line-opacity": 0.9 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "track-point",
|
||||||
|
type: "circle",
|
||||||
|
source: "track",
|
||||||
|
paint: { "circle-radius": 6, "circle-color": "#a78bfa", "circle-stroke-color": "#ffffff", "circle-stroke-width": 1 },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const map = new maplibregl.Map({
|
||||||
|
container: maplibreContainerRef.current!,
|
||||||
|
style,
|
||||||
|
center: [37.618423, 55.751244],
|
||||||
|
zoom: 11,
|
||||||
|
});
|
||||||
|
map.addControl(new maplibregl.NavigationControl({ visualizePitch: true }), "top-right");
|
||||||
|
|
||||||
|
map.on("load", () => {
|
||||||
|
const bounds = meta.bounds;
|
||||||
|
if (typeof bounds === "string") {
|
||||||
|
const parts = bounds.split(",").map((p: string) => Number(p.trim()));
|
||||||
|
if (parts.length === 4 && parts.every((n: number) => Number.isFinite(n))) {
|
||||||
|
map.fitBounds(
|
||||||
|
[
|
||||||
|
[parts[0], parts[1]],
|
||||||
|
[parts[2], parts[3]],
|
||||||
|
],
|
||||||
|
{ padding: 30, duration: 0 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
maplibreRef.current = map;
|
||||||
|
})().catch((err) => {
|
||||||
|
sendFrontLog(`maplibre init error: ${err}`, "error");
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
canceled = true;
|
||||||
|
};
|
||||||
|
}, [mapMode, vectorMetaUrl, vectorTileUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mapMode !== "vector") return;
|
||||||
|
const map = maplibreRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
const geojson = {
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: [
|
||||||
|
positions.length
|
||||||
|
? { type: "Feature", geometry: { type: "LineString", coordinates: positions.map((p: any) => [p[1], p[0]]) } }
|
||||||
|
: null,
|
||||||
|
lastPoint ? { type: "Feature", geometry: { type: "Point", coordinates: [lastPoint.lon, lastPoint.lat] } } : null,
|
||||||
|
].filter(Boolean),
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const src = map.getSource("track");
|
||||||
|
if (src && src.setData) {
|
||||||
|
src.setData(geojson);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, [mapMode, positions, lastPoint]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="layout">
|
<div className="layout">
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
|
|
@ -236,7 +422,24 @@ export default function App() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="panel map-panel">
|
<section className="panel map-panel">
|
||||||
{tileUrl ? (
|
<div className="map-switch">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn ${mapMode === "raster" ? "primary" : "ghost"}`}
|
||||||
|
onClick={() => setMapMode("raster")}
|
||||||
|
>
|
||||||
|
Raster
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn ${mapMode === "vector" ? "primary" : "ghost"}`}
|
||||||
|
onClick={() => setMapMode("vector")}
|
||||||
|
>
|
||||||
|
Vector (MBTiles)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{mapMode === "raster" ? (
|
||||||
|
tileUrl ? (
|
||||||
<MapContainer center={defaultCenter} zoom={13} scrollWheelZoom className="map">
|
<MapContainer center={defaultCenter} zoom={13} scrollWheelZoom className="map">
|
||||||
<TileLayer url={tileUrl} attribution="Local tiles" tms={sliceParams.scheme === "tms"} />
|
<TileLayer url={tileUrl} attribution="Local tiles" tms={sliceParams.scheme === "tms"} />
|
||||||
{positions.length > 0 && (
|
{positions.length > 0 && (
|
||||||
|
|
@ -249,6 +452,9 @@ export default function App() {
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
) : (
|
) : (
|
||||||
<div className="empty">Нет конфигурации тайлов</div>
|
<div className="empty">Нет конфигурации тайлов</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="maplibre" ref={maplibreContainerRef} />
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
from .state import get_gnss_service, get_log_service, get_tile_service
|
from .state import get_gnss_service, get_log_service, get_tile_service, get_vector_mbtiles_service
|
||||||
|
|
||||||
__all__ = ["get_gnss_service", "get_tile_service", "get_log_service"]
|
__all__ = ["get_gnss_service", "get_tile_service", "get_log_service", "get_vector_mbtiles_service"]
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ async def get_config() -> dict:
|
||||||
return {
|
return {
|
||||||
"tiles_url": "/tiles/{z}/{x}/{y}.png",
|
"tiles_url": "/tiles/{z}/{x}/{y}.png",
|
||||||
"tiles_url_tms": "/tiles/tms/{z}/{x}/{y}.png",
|
"tiles_url_tms": "/tiles/tms/{z}/{x}/{y}.png",
|
||||||
|
"vector_tiles_url": "/tiles/vector/{z}/{x}/{y}.pbf",
|
||||||
|
"vector_metadata_url": "/tiles/vector/metadata",
|
||||||
"ws_url": "/ws/track",
|
"ws_url": "/ws/track",
|
||||||
"gnss_source": config.GNSS_SOURCE,
|
"gnss_source": config.GNSS_SOURCE,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
gnss/api/routes/vector_tiles.py
Normal file
55
gnss/api/routes/vector_tiles.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Response
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from ..dependencies import get_vector_mbtiles_service
|
||||||
|
from ...services.vector_mbtiles import VectorMBTilesError, VectorMBTilesService
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(tags=["vector-tiles"])
|
||||||
|
|
||||||
|
|
||||||
|
class ActivateVectorMBTiles(BaseModel):
|
||||||
|
path: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/tiles/vector/activate")
|
||||||
|
async def activate_vector_mbtiles(
|
||||||
|
payload: ActivateVectorMBTiles,
|
||||||
|
service: VectorMBTilesService = Depends(get_vector_mbtiles_service),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
info = service.activate(Path(payload.path))
|
||||||
|
except VectorMBTilesError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
|
return {"status": "ok", "path": str(info.path), "format": info.metadata.get("format")}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tiles/vector/metadata")
|
||||||
|
async def vector_metadata(service: VectorMBTilesService = Depends(get_vector_mbtiles_service)):
|
||||||
|
try:
|
||||||
|
return service.get_metadata()
|
||||||
|
except VectorMBTilesError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tiles/vector/{z}/{x}/{y}.pbf")
|
||||||
|
async def vector_tile(
|
||||||
|
z: int,
|
||||||
|
x: int,
|
||||||
|
y: int,
|
||||||
|
service: VectorMBTilesService = Depends(get_vector_mbtiles_service),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
data = service.get_tile(z=z, x=x, y_xyz=y)
|
||||||
|
except KeyError:
|
||||||
|
raise HTTPException(status_code=404, detail="tile not found")
|
||||||
|
except VectorMBTilesError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
if len(data) >= 2 and data[0] == 0x1F and data[1] == 0x8B:
|
||||||
|
headers["Content-Encoding"] = "gzip"
|
||||||
|
return Response(content=data, media_type="application/x-protobuf", headers=headers)
|
||||||
|
|
||||||
|
|
@ -3,17 +3,22 @@ from typing import Optional
|
||||||
from ..services.gnss import GNSSService
|
from ..services.gnss import GNSSService
|
||||||
from ..services.logs import LogService
|
from ..services.logs import LogService
|
||||||
from ..services.tiles import TileService
|
from ..services.tiles import TileService
|
||||||
|
from ..services.vector_mbtiles import VectorMBTilesService
|
||||||
|
|
||||||
_gnss_service: Optional[GNSSService] = None
|
_gnss_service: Optional[GNSSService] = None
|
||||||
_tile_service: Optional[TileService] = None
|
_tile_service: Optional[TileService] = None
|
||||||
_log_service: Optional[LogService] = None
|
_log_service: Optional[LogService] = None
|
||||||
|
_vector_mbtiles_service: Optional[VectorMBTilesService] = None
|
||||||
|
|
||||||
|
|
||||||
def set_services(gnss: GNSSService, tiles: TileService, logger: LogService) -> None:
|
def set_services(
|
||||||
global _gnss_service, _tile_service, _log_service
|
gnss: GNSSService, tiles: TileService, logger: LogService, vector_mbtiles: VectorMBTilesService
|
||||||
|
) -> None:
|
||||||
|
global _gnss_service, _tile_service, _log_service, _vector_mbtiles_service
|
||||||
_gnss_service = gnss
|
_gnss_service = gnss
|
||||||
_tile_service = tiles
|
_tile_service = tiles
|
||||||
_log_service = logger
|
_log_service = logger
|
||||||
|
_vector_mbtiles_service = vector_mbtiles
|
||||||
|
|
||||||
|
|
||||||
def get_gnss_service() -> GNSSService:
|
def get_gnss_service() -> GNSSService:
|
||||||
|
|
@ -32,3 +37,9 @@ def get_log_service() -> LogService:
|
||||||
if _log_service is None:
|
if _log_service is None:
|
||||||
raise RuntimeError("Log service is not initialized")
|
raise RuntimeError("Log service is not initialized")
|
||||||
return _log_service
|
return _log_service
|
||||||
|
|
||||||
|
|
||||||
|
def get_vector_mbtiles_service() -> VectorMBTilesService:
|
||||||
|
if _vector_mbtiles_service is None:
|
||||||
|
raise RuntimeError("Vector MBTiles service is not initialized")
|
||||||
|
return _vector_mbtiles_service
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from . import config
|
from . import config
|
||||||
from .api import state
|
from .api import state
|
||||||
from .api.routes import config_route, health, logs, tiles, ws
|
from .api.routes import config_route, health, logs, tiles, vector_tiles, ws
|
||||||
from .services.gnss import GNSSService, SerialNMEASource, SimulatedPathSource
|
from .services.gnss import GNSSService, SerialNMEASource, SimulatedPathSource
|
||||||
from .services.logs import LogService
|
from .services.logs import LogService
|
||||||
from .services.tiles import TileService
|
from .services.tiles import TileService
|
||||||
|
from .services.vector_mbtiles import VectorMBTilesService
|
||||||
|
|
||||||
logger = logging.getLogger("gnss")
|
logger = logging.getLogger("gnss")
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
@ -37,8 +38,9 @@ tiles_dir = Path(config.TILES_DIR)
|
||||||
placeholder_tile = Path(config.TILE_PLACEHOLDER)
|
placeholder_tile = Path(config.TILE_PLACEHOLDER)
|
||||||
tile_service = TileService(tiles_dir=tiles_dir, placeholder=placeholder_tile, upload_dir=Path(config.UPLOAD_DIR))
|
tile_service = TileService(tiles_dir=tiles_dir, placeholder=placeholder_tile, upload_dir=Path(config.UPLOAD_DIR))
|
||||||
log_service = LogService(base_dir=Path("logs"))
|
log_service = LogService(base_dir=Path("logs"))
|
||||||
|
vector_mbtiles_service = VectorMBTilesService()
|
||||||
gnss_service = create_gnss_service()
|
gnss_service = create_gnss_service()
|
||||||
state.set_services(gnss=gnss_service, tiles=tile_service, logger=log_service)
|
state.set_services(gnss=gnss_service, tiles=tile_service, logger=log_service, vector_mbtiles=vector_mbtiles_service)
|
||||||
|
|
||||||
app = FastAPI(title="GNSS Backend", version="0.2.0")
|
app = FastAPI(title="GNSS Backend", version="0.2.0")
|
||||||
|
|
||||||
|
|
@ -60,6 +62,7 @@ app.include_router(config_route.router)
|
||||||
app.include_router(tiles.router)
|
app.include_router(tiles.router)
|
||||||
app.include_router(ws.router)
|
app.include_router(ws.router)
|
||||||
app.include_router(logs.router)
|
app.include_router(logs.router)
|
||||||
|
app.include_router(vector_tiles.router)
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
|
|
||||||
106
gnss/services/vector_mbtiles.py
Normal file
106
gnss/services/vector_mbtiles.py
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import sqlite3
|
||||||
|
import threading
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class VectorMBTilesError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class VectorMBTilesInfo:
|
||||||
|
path: Path
|
||||||
|
metadata: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def _read_metadata(conn: sqlite3.Connection) -> dict[str, str]:
|
||||||
|
cur = conn.execute("SELECT name, value FROM metadata")
|
||||||
|
return {name: value for name, value in cur.fetchall()}
|
||||||
|
|
||||||
|
|
||||||
|
def _has_table(conn: sqlite3.Connection, table: str) -> bool:
|
||||||
|
cur = conn.execute(
|
||||||
|
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1",
|
||||||
|
(table,),
|
||||||
|
)
|
||||||
|
return cur.fetchone() is not None
|
||||||
|
|
||||||
|
|
||||||
|
class VectorMBTilesService:
|
||||||
|
def __init__(self):
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._active: Optional[VectorMBTilesInfo] = None
|
||||||
|
|
||||||
|
def activate(self, path: Path) -> VectorMBTilesInfo:
|
||||||
|
p = Path(path).expanduser().resolve()
|
||||||
|
if not p.exists():
|
||||||
|
raise VectorMBTilesError(f"mbtiles not found: {p}")
|
||||||
|
if p.suffix.lower() != ".mbtiles":
|
||||||
|
raise VectorMBTilesError("expected .mbtiles file")
|
||||||
|
|
||||||
|
conn = sqlite3.connect(f"file:{p}?mode=ro", uri=True)
|
||||||
|
try:
|
||||||
|
metadata = _read_metadata(conn)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
fmt = (metadata.get("format") or "").lower()
|
||||||
|
if fmt not in {"pbf", "mvt"}:
|
||||||
|
raise VectorMBTilesError(
|
||||||
|
f"mbtiles format is '{fmt or 'unknown'}'; expected vector 'pbf'/'mvt' (raster mbtiles are not supported here)"
|
||||||
|
)
|
||||||
|
|
||||||
|
info = VectorMBTilesInfo(path=p, metadata=metadata)
|
||||||
|
with self._lock:
|
||||||
|
self._active = info
|
||||||
|
return info
|
||||||
|
|
||||||
|
def get_active(self) -> VectorMBTilesInfo:
|
||||||
|
with self._lock:
|
||||||
|
if self._active is None:
|
||||||
|
raise VectorMBTilesError("no active vector mbtiles; upload/activate first")
|
||||||
|
return self._active
|
||||||
|
|
||||||
|
def get_metadata(self) -> dict[str, Any]:
|
||||||
|
info = self.get_active()
|
||||||
|
meta = dict(info.metadata)
|
||||||
|
meta["path"] = str(info.path)
|
||||||
|
return meta
|
||||||
|
|
||||||
|
def get_tile(self, z: int, x: int, y_xyz: int) -> bytes:
|
||||||
|
info = self.get_active()
|
||||||
|
if z < 0:
|
||||||
|
raise VectorMBTilesError("invalid z")
|
||||||
|
max_index = (2**z) - 1
|
||||||
|
if x < 0 or x > max_index or y_xyz < 0 or y_xyz > max_index:
|
||||||
|
raise VectorMBTilesError("invalid x/y for z")
|
||||||
|
|
||||||
|
y_tms = max_index - y_xyz
|
||||||
|
conn = sqlite3.connect(f"file:{info.path}?mode=ro", uri=True)
|
||||||
|
try:
|
||||||
|
if _has_table(conn, "tiles"):
|
||||||
|
cur = conn.execute(
|
||||||
|
"SELECT tile_data FROM tiles WHERE zoom_level=? AND tile_column=? AND tile_row=? LIMIT 1",
|
||||||
|
(z, x, y_tms),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row is None:
|
||||||
|
raise KeyError("tile not found")
|
||||||
|
return row[0]
|
||||||
|
|
||||||
|
if _has_table(conn, "map") and _has_table(conn, "images"):
|
||||||
|
cur = conn.execute(
|
||||||
|
"SELECT images.tile_data FROM map JOIN images ON map.tile_id = images.tile_id "
|
||||||
|
"WHERE map.zoom_level=? AND map.tile_column=? AND map.tile_row=? LIMIT 1",
|
||||||
|
(z, x, y_tms),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row is None:
|
||||||
|
raise KeyError("tile not found")
|
||||||
|
return row[0]
|
||||||
|
|
||||||
|
raise VectorMBTilesError("unsupported mbtiles schema (no tiles table and no map/images tables)")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue