replaced leaflet with map libre

This commit is contained in:
Vasilisk9812 2025-12-04 19:16:48 +09:00
parent ffb27c2e0a
commit 6359ccf9ee
10 changed files with 708 additions and 412 deletions

379
package-lock.json generated
View file

@ -9,18 +9,13 @@
"version": "0.0.1",
"dependencies": {
"@sveltestrap/sveltestrap": "^7.1.0",
"@types/leaflet": "^1.9.19",
"bootstrap-icons": "^1.13.1",
"chart.js": "^4.5.0",
"chartjs-adapter-luxon": "^1.3.1",
"chartjs-plugin-dragdata": "^2.3.1",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",
"leaflet-heatmap": "^1.0.0",
"leaflet-timedimension": "^1.1.1",
"leaflet-velocity": "^2.1.4",
"leaflet.heat": "^0.2.0",
"luxon": "^3.6.1",
"maplibre-gl": "^4.0.0",
"svelte5-chartjs": "^1.0.0"
},
"devDependencies": {
@ -495,6 +490,81 @@
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="
},
"node_modules/@mapbox/geojson-rewind": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
"integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
"dependencies": {
"get-stream": "^6.0.1",
"minimist": "^1.2.6"
},
"bin": {
"geojson-rewind": "geojson-rewind"
}
},
"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": "0.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
"integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ=="
},
"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=="
},
"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=="
},
"node_modules/@mapbox/vector-tile": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
"integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
"dependencies": {
"@mapbox/point-geometry": "~0.1.0"
}
},
"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==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@maplibre/maplibre-gl-style-spec": {
"version": "20.4.0",
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz",
"integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==",
"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": "^2.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/maplibre-gl-style-spec/node_modules/quickselect": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw=="
},
"node_modules/@polka/url": {
"version": "1.0.0-next.28",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
@ -887,10 +957,10 @@
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="
},
"node_modules/@types/leaflet": {
"version": "1.9.19",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.19.tgz",
"integrity": "sha512-pB+n2daHcZPF2FDaWa+6B0a0mSDf4dPU35y5iTXsx7x/PzzshiX5atYiS1jlBn43X7XvM8AP+AB26lnSk0J4GA==",
"node_modules/@types/geojson-vt": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
"integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
"dependencies": {
"@types/geojson": "*"
}
@ -901,6 +971,34 @@
"integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==",
"dev": true
},
"node_modules/@types/mapbox__point-geometry": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
"integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA=="
},
"node_modules/@types/mapbox__vector-tile": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
"integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
"dependencies": {
"@types/geojson": "*",
"@types/mapbox__point-geometry": "*",
"@types/pbf": "*"
}
},
"node_modules/@types/pbf": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA=="
},
"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==",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@vincjo/datatables": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@vincjo/datatables/-/datatables-2.5.0.tgz",
@ -1076,6 +1174,11 @@
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
"dev": true
},
"node_modules/earcut": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
"integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ=="
},
"node_modules/esbuild": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
@ -1157,10 +1260,58 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/heatmap.js": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/heatmap.js/-/heatmap.js-2.0.5.tgz",
"integrity": "sha512-CG2gYFP5Cv9IQCXEg3ZRxnJDyAilhWnQlAuHYGuWVzv6mFtQelS1bR9iN80IyDmFECbFPbg6I0LR5uAFHgCthw=="
"node_modules/geojson-vt": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz",
"integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A=="
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"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=="
},
"node_modules/global-prefix": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz",
"integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==",
"dependencies": {
"ini": "^4.1.3",
"kind-of": "^6.0.3",
"which": "^4.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/import-meta-resolve": {
"version": "4.1.0",
@ -1172,6 +1323,14 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/ini": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
"integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/is-reference": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
@ -1180,10 +1339,13 @@
"@types/estree": "^1.0.6"
}
},
"node_modules/iso8601-js-period": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/iso8601-js-period/-/iso8601-js-period-0.2.1.tgz",
"integrity": "sha512-iDyz2TQFBd5WhCZjruOwHj01JkQGu7YbVLCVdpA7lCGEcBzE3ffCPAhLh/M8TAp//kCixPpYN4XU54WHCxvD2Q=="
"node_modules/isexe": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
"engines": {
"node": ">=16"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
@ -1193,6 +1355,24 @@
"node": ">=14"
}
},
"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=="
},
"node_modules/kdbush": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@ -1202,39 +1382,6 @@
"node": ">=6"
}
},
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="
},
"node_modules/leaflet-heatmap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/leaflet-heatmap/-/leaflet-heatmap-1.0.0.tgz",
"integrity": "sha512-WP/emZYwjWaEnWMcE2dftuJvtjp53zmJcHtVTHUqPN7AQEowHxDTLH5j1BJjE4uL1K5dJclBLX4oLpnOGS/qTw==",
"dependencies": {
"heatmap.js": "*",
"leaflet": "*"
}
},
"node_modules/leaflet-timedimension": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/leaflet-timedimension/-/leaflet-timedimension-1.1.1.tgz",
"integrity": "sha512-ejXldN94veRsWka1vpC+4rbH+2+3d3ztn2xYx4jcXtjYDrKC/sNnoqCmyH2UEYIy51PI2851aI2k8uGdOEbhlw==",
"dependencies": {
"iso8601-js-period": "^0.2.1",
"leaflet": "~0.7.4 || ~1"
}
},
"node_modules/leaflet-velocity": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/leaflet-velocity/-/leaflet-velocity-2.1.4.tgz",
"integrity": "sha512-uTmSb2/Kn28S0itlmJBMy2ZRKsisWUr2wm9rtkKXjpq9Sai7tqKdTRHKfLgTOgEdWFf5Ctt2bQoB7kb50qC7eg=="
},
"node_modules/leaflet.heat": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/leaflet.heat/-/leaflet.heat-0.2.0.tgz",
"integrity": "sha512-Cd5PbAA/rX3X3XKxfDoUGi9qp78FyhWYurFg3nsfhntcM/MCNK08pRkf4iEenO1KNqwVPKCmkyktjW3UD+h9bQ=="
},
"node_modules/locate-character": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
@ -1256,6 +1403,54 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/maplibre-gl": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz",
"integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==",
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
"@mapbox/point-geometry": "^0.1.0",
"@mapbox/tiny-sdf": "^2.0.6",
"@mapbox/unitbezier": "^0.0.1",
"@mapbox/vector-tile": "^1.3.1",
"@mapbox/whoots-js": "^3.1.0",
"@maplibre/maplibre-gl-style-spec": "^20.3.1",
"@types/geojson": "^7946.0.14",
"@types/geojson-vt": "3.2.5",
"@types/mapbox__point-geometry": "^0.1.4",
"@types/mapbox__vector-tile": "^1.3.4",
"@types/pbf": "^3.0.5",
"@types/supercluster": "^7.1.3",
"earcut": "^3.0.0",
"geojson-vt": "^4.0.2",
"gl-matrix": "^3.4.3",
"global-prefix": "^4.0.0",
"kdbush": "^4.0.2",
"murmurhash-js": "^1.0.0",
"pbf": "^3.3.0",
"potpack": "^2.0.0",
"quickselect": "^3.0.0",
"supercluster": "^8.0.1",
"tinyqueue": "^3.0.0",
"vt-pbf": "^3.1.3"
},
"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==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@ -1280,6 +1475,11 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"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=="
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -1298,6 +1498,18 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/pbf": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
"integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
"dependencies": {
"ieee754": "^1.1.12",
"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",
@ -1344,6 +1556,21 @@
"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=="
},
"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=="
},
"node_modules/quickselect": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@ -1357,6 +1584,14 @@
"url": "https://paulmillr.com/funding/"
}
},
"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==",
"dependencies": {
"protocol-buffers-schema": "^3.3.1"
}
},
"node_modules/rollup": {
"version": "4.39.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.39.0.tgz",
@ -1396,6 +1631,11 @@
"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=="
},
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
@ -1437,6 +1677,14 @@
"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==",
"dependencies": {
"kdbush": "^4.0.2"
}
},
"node_modules/svelte": {
"version": "5.34.8",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.34.8.tgz",
@ -1509,6 +1757,11 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyqueue": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="
},
"node_modules/totalist": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
@ -1619,6 +1872,30 @@
}
}
},
"node_modules/vt-pbf": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
"integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
"dependencies": {
"@mapbox/point-geometry": "0.1.0",
"@mapbox/vector-tile": "^1.3.1",
"pbf": "^3.2.1"
}
},
"node_modules/which": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
"integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
"dependencies": {
"isexe": "^3.1.1"
},
"bin": {
"node-which": "bin/which.js"
},
"engines": {
"node": "^16.13.0 || >=18.0.0"
}
},
"node_modules/zimmerframe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",

View file

@ -24,18 +24,13 @@
},
"dependencies": {
"@sveltestrap/sveltestrap": "^7.1.0",
"@types/leaflet": "^1.9.19",
"bootstrap-icons": "^1.13.1",
"chart.js": "^4.5.0",
"chartjs-adapter-luxon": "^1.3.1",
"chartjs-plugin-dragdata": "^2.3.1",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",
"leaflet-heatmap": "^1.0.0",
"leaflet-timedimension": "^1.1.1",
"leaflet-velocity": "^2.1.4",
"leaflet.heat": "^0.2.0",
"luxon": "^3.6.1",
"maplibre-gl": "^4.0.0",
"svelte5-chartjs": "^1.0.0"
}
}

View file

@ -5,7 +5,6 @@
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="stylesheet" href="%sveltekit.assets%/css/bootstrap.min.css">
<link rel="stylesheet" href="%sveltekit.assets%/css/bootstrap-icons.css" />
<link rel="stylesheet" href="%sveltekit.assets%/ext/leaflet-ruler/leaflet-ruler.css" />
<link rel="stylesheet" href="%sveltekit.assets%/css/custom.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

View file

@ -1,9 +1,8 @@
<script lang="ts">
import { onMount, createEventDispatcher } from "svelte";
import * as L from "leaflet";
import { ruler, Ruler } from "$lib/ext/leaflet-ruler/leaflet-ruler";
import type { Map as LeafletMap, LayerGroup } from "leaflet";
import "leaflet/dist/leaflet.css";
import maplibregl from "maplibre-gl";
import type { Map as MapLibreMap, Marker, LngLatBoundsLike } from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import WindVisualization from "$lib/components/WindVisualisation.svelte";
import { distHaversine } from "$lib/mathutil";
import type { Prediction, Telemetry } from "$lib/types";
@ -11,9 +10,9 @@
export let mode: "prediction" | "telemetry" = "prediction";
export let data: Prediction | Telemetry | null = null;
let map: LeafletMap;
let map: MapLibreMap;
let mapContainer: HTMLDivElement;
let plotLayerGroup: LayerGroup;
let markers: Marker[] = [];
let mouseLat = 0;
let mouseLng = 0;
let isSelecting = false;
@ -25,30 +24,50 @@
onMount(async () => {
if (!mapContainer) return;
map = L.map(mapContainer, { zoomControl: false }).setView([51.505, -0.09], 13);
L.control.zoom({ position: "bottomleft" }).addTo(map);
map = new maplibregl.Map({
container: mapContainer,
style: {
version: 8,
sources: {
osm: {
type: "raster",
tiles: ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"],
tileSize: 256,
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
},
},
layers: [
{
id: "osm",
type: "raster",
source: "osm",
minzoom: 0,
maxzoom: 19,
},
],
},
center: [-0.09, 51.505],
zoom: 13,
});
plotLayerGroup = L.layerGroup().addTo(map);
// Add navigation control (zoom buttons)
map.addControl(new maplibregl.NavigationControl(), "bottom-left");
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
ruler({
position: "bottomright",
}).addTo(map);
// Add scale control
map.addControl(new maplibregl.ScaleControl({ maxWidth: 100, unit: "metric" }), "bottom-right");
const response = await fetch("src/routes/testVelo.json");
windData = await response.json();
map.on("mousemove", (e: any) => {
mouseLat = e.latlng.lat;
mouseLng = e.latlng.lng;
map.on("mousemove", (e: maplibregl.MapMouseEvent) => {
mouseLat = e.lngLat.lat;
mouseLng = e.lngLat.lng;
});
map.on("click", (e: any) => {
map.on("click", (e: maplibregl.MapMouseEvent) => {
if (isSelecting) {
dispatch("coordinatesSelected", { lat: e.latlng.lat, lng: e.latlng.lng });
dispatch("coordinatesSelected", { lat: e.lngLat.lat, lng: e.lngLat.lng });
stopSelection();
}
});
@ -79,15 +98,64 @@
};
export const clearMapLayers = () => {
plotLayerGroup?.clearLayers();
// Remove all markers
markers.forEach((marker) => marker.remove());
markers = [];
// Remove all layers and sources related to flight paths
if (map && map.getLayer("flight-path")) {
map.removeLayer("flight-path");
}
if (map && map.getSource("flight-path")) {
map.removeSource("flight-path");
}
if (map && map.getLayer("telemetry-path")) {
map.removeLayer("telemetry-path");
}
if (map && map.getSource("telemetry-path")) {
map.removeSource("telemetry-path");
}
};
const launchIcon = L.icon({ iconUrl: "target-blue.png", iconSize: [10, 10], iconAnchor: [5, 5] });
const landIcon = L.icon({ iconUrl: "target-red.png", iconSize: [10, 10], iconAnchor: [5, 5] });
const burstIcon = L.icon({ iconUrl: "pop-marker.png", iconSize: [16, 16], iconAnchor: [8, 8] });
const telemetryIcon = L.icon({ iconUrl: "marker-sm-red.png", iconSize: [10, 10], iconAnchor: [5, 5] });
const createMarker = (
lng: number,
lat: number,
color: string,
iconUrl: string,
title: string,
) => {
const el = document.createElement("div");
el.className = "custom-marker";
el.style.backgroundImage = `url(${iconUrl})`;
el.style.width = "10px";
el.style.height = "10px";
el.style.backgroundSize = "100%";
el.title = title;
const marker = new maplibregl.Marker({ element: el }).setLngLat([lng, lat]).addTo(map);
markers.push(marker);
return marker;
};
const createBurstMarker = (lng: number, lat: number, title: string) => {
const el = document.createElement("div");
el.className = "custom-marker";
el.style.backgroundImage = `url(pop-marker.png)`;
el.style.width = "16px";
el.style.height = "16px";
el.style.backgroundSize = "100%";
el.title = title;
const marker = new maplibregl.Marker({ element: el }).setLngLat([lng, lat]).addTo(map);
markers.push(marker);
return marker;
};
const plotPrediction = (prediction: Prediction) => {
clearMapLayers();
const { launch, landing, burst, flight_path, flight_time } = prediction;
const range = distHaversine(launch.latlng, landing.latlng, 1);
@ -97,43 +165,154 @@
.padStart(2, "0");
const flighttime = `${f_hours}hr${f_minutes}`;
L.marker(launch.latlng, { title: `Launch`, icon: launchIcon }).addTo(plotLayerGroup);
L.marker(landing.latlng, { title: `Landing`, icon: landIcon }).addTo(plotLayerGroup);
L.marker(burst.latlng, { title: `Burst`, icon: burstIcon }).addTo(plotLayerGroup);
// Helper to extract lat/lng from either format
const getLat = (latlng: any) => (Array.isArray(latlng) ? latlng[0] : latlng.lat);
const getLng = (latlng: any) => (Array.isArray(latlng) ? latlng[1] : latlng.lng);
L.polyline(flight_path, { weight: 3, color: "#000000" }).addTo(plotLayerGroup);
// Create markers (MapLibre uses [lng, lat] order)
createMarker(getLng(launch.latlng), getLat(launch.latlng), "#0000ff", "target-blue.png", "Launch");
createMarker(getLng(landing.latlng), getLat(landing.latlng), "#ff0000", "target-red.png", "Landing");
createBurstMarker(getLng(burst.latlng), getLat(burst.latlng), "Burst");
map?.fitBounds(L.latLngBounds(flight_path));
// Add flight path as a line (convert [lat, lng] to [lng, lat] for MapLibre)
const coordinates = flight_path.map((coord) => {
if (Array.isArray(coord)) {
return [coord[1], coord[0]]; // [lat, lng, alt?] -> [lng, lat]
} else {
return [coord.lng, coord.lat]; // {lat, lng} -> [lng, lat]
}
});
map.addSource("flight-path", {
type: "geojson",
data: {
type: "Feature",
properties: {},
geometry: {
type: "LineString",
coordinates: coordinates,
},
},
});
map.addLayer({
id: "flight-path",
type: "line",
source: "flight-path",
layout: {
"line-join": "round",
"line-cap": "round",
},
paint: {
"line-color": "#000000",
"line-width": 3,
},
});
// Fit bounds to show entire path
const bounds = coordinates.reduce(
(bounds, coord) => {
return bounds.extend(coord as [number, number]);
},
new maplibregl.LngLatBounds(coordinates[0] as [number, number], coordinates[0] as [number, number]),
);
map.fitBounds(bounds as LngLatBoundsLike, { padding: 50 });
};
const plotTelemetry = (telemetry: Telemetry) => {
L.marker(telemetry.launch.latlng, { title: `Launch`, icon: launchIcon }).addTo(plotLayerGroup);
clearMapLayers();
// Helper to extract lat/lng from either format
const getLat = (latlng: any) => (Array.isArray(latlng) ? latlng[0] : latlng.lat);
const getLng = (latlng: any) => (Array.isArray(latlng) ? latlng[1] : latlng.lng);
// Launch marker (MapLibre uses [lng, lat] order)
createMarker(
getLng(telemetry.launch.latlng),
getLat(telemetry.launch.latlng),
"#0000ff",
"target-blue.png",
"Launch",
);
// Telemetry point markers with popups
telemetry.datapoints.forEach((point) => {
L.marker([point.latitude, point.longitude], {
title: `Telemetry at ${point.datetime}`,
icon: telemetryIcon,
})
.bindPopup(
`<b>Telemetry Point</b><br>Lat: ${point.latitude.toFixed(6)}<br>Lon: ${point.longitude.toFixed(6)}`,
)
.addTo(plotLayerGroup);
const el = document.createElement("div");
el.className = "custom-marker";
el.style.backgroundImage = `url(marker-sm-red.png)`;
el.style.width = "10px";
el.style.height = "10px";
el.style.backgroundSize = "100%";
const popup = new maplibregl.Popup({ offset: 25 }).setHTML(
`<b>Telemetry Point</b><br>Lat: ${point.latitude.toFixed(6)}<br>Lon: ${point.longitude.toFixed(6)}`,
);
const marker = new maplibregl.Marker({ element: el })
.setLngLat([point.longitude, point.latitude])
.setPopup(popup)
.addTo(map);
markers.push(marker);
});
L.polyline(telemetry.flight_path, { weight: 3, color: "#000000" }).addTo(plotLayerGroup);
// Add flight path as a line (convert [lat, lng] to [lng, lat] for MapLibre)
const coordinates = telemetry.flight_path.map((coord) => {
if (Array.isArray(coord)) {
return [coord[1], coord[0]]; // [lat, lng, alt?] -> [lng, lat]
} else {
return [coord.lng, coord.lat]; // {lat, lng} -> [lng, lat]
}
});
map?.fitBounds(L.latLngBounds(telemetry.flight_path));
map.addSource("telemetry-path", {
type: "geojson",
data: {
type: "Feature",
properties: {},
geometry: {
type: "LineString",
coordinates: coordinates,
},
},
});
map.addLayer({
id: "telemetry-path",
type: "line",
source: "telemetry-path",
layout: {
"line-join": "round",
"line-cap": "round",
},
paint: {
"line-color": "#000000",
"line-width": 3,
},
});
// Fit bounds to show entire path
const bounds = coordinates.reduce(
(bounds, coord) => {
return bounds.extend(coord as [number, number]);
},
new maplibregl.LngLatBounds(coordinates[0] as [number, number], coordinates[0] as [number, number]),
);
map.fitBounds(bounds as LngLatBoundsLike, { padding: 50 });
};
export const panTo = (lat: number, lng: number) => {
if (map) {
map.setView([lat, lng], map.getZoom());
map.setCenter([lng, lat]);
}
};
export const zoomTo = (lat: number, lng: number, zoomLevel: number) => {
if (map) {
map.setView([lat, lng], zoomLevel);
map.setCenter([lng, lat]);
map.setZoom(zoomLevel);
}
};
@ -156,3 +335,31 @@
<WindVisualization {map} {windData} />
{/if}
</div>
<!-- <style>
.map-container {
position: relative;
width: 100%;
height: 100%;
}
.coordinates-display {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.9);
border-radius: 4px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.card-text {
margin: 0;
font-size: 12px;
}
:global(.custom-marker) {
cursor: pointer;
}
</style> -->

View file

@ -1,289 +1,129 @@
<script>
import { onMount, onDestroy } from "svelte";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet-velocity/dist/leaflet-velocity.css";
import "leaflet-velocity/dist/leaflet-velocity";
import "leaflet.heat";
import "leaflet-timedimension";
export let map; // принимаем карту из родительского компонента
export let map; // MapLibre map instance from parent component
export let windData;
let timeDimension;
let timeDimensionControl;
let velocityLayer;
let heatLayer;
let legend;
// State for layer toggles
let showHeatmap = false;
let showVectors = false;
// Состояние переключателей
let showHeatmap = true;
let showVectors = true;
let layerControl;
// Преобразование testVelo.json в формат timeData
const prepareTimeData = (windData) => {
if (!windData || windData.length < 2) return {};
// Используем дату из header или текущую дату, если не указана
const refTime = windData[0]?.header?.refTime || new Date().toISOString();
return {
[refTime]: {
u: windData[0].data, // U-компонента (первый объект в массиве)
v: windData[1].data, // V-компонента (второй объект)
},
};
};
// Функция для нормализации данных тепловой карты
const prepareHeatData = (windData) => {
if (!windData || windData.length < 2) {
console.warn("Invalid wind data structure");
return [];
}
// Получаем U и V компоненты
const uComponent = windData.find((item) => item.header.parameterNumber === 2);
const vComponent = windData.find((item) => item.header.parameterNumber === 3);
if (!uComponent || !vComponent) {
console.warn("Missing wind components");
return [];
}
const header = uComponent.header; // Используем header из U компоненты
const { lo1, la1, dx, dy, nx, ny } = header;
const heatData = [];
let maxSpeed = 0;
// Проверяем совпадение размеров данных
if (uComponent.data.length !== vComponent.data.length) {
console.warn("U and V components have different lengths");
return [];
}
// Собираем данные и находим максимальную скорость
for (let i = 0; i < uComponent.data.length; i++) {
const u = uComponent.data[i];
const v = vComponent.data[i];
const speed = Math.sqrt(u * u + v * v);
if (!isNaN(speed)) {
// Вычисляем координаты для текущей точки
const y = Math.floor(i / nx);
const x = i % nx;
let lat = la1 - y * dy;
let lng = lo1 + x * dx;
if (lng >= 180) lng -= 360;
heatData.push([lat, lng, speed]);
maxSpeed = Math.max(maxSpeed, speed);
}
}
console.log(`Prepared heat data: ${heatData.length} points, max speed: ${maxSpeed}`);
// Нормализуем значения интенсивности от 0 до 1
if (maxSpeed > 0) {
return heatData.map(([lat, lng, intensity]) => [lat, lng, intensity / maxSpeed]);
}
return heatData;
};
// Создание тепловой карты
const createHeatLayer = (data) => {
if (!data || data.length === 0) {
console.warn("No valid heat data provided");
return null;
}
try {
return L.heatLayer(data, {
radius: 8, // Увеличьте радиус для глобальной карты
blur: 20,
// maxZoom: 10,
minOpacity: 0.7,
gradient: {
0.1: "blue",
0.3: "cyan",
0.5: "lime",
0.7: "yellow",
1.0: "red",
},
});
} catch (e) {
console.error("Failed to create heat layer:", e);
return null;
}
};
// Обновление слоев
const updateLayers = () => {
if (!map || !windData) return;
// Удаляем старые слои
if (velocityLayer) map.removeLayer(velocityLayer);
if (heatLayer) map.removeLayer(heatLayer);
if (legend) map.removeControl(legend);
// Создаем слой векторов ветра
if (showVectors) {
velocityLayer = L.velocityLayer({
displayValues: true,
displayOptions: {
velocityType: "Wind Speed",
position: "bottomright",
emptyString: "No wind data",
},
data: windData,
}).addTo(map);
}
// Создаем тепловую карту
if (showHeatmap) {
const heatData = prepareHeatData(windData);
heatLayer = createHeatLayer(heatData);
if (heatLayer) {
heatLayer.addTo(map);
createLegend(Math.max(...heatData.map((point) => point[2])));
}
}
// Обновляем контроль слоев
updateLayerControl();
};
const updateLayerControl = () => {
if (layerControl) {
map.removeControl(layerControl);
}
const overlays = {};
if (velocityLayer) {
overlays["Векторы ветра"] = velocityLayer;
}
if (heatLayer) {
overlays["Тепловая карта"] = heatLayer;
}
layerControl = L.control
.layers(null, overlays, {
collapsed: false,
position: "topright",
})
.addTo(map);
};
// Создание легенды с учетом максимальной скорости
const createLegend = (maxSpeed) => {
if (!map) return;
legend = L.control({ position: "bottomright" });
legend.onAdd = () => {
const div = L.DomUtil.create("div", "wind-heat-legend");
div.innerHTML = `
<h4>Wind Speed (m/s)</h4>
<div class="legend-scale">
<div class="legend-color" style="background: #0000FF;"></div>
<div class="legend-color" style="background: #00FFFF;"></div>
<div class="legend-color" style="background: #00FF00;"></div>
<div class="legend-color" style="background: #FFFF00;"></div>
<div class="legend-color" style="background: #FF0000;"></div>
</div>
<div class="legend-labels">
<span>0</span>
<span>${(maxSpeed * 0.25).toFixed(1)}</span>
<span>${(maxSpeed * 0.5).toFixed(1)}</span>
<span>${(maxSpeed * 0.75).toFixed(1)}</span>
<span>${maxSpeed.toFixed(1)}</span>
</div>
`;
return div;
};
legend.addTo(map);
};
// Note: This is a placeholder implementation
// MapLibre GL JS does not have direct equivalents for leaflet-velocity and leaflet.heat
// These features would need to be implemented using:
// 1. Custom WebGL layers for wind visualization
// 2. Heatmap layers using MapLibre's native heatmap style
// 3. Third-party libraries like deck.gl or mapbox-gl plugins
onMount(() => {
if (!map) return;
if (!map || !windData) return;
// 1. Настройка TimeDimension (добавьте эти строки в начале)
// L.TimeDimension.Util.setProxy('https://your-proxy.com/?url='); // Для загрузки больших данных
L.TimeDimension.Util.setCacheLimit(10); // Лимит кэшированных кадров
console.log("WindVisualization mounted with MapLibre map");
console.log("Wind data available:", windData);
// 1. Подготовка данных
const timeData = prepareTimeData(windData);
const firstTime = Object.keys(timeData)[0];
// Инициализация TimeDimension
timeDimension = new L.TimeDimension({
period: "PT1H", // Интервал 1 час
timeInterval: "${firstTime}/${firstTime}",
});
// Добавляем контролы времени
timeDimensionControl = new L.Control.TimeDimension({
timeDimension,
position: "bottomleft",
// autoPlay: true,
playerOptions: {
// transitionTime: 1000,
loop: false,
minBufferReady: -1,
},
});
map.addControl(timeDimensionControl);
// 4. Создание слоев
const velocityLayer = L.timeDimension.layer
.windVelocity({
displayValues: true,
data: timeData,
displayOptions: {
velocityType: "Wind Speed",
position: "bottomleft",
},
})
.addTo(map);
// 5. Тепловая карта (адаптируйте под ваш формат)
const heatLayer = L.timeDimension.layer
.heat({
radius: 15,
data: prepareTimeHeatData(timeData),
})
.addTo(map);
// TODO: Implement wind visualization using MapLibre GL JS
// Possible approaches:
// 1. Use MapLibre's native heatmap layer type for heat visualization
// 2. Use deck.gl ScreenGridLayer or HeatmapLayer for advanced heatmaps
// 3. Use custom WebGL shaders for wind particle animation
// 4. Use mapbox-gl-wind plugin (if compatible with MapLibre)
});
onDestroy(() => {
// Clean up any layers or resources when component is destroyed
if (map) {
if (velocityLayer) map.removeLayer(velocityLayer);
if (heatLayer) map.removeLayer(heatLayer);
if (legend) map.removeControl(legend);
// Remove any added layers
console.log("WindVisualization destroyed");
}
});
// Реактивность на изменение параметров
// Reactive statement for layer updates
$: if (map && windData) {
updateLayers();
}
const updateLayers = () => {
if (!map || !windData) return;
console.log("Updating wind layers:", { showHeatmap, showVectors });
// TODO: Implement layer toggling
// This would involve adding/removing MapLibre layers based on the toggle state
};
</script>
<!--
IMPORTANT: This is a simplified placeholder implementation.
The original Leaflet-based wind visualization used these plugins:
- leaflet-velocity: For wind vector visualization
- leaflet.heat: For heatmap visualization
- leaflet-timedimension: For time-based animation
To fully implement wind visualization in MapLibre GL JS, you would need to:
1. For Wind Vectors:
- Use a custom WebGL layer with particle animation
- Or use deck.gl's ParticleLayer or FlowmapLayer
- Or port/adapt the wind-gl-core library
2. For Heatmap:
- Use MapLibre's native 'heatmap' layer type
- Convert wind data to GeoJSON point features
- Style with appropriate color gradients
3. For Time Dimension:
- Implement custom time controls
- Update data sources based on selected time
- Use requestAnimationFrame for smooth animation
Example MapLibre heatmap implementation:
map.addSource('wind-heat', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: windPoints // Array of GeoJSON point features
}
});
map.addLayer({
id: 'wind-heatmap',
type: 'heatmap',
source: 'wind-heat',
paint: {
'heatmap-weight': ['get', 'intensity'],
'heatmap-intensity': 1,
'heatmap-color': [
'interpolate',
['linear'],
['heatmap-density'],
0, 'rgba(0,0,255,0)',
0.2, 'rgb(0,0,255)',
0.4, 'rgb(0,255,255)',
0.6, 'rgb(0,255,0)',
0.8, 'rgb(255,255,0)',
1, 'rgb(255,0,0)'
],
'heatmap-radius': 20,
'heatmap-opacity': 0.7
}
});
-->
<div class="layer-controls">
<div class="control-group">
<label>
<input type="checkbox" bind:checked={showHeatmap} />
Тепловая карта
<input type="checkbox" bind:checked={showHeatmap} disabled />
Тепловая карта (TODO)
</label>
<label>
<input type="checkbox" bind:checked={showVectors} />
Векторы ветра
<input type="checkbox" bind:checked={showVectors} disabled />
Векторы ветра (TODO)
</label>
</div>
<small style="color: #666; font-size: 11px; margin-top: 8px; display: block;">
Wind visualization requires MapLibre implementation
</small>
</div>
<style>
@ -292,7 +132,7 @@
bottom: 30px;
left: 10px;
z-index: 1000;
background: rgba(255, 255, 255, 0.8);
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
@ -311,35 +151,9 @@
font-size: 14px;
cursor: pointer;
}
:global(.wind-heat-legend) {
padding: 8px 10px;
background: rgba(255, 255, 255, 0.9);
border-radius: 5px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
line-height: 1.2;
color: #333;
font-family: Arial, sans-serif;
}
:global(.wind-heat-legend h4) {
margin: 0 0 5px;
font-size: 14px;
font-weight: bold;
}
:global(legend-scale) {
display: flex;
margin-bottom: 3px;
}
:global(legend-color) {
height: 12px;
flex-grow: 1;
}
:global(.legend-labels) {
display: flex;
justify-content: space-between;
font-size: 11px;
.control-group label:has(input:disabled) {
opacity: 0.5;
cursor: not-allowed;
}
</style>

View file

@ -1,6 +1,5 @@
import { writable } from "svelte/store";
import type { LatLngExpression } from "leaflet";
import L from "leaflet";
import type { LatLngExpression } from "./types";
import { getCsrfToken } from "./auth";
import type { PredictionStage, RawPrediction, Prediction, Point } from "./types";
@ -129,7 +128,7 @@ export function parsePrediction(prediction: PredictionStage[]): Prediction {
if (lon > 180.0) {
lon -= 360.0;
}
launch.latlng = L.latLng([launchObj.latitude, lon, launchObj.altitude]);
launch.latlng = { lat: launchObj.latitude, lng: lon, alt: launchObj.altitude };
launch.datetime = new Date(launchObj.datetime);
const burstObj = descent[0];
@ -137,7 +136,7 @@ export function parsePrediction(prediction: PredictionStage[]): Prediction {
if (lon > 180.0) {
lon -= 360.0;
}
burst.latlng = L.latLng([burstObj.latitude, lon, burstObj.altitude]);
burst.latlng = { lat: burstObj.latitude, lng: lon, alt: burstObj.altitude };
burst.datetime = new Date(burstObj.datetime);
const landingObj = descent[descent.length - 1];
@ -145,7 +144,7 @@ export function parsePrediction(prediction: PredictionStage[]): Prediction {
if (lon > 180.0) {
lon -= 360.0;
}
landing.latlng = L.latLng([landingObj.latitude, lon, landingObj.altitude]);
landing.latlng = { lat: landingObj.latitude, lng: lon, alt: landingObj.altitude };
landing.datetime = new Date(landingObj.datetime);
const profile = prediction[1].stage === "descent" ? "standard_profile" : "float_profile";

View file

@ -1,5 +1,4 @@
import { writable } from "svelte/store"
import L from "leaflet";
import type { TelemetryPoint, Telemetry } from "./types";
@ -11,7 +10,7 @@ export function parseTelemetry(telemetry: TelemetryPoint[]): Telemetry {
]);
const launch = {
latlng: L.latLng(telemetry[0].latitude, telemetry[0].longitude),
latlng: { lat: telemetry[0].latitude, lng: telemetry[0].longitude },
datetime: new Date(telemetry[0].datetime)
};

View file

@ -1,4 +1,10 @@
import type { LatLngExpression, LatLngLiteral } from "leaflet";
// Define coordinate types (previously from Leaflet)
export type LatLngTuple = [number, number];
export interface LatLngLiteral {
lat: number;
lng: number;
}
export type LatLngExpression = LatLngTuple | LatLngLiteral;
export const PROFILE_MAP = {
"Обычный": "standard_profile",

View file

@ -10,7 +10,6 @@
import { PredictionStore } from "$lib/stores";
import { addToast, removeToast } from "$lib/components/Toast.svelte";
import ToastContainer from '$lib/components/Toast.svelte';
import L, { point } from "leaflet";
let map: Map | null = null;
let panelContainer: PanelContainer | null = null;
@ -30,8 +29,13 @@
if (panelContainer) {
let element = panelContainer.getElement();
if (!element) return;
L.DomEvent.disableClickPropagation(element);
L.DomEvent.disableScrollPropagation(element);
// Disable click and scroll propagation to prevent map interaction
element.addEventListener('click', (e) => e.stopPropagation());
element.addEventListener('dblclick', (e) => e.stopPropagation());
element.addEventListener('mousedown', (e) => e.stopPropagation());
element.addEventListener('touchstart', (e) => e.stopPropagation());
element.addEventListener('wheel', (e) => e.stopPropagation());
}
});

View file

@ -86,25 +86,17 @@
z-index: 1001;
}
.leaflet-bar {
/* MapLibre control styles */
.maplibregl-ctrl-group {
border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;
border-radius: var(--bs-border-radius) !important;
}
.leaflet-tooltip-top::before {
.maplibregl-popup-tip {
border-top-color: var(--bs-border-color) !important;
}
.leaflet-tooltip-bottom::before {
border-bottom-color: var(--bs-border-color) !important;
}
.leaflet-tooltip-left::before {
border-left-color: var(--bs-border-color) !important;
}
.leaflet-tooltip-right::before {
border-right-color: var(--bs-border-color) !important;
}
.leaflet-tooltip {
.maplibregl-popup-content {
background-color: var(--bs-body-bg) !important;
border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;
border-radius: var(--bs-border-radius) !important;
@ -112,6 +104,10 @@
box-shadow: none !important;
}
.maplibregl-popup-close-button {
color: var(--bs-body-color);
}
.modal-backdrop {
opacity: var(--bs-backdrop-opacity) !important;
}