Merge branch 'components' of https://git.intra.yksa.space/mikhailov.aa/leaflet_svelte into components
This commit is contained in:
commit
3be5d6c515
13 changed files with 1131 additions and 679 deletions
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(cat:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
454
package-lock.json
generated
454
package-lock.json
generated
|
|
@ -8,19 +8,15 @@
|
|||
"name": "app4",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@sakitam-gis/maplibre-wind": "^2.0.3",
|
||||
"@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 +491,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",
|
||||
|
|
@ -770,6 +841,47 @@
|
|||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@sakitam-gis/maplibre-wind": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@sakitam-gis/maplibre-wind/-/maplibre-wind-2.0.3.tgz",
|
||||
"integrity": "sha512-KeBlh2EJ13+MsFck2l8sKXKz/ogezvnontarSCTmpfzNzB3b9nA+ydzXLFfqqUMrnZwEhsuEG+pKTFMyPv1shg==",
|
||||
"dependencies": {
|
||||
"@mapbox/geojson-rewind": "^0.5.2",
|
||||
"@sakitam-gis/rbush": "3.1.2",
|
||||
"@sakitam-gis/vis-engine": "^1.5.3",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"wind-gl-core": "2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"maplibre-gl": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sakitam-gis/rbush": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@sakitam-gis/rbush/-/rbush-3.1.2.tgz",
|
||||
"integrity": "sha512-pnNaLnxFBBMnHgGjFX+h2jkpZQg2vXquvDv1BUKfU72uJzJqPcS8smaLydJqcbXp8p7GruoPrQzUpqYG0MYyIg==",
|
||||
"dependencies": {
|
||||
"quickselect": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sakitam-gis/rbush/node_modules/quickselect": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
|
||||
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw=="
|
||||
},
|
||||
"node_modules/@sakitam-gis/vis-engine": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@sakitam-gis/vis-engine/-/vis-engine-1.5.3.tgz",
|
||||
"integrity": "sha512-IpuZwi0XRflJiP1mNTwOSjlAJZRCczOuVh6s/feVOpXctiAoSWrAuhK0HVITLpCWAQF1bN6CRKA3LW0z1nCr0g==",
|
||||
"dependencies": {
|
||||
"colord": "^2.9.3",
|
||||
"gl-matrix": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.18.1",
|
||||
"npm": ">= 6.14.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/acorn-typescript": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
|
||||
|
|
@ -887,10 +999,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 +1013,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",
|
||||
|
|
@ -1007,6 +1147,11 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/colord": {
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
|
||||
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
|
|
@ -1076,6 +1221,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",
|
||||
|
|
@ -1129,6 +1279,11 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
}
|
||||
},
|
||||
"node_modules/exifr": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz",
|
||||
"integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw=="
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
|
|
@ -1157,10 +1312,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 +1375,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 +1391,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 +1407,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 +1434,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 +1455,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 +1527,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 +1550,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 +1608,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 +1636,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 +1683,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 +1729,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 +1809,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 +1924,53 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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/wind-gl-core": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wind-gl-core/-/wind-gl-core-2.0.2.tgz",
|
||||
"integrity": "sha512-EUnUQsbucaPCFns7p6BlPE5xXiXQpb2hXMmE4t/FG4W+rKlYHjtIMWzM0wAD4M6g4Wg6JzSft7SGocPJAqjssA==",
|
||||
"dependencies": {
|
||||
"@sakitam-gis/vis-engine": "^1.5.3",
|
||||
"earcut": "^2.2.4",
|
||||
"wind-gl-worker": "2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/wind-gl-core/node_modules/earcut": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
|
||||
"integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ=="
|
||||
},
|
||||
"node_modules/wind-gl-worker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wind-gl-worker/-/wind-gl-worker-2.0.2.tgz",
|
||||
"integrity": "sha512-uEMHjQtX5w+Kn+MT0RWGyYYqou6brZMe9BMOYAqoJh74tKGpuBx0+i+4J2XppAZmD8r7KYn/UvhjGHfpOq0UlQ==",
|
||||
"dependencies": {
|
||||
"exifr": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/zimmerframe": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
|
||||
|
|
|
|||
|
|
@ -23,19 +23,15 @@
|
|||
"vite": "^6.2.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sakitam-gis/maplibre-wind": "^2.0.3",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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,10 @@
|
|||
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 animatedMarker: Marker | null = null;
|
||||
let mouseLat = 0;
|
||||
let mouseLng = 0;
|
||||
let isSelecting = false;
|
||||
|
|
@ -25,30 +25,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:
|
||||
'© <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: '© <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 +99,99 @@
|
|||
};
|
||||
|
||||
export const clearMapLayers = () => {
|
||||
plotLayerGroup?.clearLayers();
|
||||
// Remove all markers
|
||||
markers.forEach((marker) => marker.remove());
|
||||
markers = [];
|
||||
|
||||
// Remove animated marker
|
||||
removeAnimatedMarker();
|
||||
|
||||
// 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;
|
||||
|
||||
// Create popup with coordinates
|
||||
const popup = new maplibregl.Popup({ offset: 25, closeButton: false }).setHTML(
|
||||
`<b>${title}</b><br>Lat: ${lat.toFixed(6)}<br>Lng: ${lng.toFixed(6)}`,
|
||||
);
|
||||
|
||||
const marker = new maplibregl.Marker({ element: el })
|
||||
.setLngLat([lng, lat])
|
||||
.setPopup(popup)
|
||||
.addTo(map);
|
||||
|
||||
// Show popup on hover
|
||||
el.addEventListener("mouseenter", () => {
|
||||
marker.togglePopup();
|
||||
});
|
||||
el.addEventListener("mouseleave", () => {
|
||||
marker.togglePopup();
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
// Create popup with coordinates
|
||||
const popup = new maplibregl.Popup({ offset: 25, closeButton: false }).setHTML(
|
||||
`<b>${title}</b><br>Lat: ${lat.toFixed(6)}<br>Lng: ${lng.toFixed(6)}`,
|
||||
);
|
||||
|
||||
const marker = new maplibregl.Marker({ element: el })
|
||||
.setLngLat([lng, lat])
|
||||
.setPopup(popup)
|
||||
.addTo(map);
|
||||
|
||||
// Show popup on hover
|
||||
el.addEventListener("mouseenter", () => {
|
||||
marker.togglePopup();
|
||||
});
|
||||
el.addEventListener("mouseleave", () => {
|
||||
marker.togglePopup();
|
||||
});
|
||||
|
||||
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,49 +201,193 @@
|
|||
.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);
|
||||
}
|
||||
};
|
||||
|
||||
export const getMap = () => {
|
||||
return map;
|
||||
};
|
||||
|
||||
export const updateAnimatedMarker = (lat: number, lng: number) => {
|
||||
if (!map) return;
|
||||
|
||||
if (!animatedMarker) {
|
||||
// Create animated marker
|
||||
const el = document.createElement("div");
|
||||
el.className = "animated-marker";
|
||||
el.innerHTML = `
|
||||
<svg width="32" height="32" viewBox="0 0 32 32">
|
||||
<circle cx="16" cy="16" r="14" fill="#FF6B6B" opacity="0.3" class="pulse-ring"/>
|
||||
<circle cx="16" cy="16" r="8" fill="#FF1744" stroke="white" stroke-width="2"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
animatedMarker = new maplibregl.Marker({ element: el, anchor: "center" })
|
||||
.setLngLat([lng, lat])
|
||||
.addTo(map);
|
||||
} else {
|
||||
// Update position
|
||||
animatedMarker.setLngLat([lng, lat]);
|
||||
}
|
||||
|
||||
// Pan to marker
|
||||
map.panTo([lng, lat], { duration: 100 });
|
||||
};
|
||||
|
||||
export const removeAnimatedMarker = () => {
|
||||
if (animatedMarker) {
|
||||
animatedMarker.remove();
|
||||
animatedMarker = null;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="map-container" bind:this={mapContainer}>
|
||||
|
|
@ -156,3 +404,25 @@
|
|||
<WindVisualization {map} {windData} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.animated-marker) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.animated-marker .pulse-ring) {
|
||||
animation: pulse 2s ease-out infinite;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
@keyframes :global(pulse) {
|
||||
0% {
|
||||
r: 8;
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
r: 14;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
310
src/lib/components/TimeLine.svelte
Normal file
310
src/lib/components/TimeLine.svelte
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { Prediction } from "$lib/types";
|
||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
|
||||
let { prediction }: { prediction: Prediction | null } = $props();
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
timeUpdate: { index: number; lat: number; lng: number; alt: number; datetime: Date };
|
||||
}>();
|
||||
|
||||
let isPlaying = $state(false);
|
||||
let currentIndex = $state(0);
|
||||
let playbackSpeed = $state(1);
|
||||
let isCollapsed = $state(false);
|
||||
let animationFrame: number | null = null;
|
||||
let lastUpdateTime = 0;
|
||||
|
||||
$effect(() => {
|
||||
if (prediction && currentIndex >= flightPathLength) {
|
||||
currentIndex = 0;
|
||||
}
|
||||
});
|
||||
|
||||
const flightPathLength = $derived(prediction?.flight_path?.length || 0);
|
||||
const progress = $derived(flightPathLength > 0 ? (currentIndex / flightPathLength) * 100 : 0);
|
||||
|
||||
const currentPosition = $derived.by(() => {
|
||||
if (!prediction || !prediction.flight_path[currentIndex]) return null;
|
||||
|
||||
const point = prediction.flight_path[currentIndex];
|
||||
let lat: number, lng: number, alt: number;
|
||||
|
||||
if (Array.isArray(point)) {
|
||||
lat = point[0];
|
||||
lng = point[1];
|
||||
alt = point[2] || 0;
|
||||
} else {
|
||||
lat = point.lat;
|
||||
lng = point.lng;
|
||||
alt = point.alt || 0;
|
||||
}
|
||||
|
||||
const totalTime = prediction.flight_time;
|
||||
const timeProgress = (currentIndex / flightPathLength) * totalTime;
|
||||
const launchTime = prediction.launch.datetime instanceof Date
|
||||
? prediction.launch.datetime.getTime()
|
||||
: new Date(prediction.launch.datetime).getTime();
|
||||
const datetime = new Date(launchTime + timeProgress * 1000);
|
||||
|
||||
return { lat, lng, alt, datetime };
|
||||
});
|
||||
|
||||
const timeElapsed = $derived.by(() => {
|
||||
if (!prediction || !currentPosition) return "00:00:00";
|
||||
const launchTime = prediction.launch.datetime instanceof Date
|
||||
? prediction.launch.datetime.getTime()
|
||||
: new Date(prediction.launch.datetime).getTime();
|
||||
const totalSeconds = Math.floor(
|
||||
(currentPosition.datetime.getTime() - launchTime) / 1000,
|
||||
);
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||
});
|
||||
|
||||
function animate(timestamp: number) {
|
||||
if (!isPlaying) return;
|
||||
|
||||
if (!lastUpdateTime) lastUpdateTime = timestamp;
|
||||
const deltaTime = timestamp - lastUpdateTime;
|
||||
|
||||
if (deltaTime >= 50 / playbackSpeed) {
|
||||
if (currentIndex < flightPathLength - 1) {
|
||||
currentIndex++;
|
||||
if (currentPosition) {
|
||||
dispatch("timeUpdate", { ...currentPosition, index: currentIndex });
|
||||
}
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
lastUpdateTime = timestamp;
|
||||
}
|
||||
|
||||
animationFrame = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
function play() {
|
||||
if (!prediction) return;
|
||||
if (currentIndex >= flightPathLength - 1) {
|
||||
currentIndex = 0;
|
||||
}
|
||||
isPlaying = true;
|
||||
lastUpdateTime = 0;
|
||||
animationFrame = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
function pause() {
|
||||
isPlaying = false;
|
||||
if (animationFrame !== null) {
|
||||
cancelAnimationFrame(animationFrame);
|
||||
animationFrame = null;
|
||||
}
|
||||
}
|
||||
|
||||
function stop() {
|
||||
pause();
|
||||
currentIndex = 0;
|
||||
if (currentPosition) {
|
||||
dispatch("timeUpdate", { ...currentPosition, index: currentIndex });
|
||||
}
|
||||
}
|
||||
|
||||
function handleSliderChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
currentIndex = parseInt(target.value);
|
||||
if (currentPosition) {
|
||||
dispatch("timeUpdate", { ...currentPosition, index: currentIndex });
|
||||
}
|
||||
}
|
||||
|
||||
function changeSpeed() {
|
||||
const speeds = [1, 2, 5, 0.5];
|
||||
const currentSpeedIndex = speeds.indexOf(playbackSpeed);
|
||||
playbackSpeed = speeds[(currentSpeedIndex + 1) % speeds.length];
|
||||
}
|
||||
|
||||
function handleToggleCollapse() {
|
||||
isCollapsed = !isCollapsed;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
return () => {
|
||||
if (animationFrame !== null) {
|
||||
cancelAnimationFrame(animationFrame);
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="timeline-container card shadow-sm">
|
||||
<div
|
||||
class="card-header bg-primary text-white d-flex justify-content-between align-items-center p-1 px-3"
|
||||
style="cursor:pointer;"
|
||||
onclick={handleToggleCollapse}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => e.key === 'Enter' && handleToggleCollapse()}
|
||||
>
|
||||
<span class="fw-bold mb-0">Flight Timeline</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary p-0"
|
||||
aria-label="Toggle timeline visibility"
|
||||
>
|
||||
<i class="bi {isCollapsed ? 'bi-caret-left-fill' : 'bi-caret-down-fill'}"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if !isCollapsed}
|
||||
<div class="card-body p-3">
|
||||
<div class="timeline-info mb-2">
|
||||
<div class="info-section">
|
||||
<span class="form-label mb-1">Time:</span>
|
||||
<span class="fw-bold font-monospace">{timeElapsed}</span>
|
||||
</div>
|
||||
{#if currentPosition}
|
||||
<div class="info-section">
|
||||
<span class="form-label mb-1">Altitude:</span>
|
||||
<span class="fw-bold font-monospace">{Math.round(currentPosition.alt)} m</span>
|
||||
</div>
|
||||
<div class="info-section">
|
||||
<span class="form-label mb-1">Position:</span>
|
||||
<span class="fw-bold font-monospace"
|
||||
>{currentPosition.lat.toFixed(4)}, {currentPosition.lng.toFixed(4)}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="timeline-controls">
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
onclick={stop}
|
||||
disabled={!prediction || currentIndex === 0}
|
||||
title="Reset to start"
|
||||
aria-label="Reset to start"
|
||||
>
|
||||
<i class="bi bi-skip-start-fill"></i>
|
||||
</button>
|
||||
|
||||
{#if !isPlaying}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-success"
|
||||
onclick={play}
|
||||
disabled={!prediction}
|
||||
title="Play animation"
|
||||
aria-label="Play animation"
|
||||
>
|
||||
<i class="bi bi-play-fill"></i>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-warning"
|
||||
onclick={pause}
|
||||
title="Pause animation"
|
||||
aria-label="Pause animation"
|
||||
>
|
||||
<i class="bi bi-pause-fill"></i>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
onclick={changeSpeed}
|
||||
disabled={!prediction}
|
||||
title="Change playback speed"
|
||||
aria-label="Change playback speed"
|
||||
>
|
||||
{playbackSpeed}x
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1 position-relative">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={flightPathLength - 1}
|
||||
value={currentIndex}
|
||||
oninput={handleSliderChange}
|
||||
disabled={!prediction}
|
||||
class="form-range timeline-slider"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.timeline-container {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
min-width: 500px;
|
||||
max-width: 700px;
|
||||
z-index: 1000;
|
||||
background: var(--bs-body-bg, #fff);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.timeline-info {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.001rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.timeline-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Custom range slider styling to match Bootstrap theme */
|
||||
.timeline-slider::-webkit-slider-thumb {
|
||||
background: var(--bs-primary, #007bff);
|
||||
}
|
||||
|
||||
.timeline-slider::-moz-range-thumb {
|
||||
background: var(--bs-primary, #007bff);
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 767.98px) {
|
||||
.timeline-container {
|
||||
min-width: calc(100vw - 40px);
|
||||
max-width: calc(100vw - 40px);
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.timeline-info {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,244 +1,64 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
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 windData;
|
||||
// Props
|
||||
let { map, windData }: { map: any; windData: any } = $props();
|
||||
|
||||
let timeDimension;
|
||||
let timeDimensionControl;
|
||||
let velocityLayer;
|
||||
let heatLayer;
|
||||
let legend;
|
||||
|
||||
// Состояние переключателей
|
||||
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 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);
|
||||
}
|
||||
|
||||
// Обновляем контроль слоев
|
||||
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);
|
||||
// };
|
||||
// State for layer toggles
|
||||
let showHeatmap = $state(false);
|
||||
let showParticles = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
if (!map) return;
|
||||
if (!map || !windData) {
|
||||
console.warn('Map or wind data not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Настройка TimeDimension (добавьте эти строки в начале)
|
||||
// L.TimeDimension.Util.setProxy('https://your-proxy.com/?url='); // Для загрузки больших данных
|
||||
L.TimeDimension.Util.setCacheLimit(10); // Лимит кэшированных кадров
|
||||
console.log("WindVisualization component mounted");
|
||||
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);
|
||||
// NOTE: @sakitam-gis/maplibre-wind requires tile-based or image URL sources
|
||||
// It does not support raw wind data arrays directly
|
||||
//
|
||||
// The library expects:
|
||||
// - TileSource with URL template (e.g., 'https://tiles.example.com/{z}/{x}/{y}.png')
|
||||
// - ImageSource with image URL and coordinates
|
||||
//
|
||||
// To use this library, we would need to:
|
||||
// 1. Convert wind data to tiles or images
|
||||
// 2. Serve them via a tile server
|
||||
// 3. Use TileSource or ImageSource with the URLs
|
||||
//
|
||||
// Alternative approaches:
|
||||
// 1. Use deck.gl with ParticleLayer for raw data visualization
|
||||
// 2. Use MapLibre's native heatmap layers for color visualization
|
||||
// 3. Create a custom WebGL layer for particle animation
|
||||
// 4. Pre-process wind data into tiles/images server-side
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (map) {
|
||||
if (velocityLayer) map.removeLayer(velocityLayer);
|
||||
if (heatLayer) map.removeLayer(heatLayer);
|
||||
if (legend) map.removeControl(legend);
|
||||
}
|
||||
console.log("WindVisualization component destroyed");
|
||||
});
|
||||
|
||||
// Реактивность на изменение параметров
|
||||
$: if (map && windData) {
|
||||
updateLayers();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- <div class="layer-controls">
|
||||
<div class="control-group">
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={showHeatmap} />
|
||||
<input type="checkbox" bind:checked={showHeatmap} disabled />
|
||||
Тепловая карта
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={showVectors} />
|
||||
Векторы ветра
|
||||
<input type="checkbox" bind:checked={showParticles} disabled />
|
||||
Частицы ветра
|
||||
</label>
|
||||
</div>
|
||||
</div> -->
|
||||
<small style="color: #666; font-size: 11px; margin-top: 8px; display: block;">
|
||||
Wind visualization requires tile/image source
|
||||
</small>
|
||||
<small style="color: #999; font-size: 10px; margin-top: 4px; display: block;">
|
||||
See WindVisualisation.svelte for implementation notes
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.layer-controls {
|
||||
|
|
@ -246,54 +66,37 @@
|
|||
bottom: 30px;
|
||||
left: 10px;
|
||||
z-index: 1000;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
padding: 10px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 10px 12px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
gap: 8px;
|
||||
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;
|
||||
cursor: not-allowed;
|
||||
user-select: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
:global(.wind-heat-legend h4) {
|
||||
margin: 0 0 5px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
.control-group input[type="checkbox"] {
|
||||
cursor: not-allowed;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
:global(legend-scale) {
|
||||
display: flex;
|
||||
margin-bottom: 3px;
|
||||
small {
|
||||
font-style: italic;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
:global(legend-color) {
|
||||
height: 12px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
:global(.legend-labels) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
</style> -->
|
||||
|
|
|
|||
|
|
@ -1,286 +0,0 @@
|
|||
import * as L from "leaflet";
|
||||
import { distHaversine, bearingHaversine } from "$lib/mathutil";
|
||||
|
||||
// Define an interface for the control's options for type safety.
|
||||
export interface RulerOptions extends L.ControlOptions {
|
||||
events?: {
|
||||
onToggle?: (isActive: boolean) => void;
|
||||
};
|
||||
circleMarker?: L.CircleMarkerOptions;
|
||||
lineStyle?: L.PolylineOptions;
|
||||
lengthUnit?: {
|
||||
display?: string;
|
||||
decimal?: number;
|
||||
factor?: number | null;
|
||||
label?: string;
|
||||
};
|
||||
angleUnit?: {
|
||||
display?: string;
|
||||
decimal?: number;
|
||||
factor?: number | null;
|
||||
label?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Define an interface for the measurement result.
|
||||
interface MeasurementResult {
|
||||
Bearing: number;
|
||||
Distance: number;
|
||||
}
|
||||
|
||||
// Use a modern TypeScript class that extends L.Control.
|
||||
export class Ruler extends L.Control {
|
||||
// Override the default options with our custom ones.
|
||||
public options: RulerOptions = {
|
||||
position: "topright",
|
||||
events: {
|
||||
onToggle: () => {},
|
||||
},
|
||||
circleMarker: {
|
||||
color: "red",
|
||||
radius: 2,
|
||||
},
|
||||
lineStyle: {
|
||||
color: "red",
|
||||
dashArray: "1,6",
|
||||
},
|
||||
lengthUnit: {
|
||||
display: "km",
|
||||
decimal: 2,
|
||||
factor: null,
|
||||
label: "Distance:",
|
||||
},
|
||||
angleUnit: {
|
||||
display: "°",
|
||||
decimal: 2,
|
||||
factor: null,
|
||||
label: "Bearing:",
|
||||
},
|
||||
};
|
||||
|
||||
// Declare class properties with types.
|
||||
private _lastClickTime = 0;
|
||||
private _map?: L.Map;
|
||||
private _container?: HTMLElement;
|
||||
private _choice = false;
|
||||
private _defaultCursor = "";
|
||||
private _allLayers: L.LayerGroup = L.layerGroup();
|
||||
private _clickedLatLong: L.LatLng | null = null;
|
||||
private _clickedPoints: L.LatLng[] = [];
|
||||
private _totalLength = 0;
|
||||
private _clickCount = 0;
|
||||
private _tempLine: L.FeatureGroup = L.featureGroup();
|
||||
private _tempPoint: L.FeatureGroup = L.featureGroup();
|
||||
private _pointLayer: L.FeatureGroup = L.featureGroup();
|
||||
private _polylineLayer: L.FeatureGroup = L.featureGroup();
|
||||
private _movingLatLong: L.LatLng | null = null;
|
||||
private _result: MeasurementResult = { Bearing: 0, Distance: 0 };
|
||||
private _addedLength = 0;
|
||||
|
||||
constructor(options?: RulerOptions) {
|
||||
super(options);
|
||||
L.Util.setOptions(this, options);
|
||||
}
|
||||
|
||||
public isActive(): boolean {
|
||||
return this._choice;
|
||||
}
|
||||
|
||||
public onAdd(map: L.Map): HTMLElement {
|
||||
this._map = map;
|
||||
this._container = L.DomUtil.create("div", "leaflet-bar leaflet-ruler");
|
||||
L.DomEvent.disableClickPropagation(this._container);
|
||||
L.DomEvent.on(this._container, "click", this._toggleMeasure, this);
|
||||
this._defaultCursor = this._map.getContainer().style.cursor;
|
||||
this._allLayers = L.layerGroup();
|
||||
return this._container;
|
||||
}
|
||||
|
||||
public onRemove(): void {
|
||||
if (this._container) {
|
||||
L.DomEvent.off(this._container, "click", this._toggleMeasure, this);
|
||||
}
|
||||
if (this._choice) {
|
||||
this._toggleMeasure(); // Turn off measurements
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleMeasure(): void {
|
||||
this._choice = !this._choice;
|
||||
this.options.events?.onToggle?.(this._choice);
|
||||
|
||||
this._clickedLatLong = null;
|
||||
this._clickedPoints = [];
|
||||
this._totalLength = 0;
|
||||
|
||||
if (!this._map || !this._container) return;
|
||||
|
||||
const mapContainer = this._map.getContainer();
|
||||
|
||||
if (this._choice) {
|
||||
this._map.doubleClickZoom.disable();
|
||||
L.DomEvent.on(mapContainer, "keydown", this._escape, this);
|
||||
L.DomEvent.on(mapContainer, "dblclick", this._closePath, this);
|
||||
this._container.classList.add("leaflet-ruler-clicked");
|
||||
this._clickCount = 0;
|
||||
this._tempLine = L.featureGroup().addTo(this._allLayers);
|
||||
this._tempPoint = L.featureGroup().addTo(this._allLayers);
|
||||
this._pointLayer = L.featureGroup().addTo(this._allLayers);
|
||||
this._polylineLayer = L.featureGroup().addTo(this._allLayers);
|
||||
this._allLayers.addTo(this._map);
|
||||
mapContainer.style.cursor = "crosshair";
|
||||
this._map.on("click", this._clicked, this);
|
||||
this._map.on("mousemove", this._moving, this);
|
||||
} else {
|
||||
this._map.doubleClickZoom.enable();
|
||||
L.DomEvent.off(mapContainer, "keydown", this._escape, this);
|
||||
L.DomEvent.off(mapContainer, "dblclick", this._closePath, this);
|
||||
this._container.classList.remove("leaflet-ruler-clicked");
|
||||
this._map.removeLayer(this._allLayers);
|
||||
this._allLayers = L.layerGroup();
|
||||
mapContainer.style.cursor = this._defaultCursor;
|
||||
this._map.off("click", this._clicked, this);
|
||||
this._map.off("mousemove", this._moving, this);
|
||||
}
|
||||
}
|
||||
|
||||
private _clicked(e: L.LeafletMouseEvent): void {
|
||||
// hack to prevent adding the same point twice on double click
|
||||
let clickTime = Date.now();
|
||||
if (clickTime - this._lastClickTime < 200) {
|
||||
this._closePath();
|
||||
return;
|
||||
}
|
||||
|
||||
this._lastClickTime = clickTime;
|
||||
|
||||
this._clickedLatLong = e.latlng;
|
||||
this._clickedPoints.push(this._clickedLatLong);
|
||||
L.circleMarker(this._clickedLatLong, this.options.circleMarker).addTo(this._pointLayer);
|
||||
|
||||
if (this._clickCount > 0 && !e.latlng.equals(this._clickedPoints[this._clickedPoints.length - 2], 0.0001)) {
|
||||
if (this._movingLatLong) {
|
||||
L.polyline(
|
||||
[this._clickedPoints[this._clickCount - 1], this._movingLatLong],
|
||||
this.options.lineStyle
|
||||
).addTo(this._polylineLayer);
|
||||
}
|
||||
let text: string;
|
||||
this._totalLength += this._result.Distance;
|
||||
const angleUnit = this.options.angleUnit!;
|
||||
const lengthUnit = this.options.lengthUnit!;
|
||||
|
||||
if (this._clickCount > 1) {
|
||||
text = `<b>${angleUnit.label}</b> ${this._result.Bearing.toFixed(angleUnit.decimal)} ${
|
||||
angleUnit.display
|
||||
}<br><b>${lengthUnit.label}</b> ${this._totalLength.toFixed(lengthUnit.decimal)} ${
|
||||
lengthUnit.display
|
||||
}`;
|
||||
} else {
|
||||
text = `<b>${angleUnit.label}</b> ${this._result.Bearing.toFixed(angleUnit.decimal)} ${
|
||||
angleUnit.display
|
||||
}<br><b>${lengthUnit.label}</b> ${this._result.Distance.toFixed(lengthUnit.decimal)} ${
|
||||
lengthUnit.display
|
||||
}`;
|
||||
}
|
||||
L.circleMarker(this._clickedLatLong, this.options.circleMarker)
|
||||
.bindTooltip(text, { permanent: true, className: "result-tooltip" })
|
||||
.addTo(this._pointLayer)
|
||||
.openTooltip();
|
||||
}
|
||||
this._clickCount++;
|
||||
}
|
||||
|
||||
private _moving(e: L.LeafletMouseEvent): void {
|
||||
if (this._clickedLatLong && this._map) {
|
||||
this._movingLatLong = e.latlng;
|
||||
|
||||
this._tempLine.clearLayers();
|
||||
this._tempPoint.clearLayers();
|
||||
|
||||
this._calculateBearingAndDistance();
|
||||
this._addedLength = this._result.Distance + this._totalLength;
|
||||
|
||||
L.polyline([this._clickedLatLong, this._movingLatLong], this.options.lineStyle).addTo(this._tempLine);
|
||||
|
||||
const angleUnit = this.options.angleUnit!;
|
||||
const lengthUnit = this.options.lengthUnit!;
|
||||
let text: string;
|
||||
|
||||
if (this._clickCount > 1) {
|
||||
text = `<b>${angleUnit.label}</b> ${this._result.Bearing.toFixed(angleUnit.decimal)} ${
|
||||
angleUnit.display
|
||||
}<br><b>${lengthUnit.label}</b> ${this._addedLength.toFixed(lengthUnit.decimal)} ${
|
||||
lengthUnit.display
|
||||
}<br><div class="plus-length">(+${this._result.Distance.toFixed(lengthUnit.decimal)})</div>`;
|
||||
} else {
|
||||
text = `<b>${angleUnit.label}</b> ${this._result.Bearing.toFixed(angleUnit.decimal)} ${
|
||||
angleUnit.display
|
||||
}<br><b>${lengthUnit.label}</b> ${this._result.Distance.toFixed(lengthUnit.decimal)} ${
|
||||
lengthUnit.display
|
||||
}`;
|
||||
}
|
||||
L.circleMarker(this._movingLatLong, this.options.circleMarker)
|
||||
.bindTooltip(text, { sticky: true, offset: L.point(0, -40), className: "moving-tooltip" })
|
||||
.addTo(this._tempPoint)
|
||||
.openTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
private _escape(e: Event): void {
|
||||
if ((e as KeyboardEvent).key === "Escape") {
|
||||
if (this._clickCount > 0) {
|
||||
this._closePath();
|
||||
} else {
|
||||
this._toggleMeasure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _calculateBearingAndDistance(): void {
|
||||
if (!this._clickedLatLong || !this._movingLatLong) return;
|
||||
|
||||
const f1 = this._clickedLatLong.lat;
|
||||
const l1 = this._clickedLatLong.lng;
|
||||
const f2 = this._movingLatLong.lat;
|
||||
const l2 = this._movingLatLong.lng;
|
||||
|
||||
const angleUnit = this.options.angleUnit!;
|
||||
const lengthUnit = this.options.lengthUnit!;
|
||||
|
||||
const brng = bearingHaversine({ lat: f1, lng: l1 }, { lat: f2, lng: l2 });
|
||||
|
||||
const distance = distHaversine({ lat: f1, lng: l1 }, { lat: f2, lng: l2 });
|
||||
|
||||
if (angleUnit.factor) {
|
||||
this._result.Bearing = brng * angleUnit.factor;
|
||||
} else {
|
||||
this._result.Bearing = brng;
|
||||
}
|
||||
|
||||
if (lengthUnit.factor) {
|
||||
this._result.Distance = distance * lengthUnit.factor;
|
||||
} else {
|
||||
this._result.Distance = distance;
|
||||
}
|
||||
|
||||
this._result = {
|
||||
Bearing: brng,
|
||||
Distance: distance,
|
||||
};
|
||||
}
|
||||
|
||||
private _closePath(): void {
|
||||
if (!this._map || !this._container) return;
|
||||
|
||||
this._map.removeLayer(this._tempLine);
|
||||
this._map.removeLayer(this._tempPoint);
|
||||
this._choice = false;
|
||||
this._toggleMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
// Factory function for creating the control, maintaining the Leaflet convention.
|
||||
export const ruler = (options?: RulerOptions) => {
|
||||
return new Ruler(options);
|
||||
};
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
import type { LatLngExpression, LatLngLiteral } from "leaflet";
|
||||
// Define coordinate types (previously from Leaflet)
|
||||
export type LatLngTuple = [number, number] | [number, number, number]; // Support 2D and 3D coordinates
|
||||
export interface LatLngLiteral {
|
||||
lat: number;
|
||||
lng: number;
|
||||
alt?: number; // Optional altitude
|
||||
}
|
||||
export type LatLngExpression = LatLngTuple | LatLngLiteral;
|
||||
|
||||
export const PROFILE_MAP = {
|
||||
"Обычный": "standard_profile",
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
import { PredictionStore } from "$lib/stores";
|
||||
import { addToast, removeToast } from "$lib/components/ui/Toast.svelte";
|
||||
import ToastContainer from '$lib/components/ui/Toast.svelte';
|
||||
import L, { point } from "leaflet";
|
||||
import GenericPanel from "$lib/components/GenericPanel.svelte";
|
||||
import TimeLine from "$lib/components/TimeLine.svelte";
|
||||
|
||||
let map: Map | null = null;
|
||||
let panelContainer: PanelContainer | null = null;
|
||||
|
|
@ -32,8 +32,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());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -67,7 +72,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function handleTimeUpdate(event: CustomEvent<{ index: number; lat: number; lng: number; alt: number; datetime: Date }>) {
|
||||
const { lat, lng } = event.detail;
|
||||
map?.updateAnimatedMarker(lat, lng);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
|
@ -111,11 +119,12 @@
|
|||
<GenericPanel />
|
||||
{:else if activeTabRight === 'layers'}
|
||||
<GenericPanel />
|
||||
{:else if activeTabLeft === 'settings'}
|
||||
<!-- <SettingsPanel /> -->
|
||||
{/if}
|
||||
</div>
|
||||
</PanelContainer>
|
||||
<ToastContainer />
|
||||
{#if $PredictionStore}
|
||||
<TimeLine prediction={$PredictionStore} on:timeUpdate={handleTimeUpdate} />
|
||||
{/if}
|
||||
</Map>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -86,36 +86,17 @@
|
|||
z-index: 1001;
|
||||
}
|
||||
|
||||
.panel-container-right {
|
||||
position: absolute;
|
||||
top: var(--panel-top);
|
||||
right: var(--panel-left);
|
||||
width: 23rem;
|
||||
max-height: 90vh;
|
||||
max-width: calc(100vw - var(--panel-left) - var(--panel-left));
|
||||
overflow-y: auto;
|
||||
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;
|
||||
|
|
@ -123,6 +104,10 @@
|
|||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.maplibregl-popup-close-button {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
opacity: var(--bs-backdrop-opacity) !important;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue