Compare commits
4 Commits
master
...
600ed7bbb3
Author | SHA1 | Date | |
---|---|---|---|
600ed7bbb3 | |||
82adde997e | |||
5ec119b6c4 | |||
629825b53d |
@ -1,5 +1,8 @@
|
|||||||
name: Build Docker containers
|
name: Build Docker containers
|
||||||
on: [push]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Build:
|
Build:
|
||||||
|
443
webapp/frontend/package-lock.json
generated
443
webapp/frontend/package-lock.json
generated
@ -13,12 +13,7 @@
|
|||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"chartjs-adapter-moment": "^1.0.1",
|
"chartjs-adapter-moment": "^1.0.1",
|
||||||
"install": "^0.13.0",
|
"maplibre-gl": "^5.3.1",
|
||||||
"leaflet": "^1.9.4",
|
|
||||||
"leaflet-arrowheads": "^1.4.0",
|
|
||||||
"leaflet-geometryutil": "^0.10.3",
|
|
||||||
"leaflet-groupedlayercontrol": "^0.6.1",
|
|
||||||
"leaflet.markercluster": "^1.5.3",
|
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
@ -26,7 +21,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
|
||||||
"vite": "^6.2.4",
|
"vite": "^6.2.4",
|
||||||
"vite-plugin-vue-devtools": "^7.7.2"
|
"vite-plugin-vue-devtools": "^7.7.2"
|
||||||
}
|
}
|
||||||
@ -958,6 +952,95 @@
|
|||||||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"get-stream": "^6.0.1",
|
||||||
|
"minimist": "^1.2.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"geojson-rewind": "geojson-rewind"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/geojson-rewind/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==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/tiny-sdf": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/unitbezier": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/vector-tile": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"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==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||||
|
"version": "23.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-23.1.0.tgz",
|
||||||
|
"integrity": "sha512-R6/ihEuC5KRexmKIYkWqUv84Gm+/QwsOUgHyt1yy2XqCdGdLvlBWVWIIeTZWN4NGdwmY6xDzdSGU2R9oBLNg2w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||||
|
"@mapbox/unitbezier": "^0.0.1",
|
||||||
|
"json-stringify-pretty-compact": "^4.0.0",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
"quickselect": "^3.0.0",
|
||||||
|
"rw": "^1.3.3",
|
||||||
|
"tinyqueue": "^3.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"gl-style-format": "dist/gl-style-format.mjs",
|
||||||
|
"gl-style-migrate": "dist/gl-style-migrate.mjs",
|
||||||
|
"gl-style-validate": "dist/gl-style-validate.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@polka/url": {
|
"node_modules/@polka/url": {
|
||||||
"version": "1.0.0-next.29",
|
"version": "1.0.0-next.29",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||||
@ -1528,6 +1611,53 @@
|
|||||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||||
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/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==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "MIT",
|
||||||
|
"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==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/supercluster": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/web-bluetooth": {
|
"node_modules/@types/web-bluetooth": {
|
||||||
"version": "0.0.21",
|
"version": "0.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||||
@ -1548,25 +1678,6 @@
|
|||||||
"vue": "^3.2.25"
|
"vue": "^3.2.25"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-vue-jsx": {
|
|
||||||
"version": "4.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-4.1.2.tgz",
|
|
||||||
"integrity": "sha512-4Rk0GdE0QCdsIkuMmWeg11gmM4x8UmTnZR/LWPm7QJ7+BsK4tq08udrN0isrrWqz5heFy9HLV/7bOLgFS8hUjA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/core": "^7.26.7",
|
|
||||||
"@babel/plugin-transform-typescript": "^7.26.7",
|
|
||||||
"@vue/babel-plugin-jsx": "^1.2.5"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.0.0 || >=20.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vite": "^5.0.0 || ^6.0.0",
|
|
||||||
"vue": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/babel-helper-vue-transform-on": {
|
"node_modules/@vue/babel-helper-vue-transform-on": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.4.0.tgz",
|
||||||
@ -2108,6 +2219,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/earcut": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.137",
|
"version": "1.5.137",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz",
|
||||||
@ -2377,6 +2494,12 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
@ -2431,6 +2554,50 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gl-matrix": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ini": "^4.1.3",
|
||||||
|
"kind-of": "^6.0.3",
|
||||||
|
"which": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/global-prefix/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==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/global-prefix/node_modules/which": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"isexe": "^3.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"node-which": "bin/which.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.13.0 || >=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/globals": {
|
"node_modules/globals": {
|
||||||
"version": "11.12.0",
|
"version": "11.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||||
@ -2515,13 +2682,33 @@
|
|||||||
"node": ">=18.18.0"
|
"node": ">=18.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/install": {
|
"node_modules/ieee754": {
|
||||||
"version": "0.13.0",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
"integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==",
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
"license": "MIT",
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/ini": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
|
||||||
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.10"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-docker": {
|
"node_modules/is-docker": {
|
||||||
@ -2663,6 +2850,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/json-stringify-pretty-compact": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
@ -2689,6 +2882,21 @@
|
|||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kdbush": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/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==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/kolorist": {
|
"node_modules/kolorist": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
|
||||||
@ -2696,49 +2904,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/leaflet": {
|
|
||||||
"version": "1.9.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
|
||||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
|
||||||
"license": "BSD-2-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/leaflet-arrowheads": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/leaflet-arrowheads/-/leaflet-arrowheads-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-aIjsmoWe1VJXaGOpKpS6E8EzN2vpx3GGCNP/FxQteLVzAg5xMID7elf9hj/1CWLJo8FuGRjSvKkUQDj7mocrYA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"leaflet": "^1.7.1",
|
|
||||||
"leaflet-geometryutil": "^0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/leaflet-geometryutil": {
|
|
||||||
"version": "0.10.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/leaflet-geometryutil/-/leaflet-geometryutil-0.10.3.tgz",
|
|
||||||
"integrity": "sha512-Qeas+KsnenE0Km/ydt8km3AqFe7kJhVwuLdbCYM2xe2epsxv5UFEaVJiagvP9fnxS8QvBNbm7DJlDA0tkKo9VA==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"leaflet": "^1.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/leaflet-groupedlayercontrol": {
|
|
||||||
"version": "0.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/leaflet-groupedlayercontrol/-/leaflet-groupedlayercontrol-0.6.1.tgz",
|
|
||||||
"integrity": "sha512-xkGeH6ygcryFQI0FUWn6Bz93zlwrOSpeXgJQ3jLRoUQJ5h/Qgrz+GqR9nhBFfikjUKtC1sCKL3eDR9wNTWhLlA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"leaflet": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/leaflet.markercluster": {
|
|
||||||
"version": "1.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
|
|
||||||
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"leaflet": "^1.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.29.2",
|
"version": "1.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
|
||||||
@ -2986,6 +3151,47 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/maplibre-gl": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-Ihx+oUUSsZkjMou1Cw5J6silE+5OtFFQSPslWF9+7v4yFC/XDHrpsORYO9lWE4KZI0djCEUpZQJpkpnMArAbeA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"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": "^23.1.0",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
|
"@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.1",
|
||||||
|
"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/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@ -3016,6 +3222,15 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mitt": {
|
"node_modules/mitt": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
@ -3049,6 +3264,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/murmurhash-js": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
@ -3153,6 +3374,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pbf": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"ieee754": "^1.1.12",
|
||||||
|
"resolve-protobuf-schema": "^2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pbf": "bin/pbf"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/perfect-debounce": {
|
"node_modules/perfect-debounce": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
@ -3207,6 +3441,12 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/potpack": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/pretty-ms": {
|
"node_modules/pretty-ms": {
|
||||||
"version": "9.2.0",
|
"version": "9.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz",
|
||||||
@ -3223,12 +3463,33 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/protocol-buffers-schema": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/quickselect": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/resolve-protobuf-schema": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"protocol-buffers-schema": "^3.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rfdc": {
|
"node_modules/rfdc": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||||
@ -3288,6 +3549,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rw": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@ -3381,6 +3648,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/supercluster": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"kdbush": "^4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/superjson": {
|
"node_modules/superjson": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
|
||||||
@ -3409,6 +3685,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyqueue": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/totalist": {
|
"node_modules/totalist": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||||
@ -3632,6 +3914,17 @@
|
|||||||
"vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0"
|
"vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vt-pbf": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/point-geometry": "0.1.0",
|
||||||
|
"@mapbox/vector-tile": "^1.3.1",
|
||||||
|
"pbf": "^3.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "3.5.13",
|
"version": "3.5.13",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
||||||
|
@ -14,12 +14,7 @@
|
|||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"chartjs-adapter-moment": "^1.0.1",
|
"chartjs-adapter-moment": "^1.0.1",
|
||||||
"install": "^0.13.0",
|
"maplibre-gl": "^5.3.1",
|
||||||
"leaflet": "^1.9.4",
|
|
||||||
"leaflet-arrowheads": "^1.4.0",
|
|
||||||
"leaflet-geometryutil": "^0.10.3",
|
|
||||||
"leaflet-groupedlayercontrol": "^0.6.1",
|
|
||||||
"leaflet.markercluster": "^1.5.3",
|
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
@ -27,7 +22,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
|
||||||
"vite": "^6.2.4",
|
"vite": "^6.2.4",
|
||||||
"vite-plugin-vue-devtools": "^7.7.2"
|
"vite-plugin-vue-devtools": "^7.7.2"
|
||||||
}
|
}
|
||||||
|
304
webapp/frontend/src/LayerControl.js
Normal file
304
webapp/frontend/src/LayerControl.js
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
export default class LayersControl {
|
||||||
|
constructor(options) {
|
||||||
|
// This div will hold all the checkboxes and their labels
|
||||||
|
this._container = document.createElement('div');
|
||||||
|
this._container.style.padding = '12px';
|
||||||
|
this._container.classList.add(
|
||||||
|
// Built-in classes for consistency
|
||||||
|
'maplibregl-ctrl',
|
||||||
|
'maplibregl-ctrl-group',
|
||||||
|
// Custom class, see later
|
||||||
|
'layers-control',
|
||||||
|
);
|
||||||
|
// Options
|
||||||
|
this._options = options;
|
||||||
|
// Map inputs
|
||||||
|
this._mapInputs = {};
|
||||||
|
this.activeMap = null;
|
||||||
|
// Control inputs
|
||||||
|
this._controlInputs = {};
|
||||||
|
this._activeControl = {};
|
||||||
|
this._previousLayers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_createGroupHeader(name) {
|
||||||
|
const label = document.createElement('label');
|
||||||
|
const text = document.createElement('span');
|
||||||
|
text.innerText = name;
|
||||||
|
text.classList.add('layer-group-name');
|
||||||
|
label.appendChild(text);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
_createGroupContainer() {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.classList.add('layer-group');
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
_createGroup(name, data) {
|
||||||
|
const container = this._createGroupContainer();
|
||||||
|
const header = this._createGroupHeader(name);
|
||||||
|
container.appendChild(header);
|
||||||
|
for (const key of Object.keys(data.layers)) {
|
||||||
|
let input = null;
|
||||||
|
if (data.type === 'radio') {
|
||||||
|
input = this._createLabeledRadio(`${name}_${key}`, key, '_control_selection');
|
||||||
|
} else if (data.type === 'checkbox') {
|
||||||
|
input = this._createLabeledCheckbox(`${name}_${key}`, key, '_control_selection');
|
||||||
|
}
|
||||||
|
container.appendChild(input);
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyDefaults() {
|
||||||
|
for (const name of Object.keys(this._options.controls)) {
|
||||||
|
const config = this._options.controls[name];
|
||||||
|
if (typeof config.default === 'string') {
|
||||||
|
this._controlInputs[name][config.default].checked = true;
|
||||||
|
this._doControlAction(this._options.controls[name].layers[config.default]);
|
||||||
|
} else if (Array.isArray(config.default)) {
|
||||||
|
for (const id of config.default) {
|
||||||
|
this._controlInputs[name][id].checked = true;
|
||||||
|
this._doControlAction(this._options.controls[name].layers[id], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_createSeparator() {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.classList.add('layers-control-separator');
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getLayers() {
|
||||||
|
return this._map.getLayersOrder().filter(name => !this._mapInputs.hasOwnProperty(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
_getVisibleLayers() {
|
||||||
|
return this._getLayers().filter(layer => {
|
||||||
|
const visibility = this._map.getLayer(layer).getLayoutProperty('visibility') ?? 'visible';
|
||||||
|
return visibility === 'visible';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_doControlAction(config, value) {
|
||||||
|
switch(config.type) {
|
||||||
|
case 'source_filter':
|
||||||
|
const source = this._map.getSource(config.source);
|
||||||
|
const data = config.getter().filter(config.filter);
|
||||||
|
source.setData({
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: data,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'toggle_element':
|
||||||
|
if (config.element ?? false) {
|
||||||
|
document.querySelector(config.element).style.display = value ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'layer_toggle':
|
||||||
|
if (value) {
|
||||||
|
this._showLayer(config.layer);
|
||||||
|
} else {
|
||||||
|
this._hideLayer(config.layer);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'layer_control':
|
||||||
|
this._previousLayers = this._getVisibleLayers();
|
||||||
|
if (config.hideAllExcept ?? false) {
|
||||||
|
const layersToRemove = this._getVisibleLayers().filter(name => !config.hideAllExcept.includes(name));
|
||||||
|
for (const id of layersToRemove) {
|
||||||
|
this._hideLayer(id);
|
||||||
|
}
|
||||||
|
for (const id of config.hideAllExcept) {
|
||||||
|
this._showLayer(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.disableCluster ?? false) {
|
||||||
|
const style = this._map.getStyle()
|
||||||
|
style.sources[config.disableCluster].cluster = false;
|
||||||
|
this._map.setStyle(style);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_disableLayerTransitions(name) {
|
||||||
|
const layerType = this._map.getLayer(name)?.type;
|
||||||
|
if (layerType === 'fill' || layerType === 'line' || layerType === 'circle') {
|
||||||
|
this._map.setPaintProperty(name, `${layerType}-opacity-transition`, { duration: 0 });
|
||||||
|
} else if (layerType === 'symbol') {
|
||||||
|
this._map.setPaintProperty(name, 'text-opacity-transition', { duration: 0 });
|
||||||
|
this._map.setPaintProperty(name, 'icon-opacity-transition', { duration: 0 });
|
||||||
|
this._map.setPaintProperty(name, 'text-opacity', 1);
|
||||||
|
this._map.setPaintProperty(name, 'icon-opacity', 1);
|
||||||
|
const layout = this._map.getLayoutProperty(name, 'text-field');
|
||||||
|
if (layout) {
|
||||||
|
this._map.setLayoutProperty(name, 'text-optional', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_hideLayer(name) {
|
||||||
|
console.debug('hideLayer: ', name);
|
||||||
|
this._disableLayerTransitions(name);
|
||||||
|
this._map.setLayoutProperty(name, 'visibility', 'none');
|
||||||
|
}
|
||||||
|
|
||||||
|
_showLayer(name) {
|
||||||
|
console.debug('showLayer: ', name);
|
||||||
|
this._disableLayerTransitions(name);
|
||||||
|
this._map.setLayoutProperty(name, 'visibility', 'visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
_revertControlAction(config) {
|
||||||
|
switch(config.type) {
|
||||||
|
case 'source_filter':
|
||||||
|
const source = this._map.getSource(config.source);
|
||||||
|
source.setData({
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: config.getter(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'toggle_element':
|
||||||
|
if (config.element ?? false) {
|
||||||
|
const el = document.querySelector(config.element).style.display;
|
||||||
|
document.querySelector(config.element).style.display = (el === 'block' ? 'none' : 'block');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'layer_toggle':
|
||||||
|
const visibility = this._map.getLayer(config.layer).getLayoutProperty('visibility') ?? 'visible';
|
||||||
|
if (visibility === 'visible') {
|
||||||
|
this._hideLayer(config.layer);
|
||||||
|
} else {
|
||||||
|
this._showLayer(config.layer);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'layer_control':
|
||||||
|
// i want to hide all layers in current layers that weren't in previous layers, so all layers in current layers that are not in previous layers
|
||||||
|
const toHide = this._getVisibleLayers().filter(layer => !this._previousLayers.includes(layer));
|
||||||
|
const toShow = this._getLayers().filter(layer => this._previousLayers.includes(layer));
|
||||||
|
for (const id of toShow) {
|
||||||
|
this._showLayer(id);
|
||||||
|
}
|
||||||
|
for (const id of toHide) {
|
||||||
|
this._hideLayer(id);
|
||||||
|
}
|
||||||
|
this._previousLayers = [];
|
||||||
|
if (config.disableCluster ?? false) {
|
||||||
|
const style = this._map.getStyle()
|
||||||
|
style.sources[config.disableCluster].cluster = true;
|
||||||
|
this._map.setStyle(style);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_createLabeledRadio(id, display, type) {
|
||||||
|
const label = document.createElement('label');
|
||||||
|
const text = document.createElement('span');
|
||||||
|
text.innerText = ` ${display}`;
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'radio';
|
||||||
|
input.name = type;
|
||||||
|
input.value = id;
|
||||||
|
input.addEventListener('change', (e) => {
|
||||||
|
if (type === '_map_control_selection') {
|
||||||
|
if (this.activeMap != e.target.value) {
|
||||||
|
this._map.setLayoutProperty(this.activeMap, 'visibility', 'none');
|
||||||
|
this._map.setLayoutProperty(e.target.value, 'visibility', 'visible');
|
||||||
|
this.activeMap = e.target.value;
|
||||||
|
}
|
||||||
|
} else if (type === '_control_selection') {
|
||||||
|
const [ parent, child ] = id.split('_');
|
||||||
|
const layerConfig = this._options.controls[parent].layers[child];
|
||||||
|
this._revertControlAction(this._activeControl[parent] ?? {type: ''});
|
||||||
|
this._doControlAction(layerConfig);
|
||||||
|
this._activeControl[parent] = layerConfig;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (type === '_map_control_selection') {
|
||||||
|
this._mapInputs[id] = input;
|
||||||
|
} else if (type === '_control_selection') {
|
||||||
|
const [ parent, child ] = id.split('_');
|
||||||
|
this._controlInputs[parent][child] = input;
|
||||||
|
}
|
||||||
|
label.appendChild(input);
|
||||||
|
label.appendChild(text);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
_createLabeledCheckbox(id, display, type) {
|
||||||
|
const label = document.createElement('label');
|
||||||
|
const text = document.createElement('span');
|
||||||
|
text.innerText = ` ${display}`;
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'checkbox';
|
||||||
|
input.name = id;
|
||||||
|
input.addEventListener('change', (e) => {
|
||||||
|
if (type === '_control_selection') {
|
||||||
|
const [ parent, child ] = id.split('_');
|
||||||
|
const layerConfig = this._options.controls[parent].layers[child];
|
||||||
|
this._revertControlAction(this._activeControl[parent] ?? {type: ''});
|
||||||
|
this._doControlAction(layerConfig, e.target.checked);
|
||||||
|
this._activeControl[parent] = layerConfig;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (type === '_control_selection') {
|
||||||
|
const [ parent, child ] = id.split('_');
|
||||||
|
this._controlInputs[parent][child] = input;
|
||||||
|
}
|
||||||
|
label.appendChild(input);
|
||||||
|
label.appendChild(text);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdd(map) {
|
||||||
|
this._map = map;
|
||||||
|
map.on('load', () => {
|
||||||
|
// create map changer
|
||||||
|
for (const map of this._options.maps) {
|
||||||
|
const radioSelection = this._createLabeledRadio(map.id, map.display, '_map_control_selection');
|
||||||
|
this._container.appendChild(radioSelection);
|
||||||
|
}
|
||||||
|
this._container.appendChild(this._createSeparator());
|
||||||
|
// Create the checkboxes and add them to the container
|
||||||
|
for (const key of Object.keys(this._options.controls)) {
|
||||||
|
this._controlInputs[key] = {};
|
||||||
|
this._activeControl[parent] = {};
|
||||||
|
const group = this._createGroup(key, this._options.controls[key]);
|
||||||
|
this._container.appendChild(group);
|
||||||
|
}
|
||||||
|
this._options.maps.forEach(({ id, tiles, sourceExtraParams = {}, layerExtraParams = {} }) => {
|
||||||
|
map.addSource(id, {
|
||||||
|
...sourceExtraParams,
|
||||||
|
type: 'raster',
|
||||||
|
tiles,
|
||||||
|
});
|
||||||
|
map.addLayer({ ...layerExtraParams, id, source: id, type: 'raster' });
|
||||||
|
if (this._options.initialMap === id) {
|
||||||
|
map.setLayoutProperty(id, 'visibility', 'visible');
|
||||||
|
this._mapInputs[id].checked = true;
|
||||||
|
this.activeMap = id;
|
||||||
|
} else {
|
||||||
|
map.setLayoutProperty(id, 'visibility', 'none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return this._container;
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove(map) {
|
||||||
|
// Not sure why we have to do this ourselves since we are not the ones
|
||||||
|
// adding us to the map.
|
||||||
|
// Copied from their example so keeping it in.
|
||||||
|
this._container.parentNode.removeChild(this._container);
|
||||||
|
// This might be to help garbage collection? Also from their example.
|
||||||
|
// Or perhaps to ensure calls to this object do not change the map still
|
||||||
|
// after removal.
|
||||||
|
this._map = undefined;
|
||||||
|
}
|
||||||
|
}
|
19
webapp/frontend/src/LegendControl.js
Normal file
19
webapp/frontend/src/LegendControl.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export default class LegendControl {
|
||||||
|
onAdd(map) {
|
||||||
|
this._map = map;
|
||||||
|
this._container = document.createElement('div');
|
||||||
|
this._container.style.backgroundColor = 'white';
|
||||||
|
this._container.style.padding = '12px';
|
||||||
|
this._container.innerHTML = `<div style="margin-bottom:6px;"><strong>Legend</strong></div>`
|
||||||
|
+ `<div style="display:flex"><div class="icon-mqtt-connected" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> MQTT Connected</div>`
|
||||||
|
+ `<div style="display:flex"><div class="icon-mqtt-disconnected" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> MQTT Disconnected</div>`
|
||||||
|
+ `<div style="display:flex"><div class="icon-offline" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> Offline Too Long</div>`;
|
||||||
|
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group legend-control';
|
||||||
|
return this._container;
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove() {
|
||||||
|
this._container.parentNode.removeChild(this._container);
|
||||||
|
this._map = undefined;
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,46 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "leaflet/dist/leaflet.css";
|
@import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
@import "leaflet.markercluster/dist/MarkerCluster.css";
|
|
||||||
@import "leaflet.markercluster/dist/MarkerCluster.Default.css";
|
.layers-control {
|
||||||
@import "leaflet-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.css";
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-group-name {
|
||||||
|
margin-bottom: .2em;
|
||||||
|
margin-left: 3px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-group {
|
||||||
|
margin-bottom: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layers-control label {
|
||||||
|
font-size: 1.08333em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layers-control radio {
|
||||||
|
margin-top: 2px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layers-control-separator {
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
height: 0;
|
||||||
|
margin: 5px -10px 5px -6px;
|
||||||
|
}
|
||||||
|
|
||||||
/* used to prevent ui flicker before vuejs loads */
|
/* used to prevent ui flicker before vuejs loads */
|
||||||
[v-cloak] {
|
[v-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maplibregl-ctrl-basemaps:not(.closed) > .hidden {
|
||||||
|
display: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
.icon-mqtt-connected {
|
.icon-mqtt-connected {
|
||||||
background-color: #16a34a;
|
background-color: #16a34a;
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
|
@ -2,7 +2,7 @@ import { useStorage } from '@vueuse/core';
|
|||||||
|
|
||||||
// static
|
// static
|
||||||
export const CURRENT_ANNOUNCEMENT_ID = 1;
|
export const CURRENT_ANNOUNCEMENT_ID = 1;
|
||||||
export const BASE_PATH = '';
|
export const BASE_PATH = 'http://localhost:9090';
|
||||||
|
|
||||||
// string
|
// string
|
||||||
export const temperatureFormat = useStorage('temperature-display', 'fahrenheit');
|
export const temperatureFormat = useStorage('temperature-display', 'fahrenheit');
|
||||||
|
@ -1,18 +1,6 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import 'leaflet/dist/leaflet.js';
|
|
||||||
const L = window.L;
|
|
||||||
import 'leaflet.markercluster/dist/leaflet.markercluster.js';
|
|
||||||
import 'leaflet-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.js';
|
|
||||||
import {
|
|
||||||
escapeString,
|
|
||||||
findNodeById,
|
|
||||||
getRegionFrequencyRange,
|
|
||||||
hasNodeUplinkedToMqttRecently,
|
|
||||||
formatPositionPrecision,
|
|
||||||
getTerrainProfileImage,
|
|
||||||
} from './utils.js';
|
|
||||||
import { state } from './store.js';
|
|
||||||
import { markRaw } from 'vue';
|
import { markRaw } from 'vue';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { hasNodeUplinkedToMqttRecently, getRegionFrequencyRange, escapeString, formatPositionPrecision } from './utils.js';
|
||||||
|
|
||||||
// state/config
|
// state/config
|
||||||
let instance = null;
|
let instance = null;
|
||||||
@ -24,67 +12,60 @@ export function getMap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const layerGroups = {
|
export const layerGroups = {
|
||||||
nodes: new L.LayerGroup(),
|
nodes: {},
|
||||||
neighbors: new L.LayerGroup(),
|
neighbors: {},
|
||||||
waypoints: new L.LayerGroup(),
|
waypoints: {},
|
||||||
nodePositionHistory: new L.LayerGroup(),
|
nodePositionHistory: {},
|
||||||
nodeNeighbors: new L.LayerGroup(),
|
nodeNeighbors: {},
|
||||||
nodesRouter: L.markerClusterGroup({
|
nodesRouter: {},
|
||||||
showCoverageOnHover: false,
|
nodesClustered: {},
|
||||||
disableClusteringAtZoom: 10, // zoom level where node clustering is disabled
|
legend: {},
|
||||||
}),
|
none: {},
|
||||||
nodesClustered: L.markerClusterGroup({
|
|
||||||
showCoverageOnHover: false,
|
|
||||||
disableClusteringAtZoom: 10, // zoom level where node clustering is disabled
|
|
||||||
}),
|
|
||||||
legend: new L.LayerGroup(),
|
|
||||||
none: new L.LayerGroup(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tileLayers = {
|
export const tileLayers = [
|
||||||
"OpenStreetMap": L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
{
|
||||||
maxZoom: 22, // increase from 18 to 22
|
id: 'OpenStreetMap',
|
||||||
|
display: 'Open Street Map',
|
||||||
|
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
||||||
|
sourceExtraParams: {
|
||||||
|
tileSize: 256,
|
||||||
attribution: 'Tiles © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>',
|
attribution: 'Tiles © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>',
|
||||||
}),
|
maxzoom: 22
|
||||||
"OpenTopoMap": L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
|
}
|
||||||
maxZoom: 17, // open topo map doesn't have tiles closer than this
|
},
|
||||||
attribution: 'Tiles © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
{
|
||||||
}),
|
id: 'OpenTopoMap',
|
||||||
"Esri Satellite": L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
display: 'Open Topo Map',
|
||||||
maxZoom: 21, // esri doesn't have tiles closer than this
|
tiles: ['https://tile.opentopomap.org/{z}/{x}/{y}.png'],
|
||||||
attribution: 'Tiles © <a href="https://developers.arcgis.com/documentation/mapping-apis-and-services/deployment/basemap-attribution/">Esri</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>'
|
sourceExtraParams: {
|
||||||
}),
|
tileSize: 256,
|
||||||
"Google Satellite": L.tileLayer('https://{s}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', {
|
attribution: 'Tiles © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>"',
|
||||||
maxZoom: 21,
|
maxzoom: 17
|
||||||
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
|
}
|
||||||
attribution: 'Tiles © Google | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>'
|
},
|
||||||
}),
|
{
|
||||||
"Google Hybrid": L.tileLayer('https://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
|
id: 'esriSatellite',
|
||||||
maxZoom: 21,
|
display: 'ESRI Satellite',
|
||||||
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
|
tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'],
|
||||||
attribution: 'Tiles © Google | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>'
|
sourceExtraParams: {
|
||||||
}),
|
tileSize: 256,
|
||||||
};
|
attribution: 'Tiles © <a href="https://developers.arcgis.com/documentation/mapping-apis-and-services/deployment/basemap-attribution/">Esri</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>',
|
||||||
|
maxzoom: 21
|
||||||
export const icons = {
|
}
|
||||||
mqttConnected: L.divIcon({
|
},
|
||||||
className: 'icon-mqtt-connected',
|
{
|
||||||
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
|
id: 'googleSatellite',
|
||||||
}),
|
display: 'Google Satellite',
|
||||||
mqttDisconnected: L.divIcon({
|
tiles: ['https://mt0.google.com/vt/lyrs=s&x={x}&y={y}&z={z}'],
|
||||||
className: 'icon-mqtt-disconnected',
|
sourceExtraParams: {
|
||||||
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
|
tileSize: 256,
|
||||||
}),
|
attribution: 'Tiles © Google | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>',
|
||||||
offline: L.divIcon({
|
maxzoom: 21
|
||||||
className: 'icon-offline',
|
}
|
||||||
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
|
}
|
||||||
}),
|
];
|
||||||
positionHistory: L.divIcon({
|
export const icons = {mqttConnected: '#16a34a', mqttDisconnected: '#2563eb', offline: '#dc2626', positionHistory: '#a855f7'};
|
||||||
className: 'icon-position-history',
|
|
||||||
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// tooltips
|
// tooltips
|
||||||
|
|
||||||
export function getTooltipContentForWaypoint(waypoint) {
|
export function getTooltipContentForWaypoint(waypoint) {
|
||||||
@ -182,109 +163,47 @@ export function getTooltipContentForNode(node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getNeighbourTooltipContent(type, node, neighbourNode, distanceInMeters, snr) {
|
export function getNeighbourTooltipContent(type, node, neighbourNode, distanceInMeters, snr) {
|
||||||
// default to showing distance in meters
|
return '';
|
||||||
let distance = `${distanceInMeters} meters`;
|
|
||||||
|
|
||||||
// scale to distance in kms
|
|
||||||
if (distanceInMeters >= 1000) {
|
|
||||||
const distanceInKilometers = (distanceInMeters / 1000).toFixed(2);
|
|
||||||
distance = `${distanceInKilometers} kilometers`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const terrainImageUrl = type === 'weHeard' ? getTerrainProfileImage(node, neighbourNode) : getTerrainProfileImage(neighbourNode, node);
|
|
||||||
|
|
||||||
const templates = {
|
|
||||||
'weHeard': `<b>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}</b> heard <b>[${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)}</b>`
|
|
||||||
+ `<br/>SNR: ${snr}dB`
|
|
||||||
+ `<br/>Distance: ${distance}`
|
|
||||||
+ `<br/><br/>ID: ${node.node_id} heard ${neighbourNode.node_id}`
|
|
||||||
+ `<br/>Hex ID: ${node.node_id_hex} heard ${neighbourNode.node_id_hex}`
|
|
||||||
+ (node.neighbours_updated_at ? `<br/>Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : '')
|
|
||||||
+ `<br/><br/>Terrain images from <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>`
|
|
||||||
+ `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`,
|
|
||||||
'theyHeard': `<b>[${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)}</b> heard <b>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}</b>`
|
|
||||||
+ `<br/>SNR: ${snr}dB`
|
|
||||||
+ `<br/>Distance: ${distance}`
|
|
||||||
+ `<br/><br/>ID: ${neighbourNode.node_id} heard ${node.node_id}`
|
|
||||||
+ `<br/>Hex ID: ${neighbourNode.node_id_hex} heard ${node.node_id_hex}`
|
|
||||||
+ (neighbourNode.neighbours_updated_at ? `<br/>Updated: ${moment(new Date(neighbourNode.neighbours_updated_at)).fromNow()}` : '')
|
|
||||||
+ `<br/><br/>Terrain images from <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>`
|
|
||||||
+ `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`,
|
|
||||||
}
|
|
||||||
|
|
||||||
return templates[type];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup
|
// cleanup
|
||||||
|
|
||||||
export function clearAllNodes() {
|
export function clearAllNodes() {
|
||||||
layerGroups.nodes.clearLayers();
|
|
||||||
layerGroups.nodesClustered.clearLayers();
|
|
||||||
layerGroups.nodesRouter.clearLayers();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function clearAllNeighbors() {
|
export function clearAllNeighbors() {
|
||||||
layerGroups.neighbors.clearLayers();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function clearAllWaypoints() {
|
export function clearAllWaypoints() {
|
||||||
layerGroups.waypoints.clearLayers();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function clearAllPositionHistory() {
|
export function clearAllPositionHistory() {
|
||||||
layerGroups.nodePositionHistory.clearLayers();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function cleanUpPositionHistory() {
|
export function cleanUpPositionHistory() {
|
||||||
// close tooltips and popups
|
|
||||||
closeAllPopups();
|
|
||||||
closeAllTooltips();
|
|
||||||
|
|
||||||
// setup node neighbours layer
|
|
||||||
layerGroups.nodePositionHistory.clearLayers();
|
|
||||||
layerGroups.nodePositionHistory.removeFrom(getMap());
|
|
||||||
layerGroups.nodePositionHistory.addTo(getMap());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function closeAllTooltips() {
|
export function closeAllTooltips() {
|
||||||
getMap().eachLayer(function(layer) {
|
|
||||||
if (layer.options.pane === 'tooltipPane') {
|
|
||||||
layer.removeFrom(getMap());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function closeAllPopups() {
|
export function closeAllPopups() {
|
||||||
getMap().eachLayer(function(layer) {
|
|
||||||
if (layer.options.pane === 'popupPane') {
|
|
||||||
layer.removeFrom(getMap());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function cleanUpNodeNeighbors() {
|
export function cleanUpNodeNeighbors() {
|
||||||
// close tooltips and popups
|
|
||||||
closeAllPopups();
|
|
||||||
closeAllTooltips();
|
|
||||||
// setup node neighbours layer
|
|
||||||
layerGroups.nodeNeighbors.clearLayers();
|
|
||||||
layerGroups.nodeNeighbors.removeFrom(getMap());
|
|
||||||
layerGroups.nodeNeighbors.addTo(getMap());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function clearNodeOutline() {
|
export function clearNodeOutline() {
|
||||||
if (state.selectedNodeOutlineCircle) {
|
|
||||||
state.selectedNodeOutlineCircle.removeFrom(getMap());
|
|
||||||
state.selectedNodeOutlineCircle = null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function clearMap() {
|
export function clearMap() {
|
||||||
closeAllPopups();
|
|
||||||
closeAllTooltips();
|
|
||||||
clearAllNodes();
|
|
||||||
clearAllNeighbors();
|
|
||||||
clearAllWaypoints();
|
|
||||||
clearNodeOutline();
|
|
||||||
cleanUpNodeNeighbors();
|
|
||||||
};
|
};
|
@ -5,6 +5,7 @@ export const state = reactive({
|
|||||||
nodes: [],
|
nodes: [],
|
||||||
waypoints: [],
|
waypoints: [],
|
||||||
nodeMarkers: {},
|
nodeMarkers: {},
|
||||||
|
waypointMarkers: [],
|
||||||
|
|
||||||
// state
|
// state
|
||||||
searchText: '',
|
searchText: '',
|
||||||
@ -17,6 +18,7 @@ export const state = reactive({
|
|||||||
selectedNodeToShowNeighbours: null,
|
selectedNodeToShowNeighbours: null,
|
||||||
selectedNodeToShowNeighbours: null,
|
selectedNodeToShowNeighbours: null,
|
||||||
selectedNodeToShowNeighboursType: null,
|
selectedNodeToShowNeighboursType: null,
|
||||||
|
currentPopup: null,
|
||||||
|
|
||||||
// position history
|
// position history
|
||||||
selectedNodeToShowPositionHistory: null,
|
selectedNodeToShowPositionHistory: null,
|
||||||
|
@ -11,12 +11,9 @@ import Announcement from '../components/Announcement.vue';
|
|||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import 'leaflet/dist/leaflet';
|
import maplibregl from 'maplibre-gl';
|
||||||
const L = window.L;
|
import LegendControl from '../LegendControl.js';
|
||||||
import 'leaflet-geometryutil';
|
import LayerControl from '../LayerControl.js';
|
||||||
import 'leaflet-arrowheads';
|
|
||||||
import 'leaflet.markercluster';
|
|
||||||
import 'leaflet-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.js';
|
|
||||||
import { onMounted, useTemplateRef, ref, watch, markRaw } from 'vue';
|
import { onMounted, useTemplateRef, ref, watch, markRaw } from 'vue';
|
||||||
import { state } from '../store.js';
|
import { state } from '../store.js';
|
||||||
import {
|
import {
|
||||||
@ -290,6 +287,10 @@ window.showNodeNeighboursThatWeHeard = function(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isValidCoordinates(lat, lng) {
|
||||||
|
return lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;
|
||||||
|
}
|
||||||
|
|
||||||
function onNodesUpdated(nodes) {
|
function onNodesUpdated(nodes) {
|
||||||
const now = moment();
|
const now = moment();
|
||||||
state.nodes = [];
|
state.nodes = [];
|
||||||
@ -319,12 +320,6 @@ function onNodesUpdated(nodes) {
|
|||||||
node.latitude = node.latitude / 10000000;
|
node.latitude = node.latitude / 10000000;
|
||||||
node.longitude = node.longitude / 10000000;
|
node.longitude = node.longitude / 10000000;
|
||||||
|
|
||||||
// wrap longitude for shortest path, everything to left of australia should be shown on the right
|
|
||||||
let longitude = parseFloat(node.longitude);
|
|
||||||
if (longitude <= 100) {
|
|
||||||
longitude += 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
// icon based on mqtt connection state
|
// icon based on mqtt connection state
|
||||||
let icon = icons.mqttDisconnected;
|
let icon = icons.mqttDisconnected;
|
||||||
|
|
||||||
@ -342,116 +337,35 @@ function onNodesUpdated(nodes) {
|
|||||||
icon = icons.mqttConnected;
|
icon = icons.mqttConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isValidCoordinates(node.latitude, node.longitude)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// create node marker
|
// create node marker
|
||||||
const marker = L.marker([node.latitude, longitude], {
|
const marker = {
|
||||||
icon: icon,
|
type: 'Feature',
|
||||||
tagName: node.node_id,
|
properties: {
|
||||||
// we want to show online nodes above offline, but without needing to use separate layer groups
|
id: node.node_id,
|
||||||
zIndexOffset: nodeHasUplinkedToMqttRecently ? 1000 : -1000,
|
role: node.role_name,
|
||||||
}).on('click', function(event) {
|
layer: 'nodes',
|
||||||
// close tooltip on click to prevent tooltip and popup showing at same time
|
color: icon,
|
||||||
event.target.closeTooltip();
|
},
|
||||||
});
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
// add marker to node layer groups
|
coordinates: [node.longitude, node.latitude]
|
||||||
marker.addTo(layerGroups.nodes);
|
|
||||||
layerGroups.nodesClustered.addLayer(marker);
|
|
||||||
|
|
||||||
// add markers for routers and repeaters to routers layer group
|
|
||||||
if (node.role_name === 'ROUTER'
|
|
||||||
|| node.role_name === 'ROUTER_CLIENT'
|
|
||||||
|| node.role_name === 'ROUTER_LATE'
|
|
||||||
|| node.role_name === 'REPEATER') {
|
|
||||||
layerGroups.nodesRouter.addLayer(marker);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// show tooltip on desktop only
|
// add maker to cache
|
||||||
if (!isMobile()) {
|
|
||||||
marker.bindTooltip(getTooltipContentForNode(node), {
|
|
||||||
interactive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push node marker to cache
|
|
||||||
state.nodeMarkers[node.node_id] = marker;
|
state.nodeMarkers[node.node_id] = marker;
|
||||||
|
|
||||||
// show node info tooltip when clicking node marker
|
|
||||||
marker.on('click', function(event) {
|
|
||||||
// close all other popups and tooltips
|
|
||||||
closeAllTooltips();
|
|
||||||
closeAllPopups();
|
|
||||||
|
|
||||||
// find node
|
|
||||||
const node = findNodeById(event.target.options.tagName);
|
|
||||||
if (!node){
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
// set data
|
||||||
// show position precision outline
|
const source = getMap().getSource('nodes');
|
||||||
showNodeOutline(node.node_id);
|
if (source) {
|
||||||
|
source.setData({
|
||||||
// open tooltip for node
|
type: 'FeatureCollection',
|
||||||
getMap().openTooltip(getTooltipContentForNode(node), event.target.getLatLng(), {
|
features: Object.values(state.nodeMarkers),
|
||||||
interactive: true, // allow clicking buttons inside tooltip
|
|
||||||
permanent: true, // don't auto dismiss when clicking buttons inside tooltip
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
for(const node of nodes) {
|
|
||||||
// find current node
|
|
||||||
const currentNode = findNodeMarkerById(node.node_id);
|
|
||||||
if (!currentNode) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add node neighbours
|
|
||||||
var polylineOffset = 0;
|
|
||||||
const neighbours = node.neighbours ?? [];
|
|
||||||
for( const neighbour of neighbours) {
|
|
||||||
// fixme: skipping zero snr? saw some crazy long neighbours with zero snr...
|
|
||||||
if(neighbour.snr === 0){
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const neighbourNode = findNodeById(neighbour.node_id);
|
|
||||||
if (!neighbourNode) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const neighbourNodeMarker = findNodeMarkerById(neighbour.node_id);
|
|
||||||
if (neighbourNodeMarker) {
|
|
||||||
|
|
||||||
// calculate distance in meters between nodes (rounded to 2 decimal places)
|
|
||||||
const distanceInMeters = currentNode.getLatLng().distanceTo(neighbourNodeMarker.getLatLng()).toFixed(2);
|
|
||||||
|
|
||||||
// don't show this neighbour connection if further than config allows
|
|
||||||
if (neighboursMaxDistance.value != null && parseFloat(distanceInMeters) > neighboursMaxDistance.value) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add neighbour line to map
|
|
||||||
const line = L.polyline([
|
|
||||||
currentNode.getLatLng(),
|
|
||||||
neighbourNodeMarker.getLatLng(),
|
|
||||||
], {
|
|
||||||
color: '#2563eb',
|
|
||||||
opacity: 0.75,
|
|
||||||
offset: polylineOffset,
|
|
||||||
}).addTo(layerGroups.neighbors);
|
|
||||||
|
|
||||||
// increase offset so next neighbour does not overlay other neighbours from self
|
|
||||||
polylineOffset += 2;
|
|
||||||
|
|
||||||
const tooltip = getNeighbourTooltipContent('weHeard', node, neighbourNode, distanceInMeters, neighbour.snr);
|
|
||||||
line.bindTooltip(tooltip, {
|
|
||||||
sticky: true,
|
|
||||||
opacity: 1,
|
|
||||||
interactive: true,
|
|
||||||
}).bindPopup(tooltip).on('click', function(event) {
|
|
||||||
// close tooltip on click to prevent tooltip and popup showing at same time
|
|
||||||
event.target.closeTooltip();
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -487,42 +401,37 @@ function onWaypointsUpdated(waypoints) {
|
|||||||
waypoint.latitude = waypoint.latitude / 10000000;
|
waypoint.latitude = waypoint.latitude / 10000000;
|
||||||
waypoint.longitude = waypoint.longitude / 10000000;
|
waypoint.longitude = waypoint.longitude / 10000000;
|
||||||
|
|
||||||
// wrap longitude for shortest path, everything to left of australia should be shown on the right
|
|
||||||
let longitude = parseFloat(waypoint.longitude);
|
|
||||||
if (longitude <= 100) {
|
|
||||||
longitude += 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine emoji to show as marker icon
|
// determine emoji to show as marker icon
|
||||||
const emoji = waypoint.icon === 0 ? 128205 : waypoint.icon;
|
const emoji = waypoint.icon === 0 ? 128205 : waypoint.icon;
|
||||||
const emojiText = String.fromCodePoint(emoji);
|
const emojiText = String.fromCodePoint(emoji);
|
||||||
|
|
||||||
let tooltip = getTooltipContentForWaypoint(waypoint);
|
if (!isValidCoordinates(node.latitude, node.longitude)) {
|
||||||
|
continue;
|
||||||
// create waypoint marker
|
|
||||||
const marker = L.marker([waypoint.latitude, longitude], {
|
|
||||||
icon: L.divIcon({
|
|
||||||
className: 'waypoint-label',
|
|
||||||
iconSize: [26, 26], // increase from 12px to 26px
|
|
||||||
html: emojiText,
|
|
||||||
}),
|
|
||||||
}).bindPopup(tooltip).on('click', function(event) {
|
|
||||||
// close tooltip on click to prevent tooltip and popup showing at same time
|
|
||||||
event.target.closeTooltip();
|
|
||||||
});
|
|
||||||
|
|
||||||
// show tooltip on desktop only
|
|
||||||
if (!isMobile()) {
|
|
||||||
marker.bindTooltip(tooltip, {
|
|
||||||
interactive: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add marker to waypoints layer groups
|
|
||||||
marker.addTo(layerGroups.waypoints)
|
|
||||||
|
|
||||||
// add to cache
|
|
||||||
state.waypoints.push(waypoint);
|
state.waypoints.push(waypoint);
|
||||||
|
|
||||||
|
// create waypoint marker
|
||||||
|
const marker = {
|
||||||
|
type: 'Feature',
|
||||||
|
properties: {
|
||||||
|
layer: 'waypoints',
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [waypoint.longitude, waypoint.latitude]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// add maker to cache
|
||||||
|
state.waypointMarkers.push(marker);
|
||||||
|
}
|
||||||
|
// set data
|
||||||
|
const source = getMap().getSource('waypoints');
|
||||||
|
if (source) {
|
||||||
|
source.setData({
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: Object.values(state.waypointMarkers),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -661,19 +570,6 @@ function reload(goToNodeId, zoom) {
|
|||||||
onNodesUpdated(response.data.nodes);
|
onNodesUpdated(response.data.nodes);
|
||||||
// hide loading
|
// hide loading
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
// fetch waypoints (after awaiting nodes, so we can use nodes cache in waypoint tooltips)
|
|
||||||
axios.get(buildPath('/api/v1/waypoints')).then(response => {
|
|
||||||
onWaypointsUpdated(response.data.waypoints);
|
|
||||||
});
|
|
||||||
// go to node id if provided
|
|
||||||
if (goToNodeId) {
|
|
||||||
// go to node
|
|
||||||
if(goToNode(goToNodeId, false, zoom)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// fallback to showing node details since we can't go to the node
|
|
||||||
window.showNodeDetails(goToNodeId);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -732,159 +628,363 @@ function onSearchResultNodeClick(node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// set map bounds to be a little more than full size to prevent panning off screen
|
|
||||||
const bounds = [
|
const bounds = [
|
||||||
[-100, 70], // top left
|
[-100, 70], // top left
|
||||||
[100, 500], // bottom right
|
[100, 500], // bottom right
|
||||||
];
|
];
|
||||||
// create map positioned over AU and NZ
|
|
||||||
setMap(L.map(mapEl.value, {
|
|
||||||
maxBounds: bounds,
|
|
||||||
}));
|
|
||||||
// set view
|
|
||||||
getMap().setView([-15, 150], 2);
|
|
||||||
// remove leaflet link
|
|
||||||
getMap().attributionControl.setPrefix('');
|
|
||||||
|
|
||||||
// use tile layer based on config
|
const map = new maplibregl.Map({
|
||||||
const selectedTileLayer = tileLayers[selectedTileLayerName.value] || tileLayers['OpenStreetMap'];
|
container: mapEl.value,
|
||||||
selectedTileLayer.addTo(getMap());
|
attributionControl: false,
|
||||||
|
glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf',
|
||||||
// handle baselayerchange to update tile layer preference
|
style: {
|
||||||
getMap().on('baselayerchange', function(event) {
|
version: 8,
|
||||||
selectedTileLayerName.value = event.name;
|
sources: {},
|
||||||
});
|
layers: [],
|
||||||
|
glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf',
|
||||||
// create legend
|
|
||||||
const legend = L.control({position: 'bottomleft'});
|
|
||||||
legend.onAdd = function (map) {
|
|
||||||
const div = L.DomUtil.create('div', 'leaflet-control-layers');
|
|
||||||
div.style.backgroundColor = 'white';
|
|
||||||
div.style.padding = '12px';
|
|
||||||
div.innerHTML = `<div style="margin-bottom:6px;"><strong>Legend</strong></div>`
|
|
||||||
+ `<div style="display:flex"><div class="icon-mqtt-connected" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> MQTT Connected</div>`
|
|
||||||
+ `<div style="display:flex"><div class="icon-mqtt-disconnected" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> MQTT Disconnected</div>`
|
|
||||||
+ `<div style="display:flex"><div class="icon-offline" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> Offline Too Long</div>`;
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
// handle adding/remove legend on map (can't use L.Control as an overlay, so we toggle an empty L.LayerGroup)
|
|
||||||
getMap().on('overlayadd overlayremove', function(event) {
|
|
||||||
if (event.name === 'Legend') {
|
|
||||||
if (event.type === 'overlayadd') {
|
|
||||||
getMap().addControl(legend);
|
|
||||||
} else if(event.type === 'overlayremove') {
|
|
||||||
getMap().removeControl(legend);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// add layers to control ui
|
|
||||||
L.control.groupedLayers(tileLayers, {
|
|
||||||
'Nodes': {
|
|
||||||
'All': layerGroups.nodes,
|
|
||||||
'Routers': layerGroups.nodesRouter,
|
|
||||||
'Clustered': layerGroups.nodesClustered,
|
|
||||||
'None': layerGroups.none,
|
|
||||||
},
|
},
|
||||||
'Overlays': {
|
//center: [-15, 150],
|
||||||
'Legend': layerGroups.legend,
|
center: [0, 0],
|
||||||
'Neighbors': layerGroups.neighbors,
|
zoom: 2,
|
||||||
'Waypoints': layerGroups.waypoints,
|
fadeDuration: 0,
|
||||||
'Position History': layerGroups.nodePositionHistory,
|
renderWorldCopies: false
|
||||||
|
//maxBounds: [[-180, -85], [180, 85]]
|
||||||
|
});
|
||||||
|
setMap(map);
|
||||||
|
map.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-right');
|
||||||
|
// Add zoom and rotation controls to the map.
|
||||||
|
map.addControl(new maplibregl.NavigationControl({
|
||||||
|
visualizePitch: false,
|
||||||
|
visualizeRoll: false,
|
||||||
|
showZoom: true,
|
||||||
|
showCompass: false,
|
||||||
|
}), 'top-left');
|
||||||
|
const layerControl = new LayerControl({
|
||||||
|
maps: tileLayers,
|
||||||
|
initialMap: 'OpenStreetMap',
|
||||||
|
controls: {
|
||||||
|
Nodes: {
|
||||||
|
type: 'radio',
|
||||||
|
default: 'Clustered',
|
||||||
|
layers: {
|
||||||
|
'All': {
|
||||||
|
type: 'layer_control',
|
||||||
|
hideAllExcept: ['nodes'],
|
||||||
|
disableCluster: 'nodes',
|
||||||
},
|
},
|
||||||
}, {
|
'Routers': {
|
||||||
// make the "Nodes" group exclusive (use radio inputs instead of checkbox)
|
type: 'source_filter',
|
||||||
exclusiveGroups: ['Nodes'],
|
source: 'nodes',
|
||||||
}).addTo(getMap());
|
getter: function() { return Object.values(state.nodeMarkers) },
|
||||||
|
filter: function (node) { return ['ROUTER', 'ROUTER_CLIENT', 'ROUTER_LATE', 'REPEATER'].includes(node.properties.role); }
|
||||||
// enable base layers
|
},
|
||||||
layerGroups.nodesClustered.addTo(getMap());
|
'Clustered': {
|
||||||
|
type: 'layer_control',
|
||||||
// enable overlay layers based on config
|
hideAllExcept: ['clusters', 'unclustered-points', 'cluster-count'],
|
||||||
if (enabledOverlayLayers.value.includes('Legend')) {
|
},
|
||||||
layerGroups.legend.addTo(getMap());
|
'None': {
|
||||||
|
type: 'layer_control',
|
||||||
|
hideAllExcept: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Overlays: {
|
||||||
|
type: 'checkbox',
|
||||||
|
default: ['Legend', 'Position History'],
|
||||||
|
layers: {
|
||||||
|
'Legend': {
|
||||||
|
type: 'toggle_element',
|
||||||
|
element: '.legend-control',
|
||||||
|
},
|
||||||
|
'Neighbors': {},
|
||||||
|
'Waypoints': {
|
||||||
|
type: 'layer_toggle',
|
||||||
|
layer: 'waypoints',
|
||||||
|
},
|
||||||
|
'Position History': {},
|
||||||
}
|
}
|
||||||
if (enabledOverlayLayers.value.includes('Neighbors')) {
|
},
|
||||||
layerGroups.neighbors.addTo(getMap());
|
|
||||||
}
|
}
|
||||||
if (enabledOverlayLayers.value.includes('Waypoints')) {
|
|
||||||
layerGroups.waypoints.addTo(getMap());
|
|
||||||
}
|
|
||||||
if (enabledOverlayLayers.value.includes('Position History')) {
|
|
||||||
layerGroups.nodePositionHistory.addTo(getMap());
|
|
||||||
}
|
|
||||||
// update config when map overlay is added/removed
|
|
||||||
getMap().on('overlayremove', function(event) {
|
|
||||||
const layerName = event.name;
|
|
||||||
enabledOverlayLayers.value = enabledOverlayLayers.value.filter(function(enabledOverlayLayer) {
|
|
||||||
return enabledOverlayLayer !== layerName;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
map.addControl(layerControl, 'top-right');
|
||||||
|
map.addControl(new LegendControl(), 'bottom-left');
|
||||||
|
|
||||||
getMap().on('overlayadd', function(event) {
|
map.doubleClickZoom.disable(); // optional but recommended for clean UX
|
||||||
const layerName = event.name;
|
|
||||||
if (!enabledOverlayLayers.value.includes(layerName)) {
|
map.on('load', () => {
|
||||||
enabledOverlayLayers.value.push(layerName);
|
map.addSource('nodes', {
|
||||||
|
type: 'geojson',
|
||||||
|
cluster: true,
|
||||||
|
clusterMaxZoom: 14,
|
||||||
|
clusterRadius: 50,
|
||||||
|
data: {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: [],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.addSource('waypoints', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: [],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.addLayer({
|
||||||
|
id: 'clusters',
|
||||||
|
type: 'circle',
|
||||||
|
source: 'nodes',
|
||||||
|
filter: ['has', 'point_count'],
|
||||||
|
paint: {
|
||||||
|
'circle-color': [
|
||||||
|
'step',
|
||||||
|
['get', 'point_count'],
|
||||||
|
'#6ecc3999', // small clusters
|
||||||
|
10, '#f0c20c99',
|
||||||
|
30, '#f1801799' // larger clusters
|
||||||
|
],
|
||||||
|
'circle-radius': [
|
||||||
|
'step',
|
||||||
|
['get', 'point_count'],
|
||||||
|
15, 10, 20, 30, 25
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.addLayer({
|
||||||
|
id: 'cluster-count',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'nodes',
|
||||||
|
filter: ['has', 'point_count'],
|
||||||
|
layout: {
|
||||||
|
'text-field': '{point_count_abbreviated}',
|
||||||
|
'text-font': ['Open Sans Regular','Arial Unicode MS Regular'],
|
||||||
|
'text-size': 12
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.addLayer({
|
||||||
|
id: 'unclustered-points',
|
||||||
|
type: 'circle',
|
||||||
|
source: 'nodes',
|
||||||
|
filter: ['!', ['has', 'point_count']],
|
||||||
|
paint: {
|
||||||
|
'circle-radius': 6,
|
||||||
|
'circle-color': ['coalesce', ['get', 'color'], '#cccccc'],
|
||||||
|
'circle-stroke-width': 1,
|
||||||
|
'circle-stroke-color': '#ffffff'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.addLayer({
|
||||||
|
id: 'nodes',
|
||||||
|
type: 'circle',
|
||||||
|
source: 'nodes',
|
||||||
|
layout: {
|
||||||
|
visibility: 'none',
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'circle-radius': 6,
|
||||||
|
'circle-color': ['coalesce', ['get', 'color'], '#cccccc'],
|
||||||
|
'circle-stroke-width': 1,
|
||||||
|
'circle-stroke-color': '#ffffff'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.addLayer({
|
||||||
|
id: 'waypoints',
|
||||||
|
type: 'circle',
|
||||||
|
source: 'waypoints',
|
||||||
|
layout: {
|
||||||
|
visibility: 'none',
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'circle-radius': 6,
|
||||||
|
'circle-color': ['coalesce', ['get', 'color'], '#cccccc'],
|
||||||
|
'circle-stroke-width': 1,
|
||||||
|
'circle-stroke-color': '#ffffff'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
getMap().on('click', function(event) {
|
map.on('mouseenter', 'clusters', () => {
|
||||||
// remove outline when map clicked
|
map.getCanvas().style.cursor = 'pointer';
|
||||||
clearNodeOutline();
|
|
||||||
// clear search
|
|
||||||
state.searchText = '';
|
|
||||||
state.mobileSearchVisible = false;
|
|
||||||
// do nothing when clicking inside tooltip
|
|
||||||
const clickedElement = event.originalEvent.target;
|
|
||||||
if (elementOrAnyAncestorHasClass(clickedElement, 'leaflet-tooltip')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAllTooltips();
|
|
||||||
closeAllPopups();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// auto update url when lat/lng/zoom changes
|
map.on('mouseleave', 'clusters', () => {
|
||||||
getMap().on('moveend zoomend', function() {
|
map.getCanvas().style.cursor = '';
|
||||||
// check if user enabled auto updating position in url
|
|
||||||
if (!autoUpdatePositionInUrl.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get map info
|
|
||||||
const latLng = getMap().getCenter();
|
|
||||||
const zoom = getMap().getZoom();
|
|
||||||
|
|
||||||
// construct new url
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.searchParams.set('lat', latLng.lat);
|
|
||||||
url.searchParams.set('lng', latLng.lng);
|
|
||||||
url.searchParams.set('zoom', zoom);
|
|
||||||
|
|
||||||
// update current url
|
|
||||||
if(window.history.replaceState){
|
|
||||||
window.history.replaceState(null, null, url.toString());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// parse url params
|
map.on('click', 'clusters', async (e) => {
|
||||||
const queryParams = new URLSearchParams(location.search);
|
const features = map.queryRenderedFeatures(e.point, {
|
||||||
const queryNodeId = queryParams.get('node_id');
|
layers: ['clusters'] // Ensure you are targeting the 'clusters' layer
|
||||||
const queryLat = queryParams.get('lat');
|
});
|
||||||
const queryLng = queryParams.get('lng');
|
if (!features.length) return;
|
||||||
const queryZoom = queryParams.get('zoom');
|
const cluster = features[0]; // Get the clicked cluster feature
|
||||||
|
const clusterId = cluster.properties.cluster_id;
|
||||||
// go to lat/lng if provided
|
const zoom = await map.getSource('nodes').getClusterExpansionZoom(clusterId);
|
||||||
if(queryLat && queryLng){
|
map.easeTo({
|
||||||
const zoomLevel = queryZoom || goToNodeZoomLevel.value
|
center: features[0].geometry.coordinates,
|
||||||
getMap().flyTo([queryLat, queryLng], parseFloat(zoomLevel), {
|
zoom: zoom,
|
||||||
animate: false,
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// reload and go to provided node id
|
// When a click event occurs on a feature in
|
||||||
reload(queryNodeId, queryZoom);
|
// the unclustered-point layer, open a popup at
|
||||||
|
// the location of the feature, with
|
||||||
|
// description HTML from its properties.
|
||||||
|
map.on('click', 'unclustered-points', showPopupForEvent);
|
||||||
|
map.on('mouseenter', 'unclustered-points', showPopupForEvent);
|
||||||
|
map.on('click', 'nodes', showPopupForEvent);
|
||||||
|
map.on('mouseenter', 'nodes', showPopupForEvent);
|
||||||
|
|
||||||
|
layerControl.applyDefaults();
|
||||||
|
reload();
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function measurePopupSize(htmlContent) {
|
||||||
|
const popupContainer = document.createElement('div');
|
||||||
|
popupContainer.className = 'maplibregl-popup maplibregl-popup-anchor-bottom';
|
||||||
|
popupContainer.style.position = 'absolute';
|
||||||
|
popupContainer.style.top = '-9999px';
|
||||||
|
popupContainer.style.left = '-9999px';
|
||||||
|
popupContainer.style.visibility = 'hidden';
|
||||||
|
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.className = 'maplibregl-popup-content';
|
||||||
|
content.innerHTML = htmlContent;
|
||||||
|
|
||||||
|
popupContainer.appendChild(content);
|
||||||
|
document.body.appendChild(popupContainer);
|
||||||
|
|
||||||
|
const width = popupContainer.offsetWidth;
|
||||||
|
const height = popupContainer.offsetHeight;
|
||||||
|
|
||||||
|
document.body.removeChild(popupContainer);
|
||||||
|
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPopupForEvent(e) {
|
||||||
|
if (e.features[0].popup) {
|
||||||
|
return; // already has a popup open
|
||||||
|
}
|
||||||
|
const map = getMap();
|
||||||
|
const coordinates = e.features[0].geometry.coordinates.slice();
|
||||||
|
const nodeId = e.features[0].properties.id;
|
||||||
|
const html = getTooltipContentForNode(findNodeById(nodeId));
|
||||||
|
|
||||||
|
const mapContainer = map.getContainer();
|
||||||
|
const mapWidth = mapContainer.offsetWidth;
|
||||||
|
const mapHeight = mapContainer.offsetHeight;
|
||||||
|
|
||||||
|
// Get screen point of the marker
|
||||||
|
const screenPoint = map.project(coordinates);
|
||||||
|
|
||||||
|
// Measure the popup size
|
||||||
|
const { width: popupWidth, height: popupHeight } = measurePopupSize(html);
|
||||||
|
|
||||||
|
// Calculate available space around the marker
|
||||||
|
const padding = 10;
|
||||||
|
const headerHeight = document.querySelector('header')?.offsetHeight || 0;
|
||||||
|
|
||||||
|
const space = {
|
||||||
|
left: screenPoint.x - padding,
|
||||||
|
right: mapWidth - screenPoint.x - padding,
|
||||||
|
top: screenPoint.y - headerHeight - padding,
|
||||||
|
bottom: mapHeight - screenPoint.y - padding,
|
||||||
|
};
|
||||||
|
|
||||||
|
const popupOptions = {
|
||||||
|
'Top-Left': { // popup below, caret top left
|
||||||
|
anchor: 'top-left',
|
||||||
|
veriticalDeficit: Math.max(popupHeight - space.bottom, 0),
|
||||||
|
horizontalDeficit: Math.max(popupWidth - space.right, 0),
|
||||||
|
},
|
||||||
|
'Top-Right': { // popup below, caret top right
|
||||||
|
anchor: 'top-right',
|
||||||
|
veriticalDeficit: Math.max(popupHeight - space.bottom, 0),
|
||||||
|
horizontalDeficit: Math.max(popupWidth - space.left, 0),
|
||||||
|
},
|
||||||
|
'Bottom-Right': { // popup above, caret bottom right
|
||||||
|
anchor: 'bottom-right',
|
||||||
|
veriticalDeficit: Math.max(popupHeight - space.top, 0),
|
||||||
|
horizontalDeficit: Math.max(popupWidth - space.left, 0),
|
||||||
|
},
|
||||||
|
'Bottom-Left': { // popup above, caret bottom left
|
||||||
|
anchor: 'bottom-left',
|
||||||
|
veriticalDeficit: Math.max(popupHeight - space.top, 0),
|
||||||
|
horizontalDeficit: Math.max(popupWidth - space.right, 0),
|
||||||
|
},
|
||||||
|
'Left': {
|
||||||
|
anchor: 'right',
|
||||||
|
veriticalDeficit: Math.max(Math.max(((popupHeight / 2) - space.bottom), 0) + Math.max(((popupHeight/2) - space.top), 0), 0),
|
||||||
|
horizontalDeficit: Math.max(popupWidth - space.left, 0),
|
||||||
|
},
|
||||||
|
'Right': {
|
||||||
|
anchor: 'left',
|
||||||
|
veriticalDeficit: Math.max(Math.max(((popupHeight / 2) - space.bottom), 0) + Math.max(((popupHeight/2) - space.top), 0), 0),
|
||||||
|
horizontalDeficit: Math.max(popupWidth - space.right, 0),
|
||||||
|
},
|
||||||
|
'Above': {
|
||||||
|
anchor: 'bottom',
|
||||||
|
veriticalDeficit: Math.max(popupHeight - space.top, 0),
|
||||||
|
horizontalDeficit: Math.max(Math.max(((popupWidth / 2) - space.left), 0) + Math.max(((popupWidth / 2) - space.right), 0), 0),
|
||||||
|
},
|
||||||
|
'Below': {
|
||||||
|
anchor: 'top',
|
||||||
|
veriticalDeficit: Math.max(popupHeight - space.bottom, 0),
|
||||||
|
horizontalDeficit: Math.max(Math.max(((popupWidth / 2) - space.left), 0) + Math.max(((popupWidth / 2) - space.right), 0), 0),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let bestOption = null;
|
||||||
|
let bestScore = -Infinity;
|
||||||
|
const preferredAnchors = ['Left', 'Right', 'Above', 'Below'];
|
||||||
|
|
||||||
|
Object.entries(popupOptions).forEach(([key, option]) => {
|
||||||
|
const verticalCutoff = option.veriticalDeficit / popupHeight;
|
||||||
|
const horizontalCutoff = option.horizontalDeficit / popupWidth;
|
||||||
|
const totalCutoff = verticalCutoff + horizontalCutoff;
|
||||||
|
const visibilityScore = 1 - totalCutoff;
|
||||||
|
|
||||||
|
option.visibilityScore = visibilityScore; // for debugging/logging
|
||||||
|
|
||||||
|
if (
|
||||||
|
visibilityScore > bestScore ||
|
||||||
|
(
|
||||||
|
Math.abs(visibilityScore - bestScore) < 0.01 && // within tolerance
|
||||||
|
bestOption && preferredAnchors.includes(key) && !preferredAnchors.includes(bestOption)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
bestScore = visibilityScore;
|
||||||
|
bestOption = key;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const popup = new maplibregl.Popup({
|
||||||
|
closeButton: true,
|
||||||
|
closeOnClick: true,
|
||||||
|
anchor: popupOptions[bestOption].anchor,
|
||||||
|
});
|
||||||
|
|
||||||
|
popup
|
||||||
|
.setLngLat(coordinates)
|
||||||
|
.setHTML(html)
|
||||||
|
.addTo(map);
|
||||||
|
|
||||||
|
if (e.type === 'mouseenter') {
|
||||||
|
const removePopup = () => {
|
||||||
|
popup.remove();
|
||||||
|
getMap().off('mousemove', onMouseMove);
|
||||||
|
getMap().getCanvas().style.cursor = '';
|
||||||
|
};
|
||||||
|
const onMouseMove = (e) => {
|
||||||
|
const features = getMap().queryRenderedFeatures(e.point, { layers: ['unclustered-points'] });
|
||||||
|
if (!features.length) {
|
||||||
|
removePopup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getMap().on('mousemove', onMouseMove);
|
||||||
|
}
|
||||||
|
e.features[0].popup = popup;
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (lastSeenAnnouncementId.value !== CURRENT_ANNOUNCEMENT_ID) {
|
if (lastSeenAnnouncementId.value !== CURRENT_ANNOUNCEMENT_ID) {
|
||||||
state.announcementVisible = true;
|
state.announcementVisible = true;
|
||||||
@ -898,12 +998,14 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full w-full overflow-hidden">
|
<div class="flex flex-col h-full w-full overflow-hidden">
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
|
<header>
|
||||||
<Announcement />
|
<Announcement />
|
||||||
<Header
|
<Header
|
||||||
@reload="reload"
|
@reload="reload"
|
||||||
@random-node="goToRandomNode"
|
@random-node="goToRandomNode"
|
||||||
@search-click="onSearchResultNodeClick"
|
@search-click="onSearchResultNodeClick"
|
||||||
/>
|
/>
|
||||||
|
</header>
|
||||||
<div id="map" style="width:100%;height:100%;" ref="appMap"></div>
|
<div id="map" style="width:100%;height:100%;" ref="appMap"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,6 @@ import { fileURLToPath, URL } from 'node:url'
|
|||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
@ -10,7 +9,6 @@ import tailwindcss from '@tailwindcss/vite'
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
vueJsx(),
|
|
||||||
vueDevTools(),
|
vueDevTools(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
],
|
],
|
||||||
|
Reference in New Issue
Block a user