mirror of
https://github.com/azaion/ui.git
synced 2026-04-22 08:56:35 +00:00
embed mission-planner
This commit is contained in:
Submodule mission-planner deleted from e12f244082
@@ -0,0 +1 @@
|
||||
VITE_SATELLITE_TILE_URL=https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}
|
||||
@@ -0,0 +1,19 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# production
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.idea
|
||||
@@ -0,0 +1,70 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||
|
||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
@@ -0,0 +1,485 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "mission-planner",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@mui/icons-material": "^5.15.19",
|
||||
"@mui/material": "^5.16.7",
|
||||
"chart.js": "^4.4.4",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-draw": "^1.0.4",
|
||||
"leaflet-polylinedecorator": "^1.6.0",
|
||||
"react": "^18.3.1",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-world-flags": "^1.6.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/leaflet": "^1.9.12",
|
||||
"@types/leaflet-draw": "^1.0.12",
|
||||
"@types/leaflet-polylinedecorator": "^1.6.4",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="],
|
||||
|
||||
"@emotion/cache": ["@emotion/cache@11.14.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="],
|
||||
|
||||
"@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="],
|
||||
|
||||
"@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.4.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw=="],
|
||||
|
||||
"@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="],
|
||||
|
||||
"@emotion/react": ["@emotion/react@11.14.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA=="],
|
||||
|
||||
"@emotion/serialize": ["@emotion/serialize@1.3.3", "", { "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA=="],
|
||||
|
||||
"@emotion/sheet": ["@emotion/sheet@1.4.0", "", {}, "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="],
|
||||
|
||||
"@emotion/styled": ["@emotion/styled@11.14.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/is-prop-valid": "^1.3.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2" }, "peerDependencies": { "@emotion/react": "^11.0.0-rc.0", "react": ">=16.8.0" } }, "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw=="],
|
||||
|
||||
"@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="],
|
||||
|
||||
"@emotion/use-insertion-effect-with-fallbacks": ["@emotion/use-insertion-effect-with-fallbacks@1.2.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg=="],
|
||||
|
||||
"@emotion/utils": ["@emotion/utils@1.4.2", "", {}, "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="],
|
||||
|
||||
"@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@hello-pangea/dnd": ["@hello-pangea/dnd@16.6.0", "", { "dependencies": { "@babel/runtime": "^7.24.1", "css-box-model": "^1.2.1", "memoize-one": "^6.0.0", "raf-schd": "^4.0.3", "react-redux": "^8.1.3", "redux": "^4.2.1", "use-memo-one": "^1.1.3" }, "peerDependencies": { "react": "^16.8.5 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" } }, "sha512-vfZ4GydqbtUPXSLfAvKvXQ6xwRzIjUSjVU0Sx+70VOhc2xx6CdmJXJ8YhH70RpbTUGjxctslQTHul9sIOxCfFQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
|
||||
|
||||
"@mui/core-downloads-tracker": ["@mui/core-downloads-tracker@5.18.0", "", {}, "sha512-jbhwoQ1AY200PSSOrNXmrFCaSDSJWP7qk6urkTmIirvRXDROkqe+QwcLlUiw/PrREwsIF/vm3/dAXvjlMHF0RA=="],
|
||||
|
||||
"@mui/icons-material": ["@mui/icons-material@5.18.0", "", { "dependencies": { "@babel/runtime": "^7.23.9" }, "peerDependencies": { "@mui/material": "^5.0.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-1s0vEZj5XFXDMmz3Arl/R7IncFqJ+WQ95LDp1roHWGDE2oCO3IS4/hmiOv1/8SD9r6B7tv9GLiqVZYHo+6PkTg=="],
|
||||
|
||||
"@mui/material": ["@mui/material@5.18.0", "", { "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.18.0", "@mui/system": "^5.18.0", "@mui/types": "~7.2.15", "@mui/utils": "^5.17.1", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1", "react-is": "^19.0.0", "react-transition-group": "^4.4.5" }, "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled", "@types/react"] }, "sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA=="],
|
||||
|
||||
"@mui/private-theming": ["@mui/private-theming@5.17.1", "", { "dependencies": { "@babel/runtime": "^7.23.9", "@mui/utils": "^5.17.1", "prop-types": "^15.8.1" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ=="],
|
||||
|
||||
"@mui/styled-engine": ["@mui/styled-engine@5.18.0", "", { "dependencies": { "@babel/runtime": "^7.23.9", "@emotion/cache": "^11.13.5", "@emotion/serialize": "^1.3.3", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "peerDependencies": { "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled"] }, "sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg=="],
|
||||
|
||||
"@mui/system": ["@mui/system@5.18.0", "", { "dependencies": { "@babel/runtime": "^7.23.9", "@mui/private-theming": "^5.17.1", "@mui/styled-engine": "^5.18.0", "@mui/types": "~7.2.15", "@mui/utils": "^5.17.1", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled", "@types/react"] }, "sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw=="],
|
||||
|
||||
"@mui/types": ["@mui/types@7.2.24", "", { "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw=="],
|
||||
|
||||
"@mui/utils": ["@mui/utils@5.17.1", "", { "dependencies": { "@babel/runtime": "^7.23.9", "@mui/types": "~7.2.15", "@types/prop-types": "^15.7.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-is": "^19.0.0" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg=="],
|
||||
|
||||
"@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
|
||||
|
||||
"@react-leaflet/core": ["@react-leaflet/core@2.1.0", "", { "peerDependencies": { "leaflet": "^1.9.0", "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.0", "", { "os": "none", "cpu": "arm64" }, "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
||||
|
||||
"@types/hoist-non-react-statics": ["@types/hoist-non-react-statics@3.3.7", "", { "dependencies": { "hoist-non-react-statics": "^3.3.0" }, "peerDependencies": { "@types/react": "*" } }, "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g=="],
|
||||
|
||||
"@types/leaflet": ["@types/leaflet@1.9.21", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w=="],
|
||||
|
||||
"@types/leaflet-draw": ["@types/leaflet-draw@1.0.13", "", { "dependencies": { "@types/leaflet": "^1.9" } }, "sha512-YU82kilOaU+wPNbqKCCDfHH3hqepN6XilrBwG/mSeZ/z4ewumaRCOah44s3FMxSu/Aa0SVa3PPJvhIZDUA09mw=="],
|
||||
|
||||
"@types/leaflet-polylinedecorator": ["@types/leaflet-polylinedecorator@1.6.5", "", { "dependencies": { "@types/leaflet": "^1.9" } }, "sha512-m3hMuCyii8t7N/t1xc9aMzpA/tTnc/WFq63yR334Fgbw4jDytTCUcTNvACmod6bnZl5oCigqyTd7Pbb+VQtGZQ=="],
|
||||
|
||||
"@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
|
||||
|
||||
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
|
||||
|
||||
"@types/react": ["@types/react@18.3.28", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
|
||||
|
||||
"@types/react-transition-group": ["@types/react-transition-group@4.4.12", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="],
|
||||
|
||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.3", "", {}, "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||
|
||||
"babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="],
|
||||
|
||||
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
|
||||
|
||||
"css-box-model": ["css-box-model@1.2.1", "", { "dependencies": { "tiny-invariant": "^1.0.6" } }, "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw=="],
|
||||
|
||||
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
|
||||
|
||||
"css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
|
||||
|
||||
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
|
||||
|
||||
"csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
|
||||
|
||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||
|
||||
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
|
||||
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||
|
||||
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.325", "", {}, "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"leaflet": ["leaflet@1.9.4", "", {}, "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="],
|
||||
|
||||
"leaflet-draw": ["leaflet-draw@1.0.4", "", {}, "sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ=="],
|
||||
|
||||
"leaflet-polylinedecorator": ["leaflet-polylinedecorator@1.6.0", "", { "dependencies": { "leaflet-rotatedmarker": "^0.2.0" } }, "sha512-kn3krmZRetgvN0wjhgYL8kvyLS0tUogAl0vtHuXQnwlYNjbl7aLQpkoFUo8UB8gVZoB0dhI4Tb55VdTJAcYzzQ=="],
|
||||
|
||||
"leaflet-rotatedmarker": ["leaflet-rotatedmarker@0.2.0", "", {}, "sha512-yc97gxLXwbZa+Gk9VCcqI0CkvIBC9oNTTjFsHqq4EQvANrvaboib4UdeQLyTnEqDpaXHCqzwwVIDHtvz2mUiDg=="],
|
||||
|
||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
|
||||
|
||||
"memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
|
||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||
|
||||
"raf-schd": ["raf-schd@4.0.3", "", {}, "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="],
|
||||
|
||||
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
|
||||
|
||||
"react-chartjs-2": ["react-chartjs-2@5.3.1", "", { "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A=="],
|
||||
|
||||
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
|
||||
|
||||
"react-icons": ["react-icons@5.6.0", "", { "peerDependencies": { "react": "*" } }, "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA=="],
|
||||
|
||||
"react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="],
|
||||
|
||||
"react-leaflet": ["react-leaflet@4.2.1", "", { "dependencies": { "@react-leaflet/core": "^2.1.0" }, "peerDependencies": { "leaflet": "^1.9.0", "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q=="],
|
||||
|
||||
"react-redux": ["react-redux@8.1.3", "", { "dependencies": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", "@types/use-sync-external-store": "^0.0.3", "hoist-non-react-statics": "^3.3.2", "react-is": "^18.0.0", "use-sync-external-store": "^1.0.0" }, "peerDependencies": { "@types/react": "^16.8 || ^17.0 || ^18.0", "@types/react-dom": "^16.8 || ^17.0 || ^18.0", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0", "react-native": ">=0.59", "redux": "^4 || ^5.0.0-beta.0" }, "optionalPeers": ["@types/react", "@types/react-dom", "react-dom", "react-native", "redux"] }, "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||
|
||||
"react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
|
||||
|
||||
"react-world-flags": ["react-world-flags@1.6.0", "", { "dependencies": { "svg-country-flags": "^1.2.10", "svgo": "^3.0.2", "world-countries": "^5.0.0" }, "peerDependencies": { "react": ">=0.14" } }, "sha512-eutSeAy5YKoVh14js/JUCSlA6EBk1n4k+bDaV+NkNB50VhnG+f4QDTpYycnTUTsZ5cqw/saPmk0Z4Fa0VVZ1Iw=="],
|
||||
|
||||
"redux": ["redux@4.2.1", "", { "dependencies": { "@babel/runtime": "^7.9.2" } }, "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w=="],
|
||||
|
||||
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.0", "@rollup/rollup-android-arm64": "4.60.0", "@rollup/rollup-darwin-arm64": "4.60.0", "@rollup/rollup-darwin-x64": "4.60.0", "@rollup/rollup-freebsd-arm64": "4.60.0", "@rollup/rollup-freebsd-x64": "4.60.0", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", "@rollup/rollup-linux-arm-musleabihf": "4.60.0", "@rollup/rollup-linux-arm64-gnu": "4.60.0", "@rollup/rollup-linux-arm64-musl": "4.60.0", "@rollup/rollup-linux-loong64-gnu": "4.60.0", "@rollup/rollup-linux-loong64-musl": "4.60.0", "@rollup/rollup-linux-ppc64-gnu": "4.60.0", "@rollup/rollup-linux-ppc64-musl": "4.60.0", "@rollup/rollup-linux-riscv64-gnu": "4.60.0", "@rollup/rollup-linux-riscv64-musl": "4.60.0", "@rollup/rollup-linux-s390x-gnu": "4.60.0", "@rollup/rollup-linux-x64-gnu": "4.60.0", "@rollup/rollup-linux-x64-musl": "4.60.0", "@rollup/rollup-openbsd-x64": "4.60.0", "@rollup/rollup-openharmony-arm64": "4.60.0", "@rollup/rollup-win32-arm64-msvc": "4.60.0", "@rollup/rollup-win32-ia32-msvc": "4.60.0", "@rollup/rollup-win32-x64-gnu": "4.60.0", "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ=="],
|
||||
|
||||
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
||||
|
||||
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"svg-country-flags": ["svg-country-flags@1.2.10", "", {}, "sha512-xrqwo0TYf/h2cfPvGpjdSuSguUbri4vNNizBnwzoZnX0xGo3O5nGJMlbYEp7NOYcnPGBm6LE2axqDWSB847bLw=="],
|
||||
|
||||
"svgo": ["svgo@3.3.3", "", { "dependencies": { "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0", "sax": "^1.5.0" }, "bin": "./bin/svgo" }, "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng=="],
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"use-memo-one": ["use-memo-one@1.1.3", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ=="],
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||
|
||||
"vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
||||
|
||||
"world-countries": ["world-countries@5.1.0", "", {}, "sha512-CXR6EBvTbArDlDDIWU3gfKb7Qk0ck2WNZ234b/A0vuecPzIfzzxH+O6Ejnvg1sT8XuiZjVlzOH0h08ZtaO7g0w=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="],
|
||||
|
||||
"@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
|
||||
|
||||
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||
|
||||
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"react-redux/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||
}
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
bun run build
|
||||
scp -i "D:\sync\keys\azaion\.ssh\azaion_server_id" -r dist/* root@188.245.120.247:/var/www/mission-planner
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
cd /etc/nginx/sites-available
|
||||
|
||||
tee -a missions.azaion.com << END
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name missions.azaion.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/missions.azaion.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/missions.azaion.com/privkey.pem;
|
||||
|
||||
root /var/www/mission-planner;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files \$uri /index.html;
|
||||
}
|
||||
|
||||
error_page 404 /index.html;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name missions.azaion.com;
|
||||
|
||||
# Redirect all HTTP requests to HTTPS
|
||||
return 301 https://\$host\$request_uri;
|
||||
}
|
||||
END
|
||||
ln -s /etc/nginx/sites-available/missions.azaion.com /etc/nginx/sites-enabled/
|
||||
|
||||
# create certs
|
||||
certbot --nginx -d missions.azaion.com
|
||||
|
||||
mkdir /var/www/mission-planner
|
||||
sudo chown -R www-data:www-data /var/www/mission-planner
|
||||
sudo chmod -R 755 /var/www/mission-planner
|
||||
|
||||
systemctl restart nginx
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Tool for setup waypoints and zones for flight missions"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/logo128.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>Azaion Mission Planner</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "mission-planner",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@mui/icons-material": "^5.15.19",
|
||||
"@mui/material": "^5.16.7",
|
||||
"chart.js": "^4.4.4",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-draw": "^1.0.4",
|
||||
"leaflet-polylinedecorator": "^1.6.0",
|
||||
"react": "^18.3.1",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-world-flags": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/leaflet": "^1.9.12",
|
||||
"@types/leaflet-draw": "^1.0.12",
|
||||
"@types/leaflet-polylinedecorator": "^1.6.4",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.5"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "Azaion Mission Planner",
|
||||
"name": "Azaion Mission Planner",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo128.png",
|
||||
"type": "image/png",
|
||||
"sizes": "128x128"
|
||||
},
|
||||
{
|
||||
"src": "logo256.png",
|
||||
"type": "image/png",
|
||||
"sizes": "256x256"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,2 @@
|
||||
export const COORDINATE_PRECISION = 8;
|
||||
export const GOOGLE_GEOCODE_KEY = 'AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys';
|
||||
@@ -0,0 +1,5 @@
|
||||
export const actionModes = {
|
||||
points: 'points',
|
||||
workArea: 'workArea',
|
||||
prohibitedArea: 'prohibitedArea',
|
||||
} as const;
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Language } from '../types';
|
||||
|
||||
export const languages: Language[] = [
|
||||
{
|
||||
code: 'en',
|
||||
flag: 'US',
|
||||
},
|
||||
{
|
||||
code: 'ua',
|
||||
flag: 'UA',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,4 @@
|
||||
export const mapTypes = {
|
||||
classic: 'classic',
|
||||
satellite: 'satellite',
|
||||
} as const;
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Purpose } from '../types';
|
||||
|
||||
export const purposes: Purpose[] = [
|
||||
{
|
||||
value: 'tank',
|
||||
label: 'Tank',
|
||||
},
|
||||
{
|
||||
value: 'artillery',
|
||||
label: 'Artillery',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,4 @@
|
||||
export const tileUrls = {
|
||||
classic: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
satellite: import.meta.env.VITE_SATELLITE_TILE_URL || 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import type { TranslationsMap } from '../types';
|
||||
|
||||
export const translations: TranslationsMap = {
|
||||
en: {
|
||||
language: 'Language',
|
||||
aircraft: 'Aircraft',
|
||||
label: 'Altitude (meters)',
|
||||
point: 'Point',
|
||||
height: 'Altitude',
|
||||
edit: 'Edit',
|
||||
currentPos: 'Current Position',
|
||||
return: 'Return Point',
|
||||
addPoints: 'Point',
|
||||
workArea: 'Work Area',
|
||||
prohibitedArea: 'Prohibited Area',
|
||||
location: 'Location',
|
||||
currentLocation: 'current location',
|
||||
exportData: 'Get Data',
|
||||
exportPlaneData: 'Export route',
|
||||
exportMapData: 'Export map',
|
||||
editAsJson: 'Edit as JSON',
|
||||
importFromJson: 'Import',
|
||||
export: 'Export',
|
||||
import: 'Import',
|
||||
operations: 'Operations',
|
||||
rectangleColor: 'Zone Color',
|
||||
red: 'Red',
|
||||
green: 'Green',
|
||||
initialAltitude: 'Initial Altitude',
|
||||
setAltitude: 'Set Altitude',
|
||||
setPoint: 'Set return Point',
|
||||
removePoint: 'Delete point',
|
||||
windSpeed: 'Wind Speed (km/h)',
|
||||
windDirection: 'Wind Direction (degrees)',
|
||||
setWind: 'Set Wind Parameters',
|
||||
title: 'Total Distance',
|
||||
distanceLabel: 'Total Distance:',
|
||||
fuelRequiredLabel: 'Fuel Required:',
|
||||
maxFuelLabel: 'Max Fuel Capacity:',
|
||||
flightStatus: {
|
||||
good: 'Can complete',
|
||||
caution: 'Can complete, but caution is advised.',
|
||||
low: 'Can`t complete.',
|
||||
},
|
||||
calc: 'calculated',
|
||||
error: 'Error calculating distance',
|
||||
km: 'km',
|
||||
metres: 'm.',
|
||||
litres: 'liters',
|
||||
hour: 'h',
|
||||
minutes: 'min',
|
||||
battery: 'bat.',
|
||||
titleAdd: 'Add New Point',
|
||||
titleEdit: 'Edit Point',
|
||||
description: 'Enter the coordinates, altitude, and purpose of the point.',
|
||||
latitude: 'Latitude',
|
||||
longitude: 'Longitude',
|
||||
altitude: 'Altitude',
|
||||
purpose: 'Purpose',
|
||||
cancel: 'Cancel',
|
||||
submitAdd: 'Add Point',
|
||||
submitEdit: 'Save Changes',
|
||||
options: {
|
||||
artillery: 'Artillery',
|
||||
tank: 'Tank',
|
||||
},
|
||||
invalid: 'Invalid JSON format',
|
||||
editm: 'Edit the JSON data as needed.',
|
||||
save: 'Save',
|
||||
},
|
||||
ua: {
|
||||
language: 'Мова',
|
||||
aircraft: 'Літак',
|
||||
label: 'Висота (метри)',
|
||||
point: 'Точка',
|
||||
height: 'Висота',
|
||||
edit: 'Редагувати',
|
||||
currentPos: 'Поточна позиція',
|
||||
return: 'Точка повернення',
|
||||
addPoints: 'Точка',
|
||||
workArea: 'Робоча зона',
|
||||
prohibitedArea: 'Заборонена зона',
|
||||
location: 'Місцезнаходження',
|
||||
currentLocation: 'поточне місцезнаходження',
|
||||
exportData: 'Отримати дані',
|
||||
exportPlaneData: 'Export route',
|
||||
exportMapData: 'Export map',
|
||||
editAsJson: 'Редагувати як JSON',
|
||||
importFromJson: 'Імпорт',
|
||||
export: 'Експорт',
|
||||
import: 'Імпорт',
|
||||
operations: 'Операції',
|
||||
rectangleColor: 'Колір зони',
|
||||
red: 'Червоний',
|
||||
green: 'Зелений',
|
||||
initialAltitude: 'Початкова висота',
|
||||
setAltitude: 'Встановити висоту',
|
||||
setPoint: 'Встановити точку повернення',
|
||||
removePoint: 'Видалити точку',
|
||||
windSpeed: 'Швидкість вітру (км/г)',
|
||||
windDirection: 'Напрямок вітру (градуси)',
|
||||
setWind: 'Встановити параметри вітру',
|
||||
title: 'Загальна відстань',
|
||||
distanceLabel: 'Загальна відстань:',
|
||||
fuelRequiredLabel: 'Необхідне пальне:',
|
||||
maxFuelLabel: 'Максимальна ємність пального:',
|
||||
flightStatus: {
|
||||
good: 'Долетить',
|
||||
caution: 'Долетить, але є ризики.',
|
||||
low: 'Не долетить.',
|
||||
},
|
||||
calc: 'розрахункова',
|
||||
error: 'Помилка при розрахунку відстані',
|
||||
km: 'км.',
|
||||
metres: 'м.',
|
||||
litres: 'л.',
|
||||
hour: 'год.',
|
||||
minutes: 'хв.',
|
||||
battery: 'бат.',
|
||||
titleAdd: 'Додати нову точку',
|
||||
titleEdit: 'Редагувати точку',
|
||||
description: 'Введіть координати, висоту та мету точки.',
|
||||
latitude: 'Широта',
|
||||
longitude: 'Довгота',
|
||||
altitude: 'Висота',
|
||||
purpose: 'Мета',
|
||||
cancel: 'Скасувати',
|
||||
submitAdd: 'Додати точку',
|
||||
submitEdit: 'Зберегти зміни',
|
||||
options: {
|
||||
artillery: 'Артилерія',
|
||||
tank: 'Танк',
|
||||
},
|
||||
invalid: 'Невірний JSON формат',
|
||||
editm: 'Відредагуйте JSON дані за потреби.',
|
||||
save: 'Зберегти',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { newGuid } from '../utils';
|
||||
|
||||
export const AircraftType = {
|
||||
PLANE: 'Plane',
|
||||
VTOL: 'VTOL',
|
||||
} as const;
|
||||
|
||||
export type AircraftTypeValue = (typeof AircraftType)[keyof typeof AircraftType];
|
||||
|
||||
export class Aircraft {
|
||||
name: string;
|
||||
fuelperkm: number;
|
||||
maxfuel: number;
|
||||
type: AircraftTypeValue;
|
||||
downang: number | undefined;
|
||||
upang: number | undefined;
|
||||
id: string;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
fuelperkm: number,
|
||||
maxfuel: number,
|
||||
type: AircraftTypeValue,
|
||||
downang?: number,
|
||||
upang?: number,
|
||||
id: string = newGuid(),
|
||||
) {
|
||||
this.name = name;
|
||||
this.fuelperkm = fuelperkm;
|
||||
this.maxfuel = maxfuel;
|
||||
this.type = type;
|
||||
|
||||
if (type === AircraftType.PLANE) {
|
||||
this.downang = downang;
|
||||
this.upang = upang;
|
||||
} else {
|
||||
this.downang = undefined;
|
||||
this.upang = undefined;
|
||||
}
|
||||
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import 'chart.js/auto';
|
||||
import { useLanguage } from './LanguageContext';
|
||||
import { translations } from '../constants/translations';
|
||||
import type { FlightPoint } from '../types';
|
||||
|
||||
interface AltitudeChartProps {
|
||||
points: FlightPoint[];
|
||||
}
|
||||
|
||||
export default function AltitudeChart({ points }: AltitudeChartProps) {
|
||||
const { targetLanguage } = useLanguage();
|
||||
const t = translations[targetLanguage];
|
||||
|
||||
const getFontSize = () => {
|
||||
const screenWidth = window.innerWidth;
|
||||
return screenWidth > 1180 ? 12 : 8;
|
||||
};
|
||||
|
||||
const data = {
|
||||
labels: points.map((_, index) => index + 1),
|
||||
datasets: [{
|
||||
label: t.label,
|
||||
data: points.map(point => point.altitude),
|
||||
borderColor: 'rgb(0, 0, 225)',
|
||||
backgroundColor: 'rgba(0, 0, 225, 0.2)',
|
||||
pointBackgroundColor: 'rgb(255, 195, 0)',
|
||||
pointBorderColor: 'rgb(0, 0, 0)',
|
||||
pointBorderWidth: 1,
|
||||
tension: 0.1,
|
||||
}],
|
||||
};
|
||||
|
||||
const options = {
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
font: { size: getFontSize() },
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { font: { size: getFontSize() } },
|
||||
},
|
||||
y: {
|
||||
ticks: { font: { size: getFontSize() } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return <Line data={data} options={options} />;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle,
|
||||
Button, TextField, FormControl, InputLabel, Select, MenuItem, Checkbox, ListItemText,
|
||||
type SelectChangeEvent,
|
||||
} from '@mui/material';
|
||||
import { useLanguage } from './LanguageContext';
|
||||
import { COORDINATE_PRECISION } from '../config';
|
||||
import { translations } from '../constants/translations';
|
||||
import { purposes } from '../constants/purposes';
|
||||
|
||||
interface AltitudeDialogProps {
|
||||
openDialog: boolean;
|
||||
handleDialogClose: () => void;
|
||||
handleAltitudeSubmit: () => void;
|
||||
altitude: number;
|
||||
setAltitude: (value: number) => void;
|
||||
latitude: number;
|
||||
setLatitude: (value: number) => void;
|
||||
longitude: number;
|
||||
setLongitude: (value: number) => void;
|
||||
meta: string[];
|
||||
setMeta: (value: string[]) => void;
|
||||
isEditMode?: boolean;
|
||||
}
|
||||
|
||||
export default function AltitudeDialog({
|
||||
openDialog,
|
||||
handleDialogClose,
|
||||
handleAltitudeSubmit,
|
||||
altitude,
|
||||
setAltitude,
|
||||
latitude,
|
||||
setLatitude,
|
||||
longitude,
|
||||
setLongitude,
|
||||
meta,
|
||||
setMeta,
|
||||
isEditMode,
|
||||
}: AltitudeDialogProps) {
|
||||
const { targetLanguage } = useLanguage();
|
||||
const t = translations[targetLanguage];
|
||||
|
||||
const handleMetaChange = (event: SelectChangeEvent<string[]>) => {
|
||||
const { value } = event.target;
|
||||
setMeta(typeof value === 'string' ? value.split(',') : value);
|
||||
};
|
||||
|
||||
const handleLatitudeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
if (!isNaN(Number(value))) {
|
||||
const roundedValue = parseFloat(parseFloat(value).toFixed(COORDINATE_PRECISION));
|
||||
setLatitude(roundedValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLongitudeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
if (!isNaN(Number(value))) {
|
||||
const roundedValue = parseFloat(parseFloat(value).toFixed(COORDINATE_PRECISION));
|
||||
setLongitude(roundedValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setMeta([purposes[0].value, purposes[1].value]);
|
||||
handleDialogClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={openDialog} onClose={handleClose}>
|
||||
<DialogTitle>{isEditMode ? t.titleEdit : t.titleAdd}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t.description}
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
margin="dense"
|
||||
label={t.latitude}
|
||||
type="number"
|
||||
fullWidth
|
||||
value={Number(latitude).toFixed(COORDINATE_PRECISION)}
|
||||
onChange={handleLatitudeChange}
|
||||
inputProps={{ maxLength: 12 }}
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
label={t.longitude}
|
||||
type="number"
|
||||
fullWidth
|
||||
value={Number(longitude).toFixed(COORDINATE_PRECISION)}
|
||||
onChange={handleLongitudeChange}
|
||||
inputProps={{ maxLength: 12 }}
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
label={t.altitude}
|
||||
type="number"
|
||||
fullWidth
|
||||
value={altitude}
|
||||
onChange={(e) => setAltitude(Number(e.target.value))}
|
||||
/>
|
||||
|
||||
<FormControl fullWidth margin="dense">
|
||||
<InputLabel id="purpose-label">{t.purpose}</InputLabel>
|
||||
<Select
|
||||
labelId="purpose-label"
|
||||
label={t.purpose}
|
||||
multiple
|
||||
value={meta}
|
||||
onChange={handleMetaChange}
|
||||
renderValue={(selected) => selected.map(val => t.options[val]).join(', ')}
|
||||
>
|
||||
{purposes.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
<Checkbox checked={meta.indexOf(option.value) > -1} />
|
||||
<ListItemText primary={t.options[option.value]} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
{t.cancel}
|
||||
</Button>
|
||||
<Button onClick={handleAltitudeSubmit} color="primary">
|
||||
{isEditMode ? t.submitEdit : t.submitAdd}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet-draw';
|
||||
import { useMap } from 'react-leaflet';
|
||||
import type { MapRectangle } from '../types';
|
||||
|
||||
interface DrawControlProps {
|
||||
color: string;
|
||||
rectangles: MapRectangle[];
|
||||
setRectangles: React.Dispatch<React.SetStateAction<MapRectangle[]>>;
|
||||
controlChange: (control: L.Control.Draw) => void;
|
||||
}
|
||||
|
||||
export default function DrawControl({ color, rectangles, setRectangles, controlChange }: DrawControlProps) {
|
||||
const map = useMap();
|
||||
const drawControlRef = useRef<L.Control.Draw | null>(null);
|
||||
const editableLayerRef = useRef(new L.FeatureGroup());
|
||||
|
||||
useEffect(() => {
|
||||
if (drawControlRef.current) {
|
||||
map.removeControl(drawControlRef.current);
|
||||
}
|
||||
|
||||
const editableLayer = editableLayerRef.current;
|
||||
map.addLayer(editableLayer);
|
||||
|
||||
const drawControl = new L.Control.Draw({
|
||||
draw: {
|
||||
polygon: false,
|
||||
polyline: false,
|
||||
rectangle: {},
|
||||
circle: false,
|
||||
circlemarker: false,
|
||||
marker: false,
|
||||
},
|
||||
edit: {
|
||||
featureGroup: editableLayer,
|
||||
remove: true,
|
||||
},
|
||||
});
|
||||
controlChange(drawControl);
|
||||
|
||||
drawControlRef.current = drawControl;
|
||||
map.addControl(drawControl);
|
||||
|
||||
map.on(L.Draw.Event.CREATED, handleDrawCreated);
|
||||
map.on(L.Draw.Event.EDITED, handleDrawEdited);
|
||||
map.on(L.Draw.Event.DELETED, handleDrawDeleted);
|
||||
|
||||
return () => {
|
||||
if (drawControlRef.current) {
|
||||
map.removeControl(drawControlRef.current);
|
||||
}
|
||||
map.off(L.Draw.Event.CREATED, handleDrawCreated);
|
||||
map.off(L.Draw.Event.EDITED, handleDrawEdited);
|
||||
map.off(L.Draw.Event.DELETED, handleDrawDeleted);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [color, map, setRectangles]);
|
||||
|
||||
const handleDrawCreated = (e: L.LeafletEvent) => {
|
||||
const event = e as L.DrawEvents.Created;
|
||||
const layer = event.layer;
|
||||
const bounds = (layer as L.Rectangle).getBounds();
|
||||
const newRectangle: MapRectangle = { layer, color, bounds };
|
||||
|
||||
const isDuplicate = rectangles.some(
|
||||
(rectangle) => rectangle.bounds && (rectangle.bounds as L.LatLngBounds).equals?.(bounds)
|
||||
);
|
||||
|
||||
if (!isDuplicate) {
|
||||
(layer as L.Path).setStyle({ color: color });
|
||||
setRectangles(prevRectangles => [...prevRectangles, newRectangle]);
|
||||
editableLayerRef.current.addLayer(layer);
|
||||
} else {
|
||||
layer.remove();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrawEdited = (e: L.LeafletEvent) => {
|
||||
const event = e as L.DrawEvents.Edited;
|
||||
const layers = event.layers;
|
||||
layers.eachLayer((layer: L.Layer) => {
|
||||
setRectangles(prevRectangles =>
|
||||
prevRectangles.map(rectangle =>
|
||||
rectangle.layer === layer
|
||||
? { ...rectangle, layer }
|
||||
: rectangle
|
||||
)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDrawDeleted = (e: L.LeafletEvent) => {
|
||||
const event = e as L.DrawEvents.Deleted;
|
||||
const layers = event.layers;
|
||||
layers.eachLayer((layer: L.Layer) => {
|
||||
setRectangles(prevRectangles =>
|
||||
prevRectangles.filter(rectangle => rectangle.layer !== layer)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useLanguage } from './LanguageContext';
|
||||
import { translations } from '../constants/translations';
|
||||
|
||||
interface JsonEditorDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
jsonText: string;
|
||||
onSave: (json: string) => void;
|
||||
}
|
||||
|
||||
const JsonEditorDialog = ({ open, onClose, jsonText, onSave }: JsonEditorDialogProps) => {
|
||||
const [editedJson, setEditedJson] = useState(jsonText);
|
||||
const [isValid, setIsValid] = useState(true);
|
||||
const { targetLanguage } = useLanguage();
|
||||
const t = translations[targetLanguage];
|
||||
|
||||
useEffect(() => {
|
||||
setEditedJson(jsonText);
|
||||
}, [jsonText]);
|
||||
|
||||
const handleJsonChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEditedJson(e.target.value);
|
||||
|
||||
try {
|
||||
JSON.parse(e.target.value);
|
||||
setIsValid(true);
|
||||
} catch {
|
||||
setIsValid(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (isValid) {
|
||||
onSave(editedJson);
|
||||
} else {
|
||||
alert(t.invalid);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} fullWidth maxWidth="md">
|
||||
<DialogTitle>{t.edit}</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
multiline
|
||||
rows={20}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={editedJson}
|
||||
onChange={handleJsonChange}
|
||||
helperText={!isValid ? t.invalid : t.editm}
|
||||
error={!isValid}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color="primary">
|
||||
{t.cancel}
|
||||
</Button>
|
||||
<Button onClick={handleSave} color="primary" disabled={!isValid}>
|
||||
{t.save}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default JsonEditorDialog;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createContext, useContext, useState, type ReactNode } from 'react';
|
||||
|
||||
interface LanguageContextType {
|
||||
targetLanguage: string;
|
||||
toggleLanguage: (lang: string) => void;
|
||||
}
|
||||
|
||||
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||
|
||||
export const useLanguage = (): LanguageContextType => {
|
||||
const context = useContext(LanguageContext);
|
||||
if (!context) {
|
||||
throw new Error('useLanguage must be used within a LanguageProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface LanguageProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const LanguageProvider = ({ children }: LanguageProviderProps) => {
|
||||
const [targetLanguage, setTargetLanguage] = useState('en');
|
||||
|
||||
const toggleLanguage = (lang: string) => {
|
||||
setTargetLanguage(lang);
|
||||
};
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={{ targetLanguage, toggleLanguage }}>
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
.editor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#language-label {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.flag {
|
||||
width: 24px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
@media (min-width: 680px) and (max-width: 1024px) and (orientation: landscape){
|
||||
#language-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.flag {
|
||||
width: 18px!important;
|
||||
height: 12px!important;
|
||||
}
|
||||
|
||||
.language-selector .MuiSelect-select{
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import Flag from 'react-world-flags';
|
||||
import { useLanguage } from './LanguageContext';
|
||||
import { InputLabel, MenuItem, Select } from '@mui/material';
|
||||
import { languages } from '../constants/languages';
|
||||
import { translations } from '../constants/translations';
|
||||
import './LanguageSwitcher.css';
|
||||
|
||||
const LanguageSwitcher = () => {
|
||||
const { targetLanguage, toggleLanguage } = useLanguage();
|
||||
const t = translations[targetLanguage];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InputLabel id='language-label'>{t.language}</InputLabel>
|
||||
<Select
|
||||
className='language-selector'
|
||||
labelId='language-label'
|
||||
value={targetLanguage}
|
||||
>
|
||||
{languages.map((language, index) => (
|
||||
<MenuItem key={index} value={language.code} onClick={() => toggleLanguage(language.code)}>
|
||||
<Flag className='flag' code={language.flag} style={{ width: '24px', height: '16px' }} />
|
||||
{language.code.toUpperCase()}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSwitcher;
|
||||
@@ -0,0 +1,233 @@
|
||||
.left-board {
|
||||
min-width: 25%;
|
||||
max-width: 33%;
|
||||
height: 100vh;
|
||||
background-color: #c7c7c6;
|
||||
border: 1px solid #333333;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-menu {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: end;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.selectors {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
#aircraft-label {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#altitude-label {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.actions-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin: 8px 0;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.action-buttons-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-buttons-row .btn {
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex !important;
|
||||
font-size: 0.8rem !important;
|
||||
justify-content: space-evenly !important;
|
||||
padding: 4px !important;
|
||||
border: 2px solid #1976d2 !important;
|
||||
}
|
||||
|
||||
.location-search {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.location-search #location-label {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.current-location-label {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
margin-top: 6px;
|
||||
padding-left: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background-color: #c7c7c6 !important;
|
||||
}
|
||||
|
||||
.active-mode-btn {
|
||||
cursor: default !important;
|
||||
background-color: #1565c0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.arrow-btn {
|
||||
min-width: 56px !important;
|
||||
width: 10%;
|
||||
aspect-ratio: 1/1;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
max-width: 36px;
|
||||
background-color: #c7c7c6;
|
||||
padding: 20px 0.5%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 0 !important;
|
||||
min-width: unset !important;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.active-toolbar-btn {
|
||||
padding: 4px !important;
|
||||
min-width: unset !important;
|
||||
aspect-ratio: 1 / 1;
|
||||
background-color: #1976d2 !important;
|
||||
}
|
||||
|
||||
#point-btn {
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.action-btn-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.operations-section {
|
||||
margin: 12px 0;
|
||||
margin-top: auto;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.operations-section #operations-label {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.section-buttons-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-btn {
|
||||
flex: 1;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
@media (min-width: 680px) and (max-width: 1024px) and (orientation: landscape) {
|
||||
.left-board {
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.top-menu {
|
||||
margin: 4px 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.selectors {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
#altitude-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.editor-container .MuiOutlinedInput-input {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.arrow-btn {
|
||||
min-width: 35px !important;
|
||||
}
|
||||
|
||||
.actions-block {
|
||||
margin: 2px 0;
|
||||
margin-bottom: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-buttons-row {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.action-buttons-row .btn {
|
||||
width: 33%;
|
||||
font-size: 0.5rem !important;
|
||||
padding: 1px 3px !important;
|
||||
line-height: 1 !important;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.location-search #location-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.location-search .MuiOutlinedInput-input {
|
||||
padding: 6px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.current-location-label {
|
||||
font-size: 10px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.action-btn-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.operations-section {
|
||||
margin: 6px 0;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.operations-section #operations-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.section-buttons-row {
|
||||
gap: 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.section-btn {
|
||||
flex: 1;
|
||||
font-size: 0.6rem !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import { useState } from 'react';
|
||||
import { useLanguage } from './LanguageContext';
|
||||
import Button from '@mui/material/Button';
|
||||
import { FormControl, TextField, InputLabel } from '@mui/material';
|
||||
import { DragDropContext, type DropResult } from '@hello-pangea/dnd';
|
||||
import PointsList from './PointsList';
|
||||
import AltitudeChart from './AltitudeChart';
|
||||
import TotalDistance from './TotalDistance';
|
||||
import LanguageSwitcher from './LanguageSwitcher';
|
||||
import { translations } from '../constants/translations';
|
||||
import './LeftBoard.css';
|
||||
import { actionModes } from '../constants/actionModes';
|
||||
import { DashedAreaIcon, HideSidebarIcon, ShowSidebarIcon } from '../icons/SidebarIcons';
|
||||
import { FaLocationDot } from 'react-icons/fa6';
|
||||
import { GOOGLE_GEOCODE_KEY } from '../config';
|
||||
import type { FlightPoint, CalculatedPointInfo, AircraftParams, LatLngPosition } from '../types';
|
||||
|
||||
interface LeftBoardProps {
|
||||
points: FlightPoint[];
|
||||
setPoints: React.Dispatch<React.SetStateAction<FlightPoint[]>>;
|
||||
calculatedPointInfo: CalculatedPointInfo[];
|
||||
setCalculatedPointInfo: React.Dispatch<React.SetStateAction<CalculatedPointInfo[]>>;
|
||||
aircraft: AircraftParams | null;
|
||||
initialAltitude: number;
|
||||
setInitialAltitude: (value: number) => void;
|
||||
currentAltitude: number;
|
||||
actionMode: string;
|
||||
setActionMode: (mode: string) => void;
|
||||
setRectangleColor: (color: string) => void;
|
||||
editAsJson: () => void;
|
||||
exportMap: () => void;
|
||||
updatePosition: (pos: LatLngPosition) => void;
|
||||
currentPosition: LatLngPosition;
|
||||
}
|
||||
|
||||
export default function LeftBoard({
|
||||
points,
|
||||
setPoints,
|
||||
calculatedPointInfo,
|
||||
setCalculatedPointInfo,
|
||||
aircraft,
|
||||
initialAltitude,
|
||||
setInitialAltitude,
|
||||
currentAltitude,
|
||||
actionMode,
|
||||
setActionMode,
|
||||
setRectangleColor,
|
||||
editAsJson,
|
||||
exportMap,
|
||||
updatePosition,
|
||||
currentPosition,
|
||||
}: LeftBoardProps) {
|
||||
const { targetLanguage } = useLanguage();
|
||||
const t = translations[targetLanguage];
|
||||
const [isShowed, setIsShowed] = useState(true);
|
||||
const [locationInput, setLocationInput] = useState('');
|
||||
|
||||
const changeShowing = () => {
|
||||
setIsShowed(!isShowed);
|
||||
};
|
||||
|
||||
const removePoint = (id: string) => {
|
||||
const index = points.findIndex((point) => point.id === id);
|
||||
setCalculatedPointInfo(calculatedPointInfo.filter((_pi, i) => i !== index));
|
||||
setPoints(points.filter(point => point.id !== id));
|
||||
};
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result.destination) return;
|
||||
const items = Array.from(points);
|
||||
const [reorderedItem] = items.splice(result.source.index, 1);
|
||||
items.splice(result.destination.index, 0, reorderedItem);
|
||||
setPoints(items);
|
||||
};
|
||||
|
||||
const handleChangeActionMode = (mode: string) => {
|
||||
setActionMode(mode);
|
||||
mode === actionModes.workArea ? setRectangleColor('green')
|
||||
: setRectangleColor('red');
|
||||
};
|
||||
|
||||
const handleLocationSearch = async (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
const coords = parseCoordinates(locationInput);
|
||||
if (coords) {
|
||||
updatePosition({ lat: coords.lat, lng: coords.lng });
|
||||
} else {
|
||||
const geocodedCoords = await geocodeAddress(locationInput);
|
||||
if (geocodedCoords) {
|
||||
updatePosition({ lat: geocodedCoords.lat, lng: geocodedCoords.lng });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const parseCoordinates = (input: string): LatLngPosition | null => {
|
||||
const cleaned = input.trim().replace(/[°NSEW]/gi, '');
|
||||
const parts = cleaned.split(/[,\s]+/).filter(p => p);
|
||||
|
||||
if (parts.length >= 2) {
|
||||
const lat = parseFloat(parts[0]);
|
||||
const lng = parseFloat(parts[1]);
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
|
||||
return { lat, lng };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const geocodeAddress = async (address: string): Promise<LatLngPosition | null> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${GOOGLE_GEOCODE_KEY}`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'OK' && data.results.length > 0) {
|
||||
const location = data.results[0].geometry.location;
|
||||
return { lat: location.lat, lng: location.lng };
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isShowed ?
|
||||
<div className={'left-board'}>
|
||||
<div className={'top-menu'}>
|
||||
<div className='selectors'>
|
||||
<LanguageSwitcher />
|
||||
|
||||
<div className='editor-container'>
|
||||
<InputLabel id='altitude-label'>{t.initialAltitude}</InputLabel>
|
||||
<FormControl fullWidth margin="none">
|
||||
<TextField
|
||||
type="number"
|
||||
value={initialAltitude}
|
||||
onChange={(e) => setInitialAltitude(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button className='arrow-btn' onClick={changeShowing}>
|
||||
<HideSidebarIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='actions-block'>
|
||||
<div className='action-buttons-row'>
|
||||
<Button className={actionMode !== actionModes.points ? 'btn action-btn' : 'btn active-mode-btn'}
|
||||
variant="contained" onClick={() => handleChangeActionMode(actionModes.points)}>
|
||||
<FaLocationDot className='action-btn-icon' />
|
||||
{t.addPoints}
|
||||
</Button>
|
||||
<Button className={actionMode !== actionModes.workArea ? 'btn action-btn' : 'btn active-mode-btn'}
|
||||
variant="contained" onClick={() => handleChangeActionMode(actionModes.workArea)}>
|
||||
<DashedAreaIcon className='action-btn-icon' color='green' />
|
||||
{t.workArea}
|
||||
</Button>
|
||||
<Button className={actionMode !== actionModes.prohibitedArea ? 'btn action-btn' : 'btn active-mode-btn'}
|
||||
variant="contained" onClick={() => handleChangeActionMode(actionModes.prohibitedArea)}>
|
||||
<DashedAreaIcon className='action-btn-icon' color='red' />
|
||||
{t.prohibitedArea}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='location-search'>
|
||||
<InputLabel id='location-label'>{t.location}:</InputLabel>
|
||||
<FormControl fullWidth margin="none">
|
||||
<TextField
|
||||
placeholder="47.242, 35.024"
|
||||
value={locationInput}
|
||||
onChange={(e) => setLocationInput(e.target.value)}
|
||||
onKeyDown={handleLocationSearch}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className='current-location-label'>
|
||||
{t.currentLocation}: {currentPosition?.lat.toFixed(6)}, {currentPosition?.lng.toFixed(6)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<PointsList
|
||||
points={points}
|
||||
setPoints={setPoints}
|
||||
calculatedPointInfo={calculatedPointInfo}
|
||||
setCalculatedPointInfo={setCalculatedPointInfo}
|
||||
removePoint={removePoint}
|
||||
aircraft={aircraft}
|
||||
initialAltitude={currentAltitude}
|
||||
/>
|
||||
</DragDropContext>
|
||||
|
||||
<TotalDistance points={points} calculatedPointInfo={calculatedPointInfo} aircraft={aircraft} initialAltitude={currentAltitude} />
|
||||
|
||||
<AltitudeChart points={points} />
|
||||
|
||||
<div className='operations-section'>
|
||||
<InputLabel id='operations-label'>{t.operations}:</InputLabel>
|
||||
</div>
|
||||
|
||||
<div className='section-buttons-row'>
|
||||
<Button className='btn section-btn' size="small"
|
||||
variant="contained" onClick={editAsJson}>
|
||||
{t.editAsJson}
|
||||
</Button>
|
||||
<Button className='btn section-btn' size="small"
|
||||
variant="contained" onClick={exportMap}>
|
||||
{t.exportMapData}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className='toolbar'>
|
||||
<Button className='toolbar-btn' onClick={changeShowing}>
|
||||
<ShowSidebarIcon />
|
||||
</Button>
|
||||
<Button className={actionMode !== actionModes.points ? 'toolbar-btn' : 'active-toolbar-btn'} id='point-btn'
|
||||
onClick={() => handleChangeActionMode(actionModes.points)}>
|
||||
<FaLocationDot size={'70%'} color='red' />
|
||||
</Button>
|
||||
<Button className={actionMode !== actionModes.workArea ? 'toolbar-btn' : 'active-toolbar-btn'}
|
||||
onClick={() => handleChangeActionMode(actionModes.workArea)}>
|
||||
<DashedAreaIcon color='green' />
|
||||
</Button>
|
||||
<Button className={actionMode !== actionModes.prohibitedArea ? 'toolbar-btn' : 'active-toolbar-btn'}
|
||||
onClick={() => handleChangeActionMode(actionModes.prohibitedArea)}>
|
||||
<DashedAreaIcon color='red' />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useRef } from 'react';
|
||||
import { useLanguage } from './LanguageContext';
|
||||
import { CircleMarker, Marker, Popup } from 'react-leaflet';
|
||||
import { Button, Checkbox, FormControl, InputLabel, ListItemText, MenuItem, Select, Slider, Typography, type SelectChangeEvent } from '@mui/material';
|
||||
import { purposes } from '../constants/purposes';
|
||||
import { translations } from '../constants/translations';
|
||||
import { pointIconBlue, pointIconGreen, pointIconRed } from '../icons/PointIcons';
|
||||
import { TiDelete } from 'react-icons/ti';
|
||||
import type { FlightPoint, MovingPointInfo } from '../types';
|
||||
import type L from 'leaflet';
|
||||
|
||||
interface MapPointProps {
|
||||
map: HTMLElement | null;
|
||||
point: FlightPoint;
|
||||
points: FlightPoint[];
|
||||
index: number;
|
||||
setDraggablePoints: React.Dispatch<React.SetStateAction<FlightPoint[]>>;
|
||||
setPoints: React.Dispatch<React.SetStateAction<FlightPoint[]>>;
|
||||
setMovingPoint: React.Dispatch<React.SetStateAction<MovingPointInfo | null>>;
|
||||
removePoint: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function MapPoint({
|
||||
map,
|
||||
point,
|
||||
points,
|
||||
index,
|
||||
setDraggablePoints,
|
||||
setPoints,
|
||||
setMovingPoint,
|
||||
removePoint,
|
||||
}: MapPointProps) {
|
||||
const { targetLanguage } = useLanguage();
|
||||
const t = translations[targetLanguage];
|
||||
const markerRef = useRef<L.Marker>(null);
|
||||
|
||||
const handleDrag = (e: L.LeafletEvent) => {
|
||||
const updatedPoints = [...points];
|
||||
updatedPoints[index] = {
|
||||
...updatedPoints[index],
|
||||
position: (e.target as L.Marker).getLatLng(),
|
||||
};
|
||||
setDraggablePoints(updatedPoints);
|
||||
};
|
||||
|
||||
const handleDragEnd = (e: L.LeafletEvent) => {
|
||||
const updatedPoints = [...points];
|
||||
updatedPoints[index] = {
|
||||
...updatedPoints[index],
|
||||
position: (e.target as L.Marker).getLatLng(),
|
||||
};
|
||||
setPoints(updatedPoints);
|
||||
};
|
||||
|
||||
const setPointPosition = (e: L.LeafletEvent) => {
|
||||
const marker = markerRef.current;
|
||||
if (!marker || !map) return;
|
||||
|
||||
const markerElement = (marker as unknown as { _icon: HTMLElement })._icon;
|
||||
if (!markerElement) return;
|
||||
|
||||
const mapRect = map.getBoundingClientRect();
|
||||
const markerRect = markerElement.getBoundingClientRect();
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
let displacementX = 150;
|
||||
let displacementY = 150;
|
||||
if (screenWidth < 1024) {
|
||||
displacementX = 70;
|
||||
displacementY = 70;
|
||||
}
|
||||
|
||||
const offsetX = markerRect.left - mapRect.left + markerRect.width > mapRect.width / 2
|
||||
? -displacementX : displacementX + 50;
|
||||
const offsetY = markerRect.top + markerRect.height > mapRect.height / 2
|
||||
? -displacementY : displacementY;
|
||||
|
||||
const x = markerRect.left - mapRect.left + offsetX;
|
||||
const y = markerRect.top - mapRect.top + offsetY;
|
||||
|
||||
setMovingPoint({
|
||||
x,
|
||||
y,
|
||||
latlng: (e.target as L.Marker).getLatLng(),
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeAltitude = (newAltitude: number) => {
|
||||
const updatedPoints = [...points];
|
||||
updatedPoints[index] = { ...updatedPoints[index], altitude: newAltitude };
|
||||
setPoints(updatedPoints);
|
||||
};
|
||||
|
||||
const handlePurposeChange = (e: SelectChangeEvent<string[]>) => {
|
||||
const value = e.target.value;
|
||||
const updatedPoints = [...points];
|
||||
updatedPoints[index] = {
|
||||
...updatedPoints[index],
|
||||
meta: typeof value === 'string' ? value.split(',') : value,
|
||||
};
|
||||
setPoints(updatedPoints);
|
||||
};
|
||||
|
||||
return (
|
||||
<Marker
|
||||
key={point.id}
|
||||
position={point.position}
|
||||
icon={index === 0 ? pointIconGreen : index === points.length - 1 ? pointIconRed : pointIconBlue}
|
||||
draggable={true}
|
||||
eventHandlers={{
|
||||
drag: (event) => handleDrag(event),
|
||||
dragend: (event) => handleDragEnd(event),
|
||||
move: (e) => setPointPosition(e),
|
||||
moveend: () => { setMovingPoint(null); },
|
||||
}}
|
||||
ref={markerRef}
|
||||
>
|
||||
<CircleMarker center={point.position} radius={35} stroke={false} />
|
||||
<Popup className='popup'>
|
||||
{t.point} {index + 1}
|
||||
<br />
|
||||
<Typography variant='subtitle2'>{t.height} </Typography>
|
||||
<Slider
|
||||
value={point.altitude}
|
||||
onChange={(_e, value) => handleChangeAltitude(value as number)}
|
||||
min={0}
|
||||
max={3000}
|
||||
step={1}
|
||||
valueLabelDisplay="auto"
|
||||
className='popup-slider'
|
||||
/>
|
||||
<br />
|
||||
<FormControl fullWidth margin="none">
|
||||
<InputLabel id="popup-purpose-label" className="form-label">{t.purpose}</InputLabel>
|
||||
<Select
|
||||
labelId="popup-purpose-label"
|
||||
label={t.purpose}
|
||||
multiple
|
||||
value={point.meta}
|
||||
onChange={(e) => handlePurposeChange(e as SelectChangeEvent<string[]>)}
|
||||
renderValue={(selected) => (selected as string[]).map(val => t.options[val]).join(', ')}
|
||||
style={{ height: '40px' }}
|
||||
>
|
||||
{purposes.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
<Checkbox checked={point.meta.indexOf(option.value) > -1} />
|
||||
<ListItemText primary={t.options[option.value]} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button onClick={() => { removePoint(point.id); }}>
|
||||
{t.removePoint}
|
||||
<TiDelete size={24} />
|
||||
</Button>
|
||||
</Popup>
|
||||
</Marker>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
.map {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.map-ctn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.popup-slider {
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.satellite-btn {
|
||||
min-width: unset !important;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: absolute !important;
|
||||
z-index: 1000;
|
||||
padding: 4px !important;
|
||||
right: 6px;
|
||||
bottom: 40px;
|
||||
background-color: white !important;
|
||||
border: 1px solid grey !important;
|
||||
}
|
||||
|
||||
.active-satellite-btn {
|
||||
min-width: unset !important;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: absolute !important;
|
||||
z-index: 1000;
|
||||
right: 6px;
|
||||
bottom: 40px;
|
||||
border: 1px solid grey !important;
|
||||
padding: 6px !important;
|
||||
background-color: blue !important;
|
||||
}
|
||||
|
||||
.custom-icon {
|
||||
margin-left: -30px !important;
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
}
|
||||
|
||||
.form-label{
|
||||
font-size: 1em;
|
||||
top: -0.3em;
|
||||
}
|
||||
|
||||
@media (min-width: 680px) and (max-width: 1024px) and (orientation: landscape) {
|
||||
.popup {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 6px 12px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.leaflet-popup-content .MuiButton-text {
|
||||
padding: 3px 4px;
|
||||
font-size: 0.9em;
|
||||
|
||||
}
|
||||
|
||||
.leaflet-popup-content .MuiInputBase-input {
|
||||
padding: 6px 4px;
|
||||
line-height: 0.9em;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import { MapContainer, TileLayer, Marker, Popup, Polyline, Rectangle, useMap, useMapEvents } from 'react-leaflet';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import 'leaflet-draw';
|
||||
import 'leaflet-draw/dist/leaflet.draw.css';
|
||||
import 'leaflet-polylinedecorator';
|
||||
import DrawControl from './DrawControl';
|
||||
import { newGuid } from '../utils';
|
||||
import AltitudeDialog from './AltitudeDialog';
|
||||
import { useLanguage } from './LanguageContext';
|
||||
import { defaultIcon } from '../icons/PointIcons';
|
||||
import { translations } from '../constants/translations';
|
||||
import { actionModes } from '../constants/actionModes';
|
||||
import './MapView.css';
|
||||
import MiniMap from './MiniMap';
|
||||
import MapPoint from './MapPoint';
|
||||
import { SatelliteMapIcon } from '../icons/MapIcons';
|
||||
import { mapTypes } from '../constants/maptypes';
|
||||
import { Button } from '@mui/material';
|
||||
import { purposes } from '../constants/purposes';
|
||||
import { tileUrls } from '../constants/tileUrls';
|
||||
import type { FlightPoint, CalculatedPointInfo, MapRectangle, MovingPointInfo, LatLngPosition } from '../types';
|
||||
|
||||
interface MapEventsProps {
|
||||
points: FlightPoint[];
|
||||
rectangles: MapRectangle[];
|
||||
rectangleColor: string;
|
||||
handlePolylineClick: (e: L.LeafletMouseEvent) => void;
|
||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||
updateMapCenter: (center: L.LatLng) => void;
|
||||
}
|
||||
|
||||
function MapEvents({ points, rectangles, rectangleColor, handlePolylineClick, containerRef, updateMapCenter }: MapEventsProps) {
|
||||
const map = useMap();
|
||||
const polylineRef = useRef<L.Polyline | null>(null);
|
||||
const arrowLayerRef = useRef<L.FeatureGroup | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMapMove = () => {
|
||||
const center = map.getCenter();
|
||||
updateMapCenter(center);
|
||||
};
|
||||
|
||||
map.on('moveend', handleMapMove);
|
||||
|
||||
return () => {
|
||||
map.off('moveend', handleMapMove);
|
||||
};
|
||||
}, [map, updateMapCenter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (map && points.length > 1) {
|
||||
if (polylineRef.current) {
|
||||
map.removeLayer(polylineRef.current);
|
||||
}
|
||||
if (arrowLayerRef.current) {
|
||||
map.removeLayer(arrowLayerRef.current);
|
||||
}
|
||||
|
||||
const positions: L.LatLngTuple[] = points.map(point => [point.position.lat, point.position.lng]);
|
||||
|
||||
polylineRef.current = L.polyline(positions, {
|
||||
color: 'blue',
|
||||
weight: 8,
|
||||
opacity: 0.7,
|
||||
lineJoin: 'round',
|
||||
}).addTo(map);
|
||||
|
||||
arrowLayerRef.current = L.polylineDecorator(polylineRef.current, {
|
||||
patterns: [
|
||||
{
|
||||
offset: '10%',
|
||||
repeat: '40%',
|
||||
symbol: L.Symbol.arrowHead({
|
||||
pixelSize: 15,
|
||||
pathOptions: {
|
||||
fillOpacity: 1,
|
||||
weight: 0,
|
||||
color: 'blue',
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
}).addTo(map);
|
||||
|
||||
polylineRef.current.on('click', handlePolylineClick);
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (map) {
|
||||
map.invalidateSize();
|
||||
}
|
||||
});
|
||||
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (polylineRef.current) {
|
||||
map.removeLayer(polylineRef.current);
|
||||
polylineRef.current = null;
|
||||
}
|
||||
if (arrowLayerRef.current) {
|
||||
map.removeLayer(arrowLayerRef.current);
|
||||
arrowLayerRef.current = null;
|
||||
}
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [map, points, rectangles, rectangleColor, handlePolylineClick, containerRef]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
interface UpdateMapCenterProps {
|
||||
currentPosition: L.LatLngExpression;
|
||||
}
|
||||
|
||||
export const UpdateMapCenter = ({ currentPosition }: UpdateMapCenterProps) => {
|
||||
const map = useMap();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPosition) {
|
||||
map.setView(currentPosition);
|
||||
}
|
||||
}, [currentPosition, map]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
interface MapViewProps {
|
||||
points: FlightPoint[];
|
||||
setPoints: React.Dispatch<React.SetStateAction<FlightPoint[]>>;
|
||||
calculatedPointInfo: CalculatedPointInfo[];
|
||||
setCalculatedPointInfo: React.Dispatch<React.SetStateAction<CalculatedPointInfo[]>>;
|
||||
initialAltitude: number;
|
||||
currentPosition: LatLngPosition;
|
||||
updatePosition: (pos: LatLngPosition) => void;
|
||||
updateMapCenter: (center: L.LatLng) => void;
|
||||
handlePolylineClick: (e: L.LeafletMouseEvent) => void;
|
||||
rectangles: MapRectangle[];
|
||||
setRectangles: React.Dispatch<React.SetStateAction<MapRectangle[]>>;
|
||||
rectangleColor: string;
|
||||
actionMode: string;
|
||||
}
|
||||
|
||||
export default function MapView({
|
||||
points,
|
||||
setPoints,
|
||||
calculatedPointInfo,
|
||||
setCalculatedPointInfo,
|
||||
initialAltitude,
|
||||
currentPosition,
|
||||
updatePosition,
|
||||
updateMapCenter,
|
||||
handlePolylineClick,
|
||||
rectangles,
|
||||
setRectangles,
|
||||
rectangleColor,
|
||||
actionMode,
|
||||
}: MapViewProps) {
|
||||
const { targetLanguage } = useLanguage();
|
||||
const t = translations[targetLanguage];
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [editingPoint, setEditingPoint] = useState<FlightPoint | null>(null);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [mapType, setMapType] = useState<string>(mapTypes.satellite);
|
||||
const [draggablePoints, setDraggablePoints] = useState<FlightPoint[]>(points);
|
||||
const [altitude, setAltitude] = useState(300);
|
||||
const [latitude, setLatitude] = useState(0);
|
||||
const [longitude, setLongitude] = useState(0);
|
||||
const [meta, setMeta] = useState<string[]>([purposes[0].value, purposes[1].value]);
|
||||
const [drawControl, setDrawControl] = useState<L.Control.Draw | null>(null);
|
||||
const [movingPoint, setMovingPoint] = useState<MovingPointInfo | null>(null);
|
||||
const polylineClickRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDraggablePoints(points);
|
||||
}, [points]);
|
||||
|
||||
const handleControl = (control: L.Control.Draw) => {
|
||||
setDrawControl(control);
|
||||
};
|
||||
|
||||
const handleMarkerDoubleClick = () => {
|
||||
setLatitude(currentPosition.lat);
|
||||
setLongitude(currentPosition.lng);
|
||||
setAltitude(300);
|
||||
setMeta([purposes[0].value, purposes[1].value]);
|
||||
|
||||
setEditingPoint({
|
||||
id: newGuid(),
|
||||
position: currentPosition,
|
||||
altitude: 0,
|
||||
meta: [purposes[0].value, purposes[1].value],
|
||||
});
|
||||
|
||||
setIsEditMode(false);
|
||||
setOpenDialog(true);
|
||||
};
|
||||
|
||||
const handleMapTypeChange = () => {
|
||||
const mt = mapType === mapTypes.classic
|
||||
? mapTypes.satellite
|
||||
: mapTypes.classic;
|
||||
setMapType(mt);
|
||||
};
|
||||
|
||||
const addPoint = (lat: number, lng: number) => {
|
||||
if (!polylineClickRef.current) {
|
||||
const newPoint: FlightPoint = {
|
||||
id: newGuid(),
|
||||
position: { lat: parseFloat(String(lat)), lng: parseFloat(String(lng)) },
|
||||
altitude: parseFloat(String(initialAltitude)),
|
||||
meta,
|
||||
};
|
||||
|
||||
const pointInfo = {
|
||||
bat: 100,
|
||||
time: 0,
|
||||
};
|
||||
|
||||
const updatedPoints = [...points];
|
||||
updatedPoints.push(newPoint);
|
||||
const updatedInfo = [...calculatedPointInfo];
|
||||
updatedInfo.push(pointInfo);
|
||||
|
||||
setCalculatedPointInfo(updatedInfo);
|
||||
setDraggablePoints(updatedPoints);
|
||||
setPoints(updatedPoints);
|
||||
}
|
||||
polylineClickRef.current = false;
|
||||
};
|
||||
|
||||
const removePoint = (id: string) => {
|
||||
const index = points.findIndex((point) => point.id === id);
|
||||
setCalculatedPointInfo(calculatedPointInfo.filter((_pi, i) => i !== index));
|
||||
setPoints(points.filter(point => point.id !== id));
|
||||
};
|
||||
|
||||
const validBounds = (bounds: unknown): boolean => {
|
||||
if (!bounds) return false;
|
||||
if (bounds instanceof L.LatLngBounds) return bounds.isValid();
|
||||
if (Array.isArray(bounds) && bounds.length === 2) {
|
||||
const [sw, ne] = bounds;
|
||||
return sw != null && ne != null &&
|
||||
typeof sw.lat === 'number' && typeof sw.lng === 'number' &&
|
||||
typeof ne.lat === 'number' && typeof ne.lng === 'number';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleAltitudeSubmit = () => {
|
||||
if (!editingPoint) return;
|
||||
|
||||
if (!isEditMode) {
|
||||
const newPoint: FlightPoint = {
|
||||
id: editingPoint.id,
|
||||
position: { lat: latitude, lng: longitude },
|
||||
altitude,
|
||||
meta,
|
||||
};
|
||||
setPoints(prevPoints => [...prevPoints, newPoint]);
|
||||
} else {
|
||||
const updatedPoints = points.map(p =>
|
||||
p.id === editingPoint.id
|
||||
? { ...p, position: { lat: latitude, lng: longitude }, altitude, meta }
|
||||
: p
|
||||
);
|
||||
setPoints(updatedPoints);
|
||||
}
|
||||
|
||||
setOpenDialog(false);
|
||||
};
|
||||
|
||||
const startDrawingRectangle = () => {
|
||||
if (drawControl) {
|
||||
// @ts-expect-error: Accessing internal leaflet-draw toolbar API
|
||||
drawControl._toolbars.draw._modes.rectangle.handler.enable();
|
||||
}
|
||||
};
|
||||
|
||||
const polylineClick = (e: L.LeafletMouseEvent) => {
|
||||
if (actionMode === actionModes.points) {
|
||||
polylineClickRef.current = true;
|
||||
handlePolylineClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
const LocationFinder = () => {
|
||||
useMapEvents({
|
||||
click(e) {
|
||||
actionMode === actionModes.points
|
||||
? addPoint(e.latlng.lat, e.latlng.lng)
|
||||
: startDrawingRectangle();
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='map' ref={containerRef}>
|
||||
<MapContainer className='map-ctn' center={currentPosition} zoom={15}>
|
||||
<LocationFinder />
|
||||
|
||||
{mapType === mapTypes.classic
|
||||
?
|
||||
<TileLayer
|
||||
url={tileUrls.classic}
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
/>
|
||||
:
|
||||
<TileLayer
|
||||
url={tileUrls.satellite}
|
||||
attribution="Satellite imagery"
|
||||
/>
|
||||
}
|
||||
|
||||
<MapEvents
|
||||
points={draggablePoints}
|
||||
rectangles={rectangles}
|
||||
rectangleColor={rectangleColor}
|
||||
handlePolylineClick={polylineClick}
|
||||
containerRef={containerRef}
|
||||
updateMapCenter={updateMapCenter}
|
||||
/>
|
||||
|
||||
{movingPoint &&
|
||||
<MiniMap
|
||||
pointPosition={movingPoint}
|
||||
mapType={mapType}
|
||||
/>
|
||||
}
|
||||
|
||||
{draggablePoints.map((point, index) => (
|
||||
<MapPoint
|
||||
key={point.id}
|
||||
map={containerRef.current}
|
||||
point={point}
|
||||
points={draggablePoints}
|
||||
index={index}
|
||||
setDraggablePoints={setDraggablePoints}
|
||||
setPoints={setPoints}
|
||||
setMovingPoint={setMovingPoint}
|
||||
removePoint={removePoint}
|
||||
/>
|
||||
))}
|
||||
|
||||
{draggablePoints.length > 0 && (
|
||||
<Polyline
|
||||
positions={[
|
||||
[draggablePoints[draggablePoints.length - 1].position.lat, draggablePoints[draggablePoints.length - 1].position.lng],
|
||||
[draggablePoints[0].position.lat, draggablePoints[0].position.lng],
|
||||
]}
|
||||
color="blue"
|
||||
dashArray="5, 10"
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentPosition && (
|
||||
<Marker
|
||||
position={currentPosition}
|
||||
icon={defaultIcon}
|
||||
draggable={true}
|
||||
eventHandlers={{
|
||||
dragend: (event) => {
|
||||
updatePosition((event.target as L.Marker).getLatLng());
|
||||
},
|
||||
dblclick: handleMarkerDoubleClick,
|
||||
}}
|
||||
>
|
||||
<Popup>{t.currentPos}</Popup>
|
||||
</Marker>
|
||||
)}
|
||||
{rectangles.map((rect, index) =>
|
||||
validBounds(rect.bounds) ? (
|
||||
<Rectangle
|
||||
key={index}
|
||||
bounds={rect.bounds}
|
||||
pathOptions={{ color: rect.color }}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
|
||||
<DrawControl color={rectangleColor} rectangles={rectangles} setRectangles={setRectangles} controlChange={handleControl} />
|
||||
<UpdateMapCenter currentPosition={currentPosition} />
|
||||
</MapContainer>
|
||||
|
||||
<Button className={mapType === mapTypes.classic ? 'satellite-btn' : ' active-satellite-btn'}
|
||||
onClick={() => handleMapTypeChange()}>
|
||||
<SatelliteMapIcon />
|
||||
</Button>
|
||||
|
||||
<AltitudeDialog
|
||||
openDialog={openDialog}
|
||||
handleDialogClose={() => setOpenDialog(false)}
|
||||
handleAltitudeSubmit={handleAltitudeSubmit}
|
||||
altitude={altitude}
|
||||
setAltitude={setAltitude}
|
||||
latitude={latitude}
|
||||
setLatitude={setLatitude}
|
||||
longitude={longitude}
|
||||
setLongitude={setLongitude}
|
||||
meta={meta}
|
||||
setMeta={setMeta}
|
||||
isEditMode={isEditMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { CircleMarker, MapContainer, TileLayer } from 'react-leaflet';
|
||||
import { UpdateMapCenter } from './MapView';
|
||||
import { mapTypes } from '../constants/maptypes';
|
||||
import { tileUrls } from '../constants/tileUrls';
|
||||
import './Minimap.css';
|
||||
import type { MovingPointInfo } from '../types';
|
||||
|
||||
interface MiniMapProps {
|
||||
pointPosition: MovingPointInfo;
|
||||
mapType: string;
|
||||
}
|
||||
|
||||
export default function MiniMap({ pointPosition, mapType }: MiniMapProps) {
|
||||
return (
|
||||
<div className="mini-map"
|
||||
style={{
|
||||
top: pointPosition.y,
|
||||
left: pointPosition.x,
|
||||
}}>
|
||||
<MapContainer center={pointPosition.latlng} zoom={25} zoomControl={false}>
|
||||
{mapType === mapTypes.classic
|
||||
? <TileLayer url={tileUrls.classic} />
|
||||
: <TileLayer url={tileUrls.satellite} />
|
||||
}
|
||||
<CircleMarker center={pointPosition.latlng} radius={2} color="red" />
|
||||
<UpdateMapCenter currentPosition={pointPosition.latlng} />
|
||||
</MapContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
.mini-map{
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 50%;
|
||||
border: 2px solid black;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@media (min-width: 680px) and (max-width: 1024px) and (orientation: landscape){
|
||||
.mini-map{
|
||||
width: 140px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
.flight-plan-point {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.point-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.uncalculated {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.point-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flight-plan-points-container {
|
||||
min-height: 50px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.flight-plan-point {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
@media (min-width: 680px) and (max-width: 1024px) and (orientation: landscape) {
|
||||
.flight-plan-point {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.point-details {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.point-actions {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.point-actions .MuiButtonBase-root {
|
||||
padding: 4px;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.small-icon {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Droppable, Draggable } from '@hello-pangea/dnd';
|
||||
import Button from '@mui/material/Button';
|
||||
import { FaTrash, FaEdit } from 'react-icons/fa';
|
||||
import AltitudeDialog from './AltitudeDialog';
|
||||
import './PointsList.css';
|
||||
import { newGuid } from '../utils';
|
||||
import { useLanguage } from './LanguageContext';
|
||||
import { translations } from '../constants/translations';
|
||||
import { calculateBatteryPercentUsed } from '../services/calculateBatteryUsage';
|
||||
import { calculateDistance } from '../services/calculateDistance';
|
||||
import type { FlightPoint, CalculatedPointInfo, AircraftParams } from '../types';
|
||||
|
||||
interface PointsListProps {
|
||||
points: FlightPoint[];
|
||||
calculatedPointInfo: CalculatedPointInfo[];
|
||||
setCalculatedPointInfo: React.Dispatch<React.SetStateAction<CalculatedPointInfo[]>>;
|
||||
setPoints: React.Dispatch<React.SetStateAction<FlightPoint[]>>;
|
||||
removePoint: (id: string) => void;
|
||||
aircraft: AircraftParams | null;
|
||||
initialAltitude: number;
|
||||
}
|
||||
|
||||
export default function PointsList({
|
||||
points,
|
||||
calculatedPointInfo,
|
||||
setCalculatedPointInfo,
|
||||
setPoints,
|
||||
removePoint,
|
||||
aircraft,
|
||||
initialAltitude,
|
||||
}: PointsListProps) {
|
||||
const { targetLanguage } = useLanguage();
|
||||
const t = translations[targetLanguage];
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [currentPoint, setCurrentPoint] = useState<FlightPoint | null>(null);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [isCalculated, setIsCalculated] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setIsCalculated(false);
|
||||
|
||||
const calculatePoints = async () => {
|
||||
const calculatedData = await calculateBatteryUsageForPoints(points);
|
||||
setCalculatedPointInfo([...calculatedData]);
|
||||
setIsCalculated(true);
|
||||
};
|
||||
|
||||
calculatePoints();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [points]);
|
||||
|
||||
const calculateBatteryUsageForPoints = async (pts: FlightPoint[]): Promise<CalculatedPointInfo[]> => {
|
||||
const pointsInfo: CalculatedPointInfo[] = [{
|
||||
bat: 100,
|
||||
time: 0,
|
||||
}];
|
||||
|
||||
if (!aircraft) return pointsInfo;
|
||||
|
||||
for (let index = 1; index < pts.length; index++) {
|
||||
const point1 = pts[index - 1];
|
||||
const point2 = pts[index];
|
||||
|
||||
const midPoint = {
|
||||
lat: (point1.position.lat + point2.position.lat) / 2,
|
||||
lon: (point1.position.lng + point2.position.lng) / 2,
|
||||
};
|
||||
|
||||
const distance = calculateDistance(point1, point2, aircraft.type, initialAltitude, aircraft.downang, aircraft.upang);
|
||||
const time = distance / aircraft.speed;
|
||||
|
||||
const percent = await calculateBatteryPercentUsed(aircraft.speed, time, midPoint);
|
||||
pointsInfo.push({
|
||||
bat: pointsInfo[index - 1].bat - percent,
|
||||
time: pointsInfo[index - 1].time + time,
|
||||
});
|
||||
}
|
||||
return pointsInfo;
|
||||
};
|
||||
|
||||
const handleDialogOpen = (point: FlightPoint | null = null) => {
|
||||
setIsEditMode(!!point);
|
||||
setCurrentPoint(point || { id: '', position: { lat: 0, lng: 0 }, altitude: 0, meta: [] });
|
||||
setOpenDialog(true);
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
setOpenDialog(false);
|
||||
setCurrentPoint(null);
|
||||
};
|
||||
|
||||
const handleAltitudeSubmit = () => {
|
||||
if (!currentPoint) return;
|
||||
|
||||
if (isEditMode) {
|
||||
setPoints((prevPoints) =>
|
||||
prevPoints.map((point) =>
|
||||
point.id === currentPoint.id ? currentPoint : point
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setPoints((prevPoints) => [
|
||||
...prevPoints,
|
||||
{ ...currentPoint, id: newGuid() },
|
||||
]);
|
||||
}
|
||||
handleDialogClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Droppable droppableId="points">
|
||||
{(provided) => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
className="flight-plan-points-container"
|
||||
>
|
||||
{points.map((point, index) => (
|
||||
<Draggable key={point.id} draggableId={point.id} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className={'flight-plan-point'}
|
||||
>
|
||||
<div className={isCalculated ? 'point-details' : 'point-details uncalculated'}>
|
||||
<strong>{t.point} {index < 9 ? `0${index + 1}` : index + 1}: </strong>
|
||||
{point.altitude}{t.metres} {Math.floor(calculatedPointInfo[index]?.bat)}%{t.battery} {Math.floor(calculatedPointInfo[index]?.time)}{t.hour}
|
||||
{Math.floor((calculatedPointInfo[index]?.time - Math.floor(calculatedPointInfo[index]?.time)) * 60)}{t.minutes}
|
||||
</div>
|
||||
<div className="point-actions">
|
||||
<Button
|
||||
className="icon"
|
||||
onClick={() => handleDialogOpen(point)}
|
||||
size="small"
|
||||
color="primary"
|
||||
startIcon={<FaEdit className='small-icon' />}
|
||||
/>
|
||||
<Button
|
||||
className="icon"
|
||||
onClick={() => removePoint(point.id)}
|
||||
size="small"
|
||||
color="error"
|
||||
startIcon={<FaTrash className='small-icon' />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
{openDialog && currentPoint && (
|
||||
<AltitudeDialog
|
||||
openDialog={openDialog}
|
||||
handleDialogClose={handleDialogClose}
|
||||
handleAltitudeSubmit={handleAltitudeSubmit}
|
||||
altitude={currentPoint.altitude}
|
||||
setAltitude={(altitude) =>
|
||||
setCurrentPoint((prev) => prev ? { ...prev, altitude: Number(altitude) } : prev)
|
||||
}
|
||||
latitude={currentPoint.position.lat}
|
||||
setLatitude={(latitude) =>
|
||||
setCurrentPoint((prev) => prev ? {
|
||||
...prev,
|
||||
position: { ...prev.position, lat: Number(latitude) },
|
||||
} : prev)
|
||||
}
|
||||
longitude={currentPoint.position.lng}
|
||||
setLongitude={(longitude) =>
|
||||
setCurrentPoint((prev) => prev ? {
|
||||
...prev,
|
||||
position: { ...prev.position, lng: Number(longitude) },
|
||||
} : prev)
|
||||
}
|
||||
meta={currentPoint.meta || []}
|
||||
setMeta={(meta) => setCurrentPoint((prev) => prev ? { ...prev, meta } : prev)}
|
||||
isEditMode={isEditMode}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
.distance-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 680px) and (max-width: 1024px) and (orientation: landscape){
|
||||
.distance-container{
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-block{
|
||||
margin: 6px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useLanguage } from './LanguageContext';
|
||||
import { calculateDistance } from '../services/calculateDistance';
|
||||
import { translations } from '../constants/translations';
|
||||
import './TotalDistance.css';
|
||||
import type { FlightPoint, CalculatedPointInfo, AircraftParams, TranslationStrings } from '../types';
|
||||
|
||||
interface BatteryStatus {
|
||||
color: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const getBatteryStatus = (batteryPercent: number, t: TranslationStrings): BatteryStatus => {
|
||||
if (batteryPercent > 12) {
|
||||
return { color: 'green', message: t.flightStatus.good };
|
||||
} else if (batteryPercent > 5) {
|
||||
return { color: 'yellow', message: t.flightStatus.caution };
|
||||
} else {
|
||||
return { color: 'red', message: t.flightStatus.low };
|
||||
}
|
||||
};
|
||||
|
||||
interface TotalDistanceProps {
|
||||
points: FlightPoint[];
|
||||
calculatedPointInfo: CalculatedPointInfo[];
|
||||
aircraft: AircraftParams | null;
|
||||
initialAltitude: number;
|
||||
}
|
||||
|
||||
const TotalDistance = ({ points, calculatedPointInfo, aircraft, initialAltitude }: TotalDistanceProps) => {
|
||||
const { targetLanguage } = useLanguage();
|
||||
const t = translations[targetLanguage];
|
||||
|
||||
const returnPoint = points[points.length - 1];
|
||||
|
||||
if (!aircraft || !points || (returnPoint ? points.length < 1 : points.length < 2)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalDistance = points.reduce((acc, point, index) => {
|
||||
if (index === 0) return acc;
|
||||
|
||||
const prevPoint = points[index - 1];
|
||||
|
||||
return acc + calculateDistance(
|
||||
prevPoint,
|
||||
point,
|
||||
aircraft.type,
|
||||
initialAltitude,
|
||||
aircraft.downang,
|
||||
aircraft.upang,
|
||||
);
|
||||
}, 0);
|
||||
|
||||
const formattedReturnPoint = returnPoint?.position
|
||||
? { position: { lat: returnPoint.position.lat, lng: returnPoint.position.lng } } as FlightPoint
|
||||
: null;
|
||||
|
||||
const distanceToReturnPoint = formattedReturnPoint
|
||||
? calculateDistance(points[points.length - 1], formattedReturnPoint, aircraft.type, initialAltitude, aircraft.downang, aircraft.upang)
|
||||
: 0;
|
||||
|
||||
const totalDistanceWithReturn = totalDistance + distanceToReturnPoint;
|
||||
|
||||
if (isNaN(totalDistanceWithReturn) || totalDistanceWithReturn <= 0) {
|
||||
console.error('Invalid total distance:', totalDistanceWithReturn);
|
||||
return <div>{t.error}</div>;
|
||||
}
|
||||
|
||||
const lastPointInfo = calculatedPointInfo?.[calculatedPointInfo.length - 1];
|
||||
if (!lastPointInfo || lastPointInfo.bat === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const status = getBatteryStatus(lastPointInfo.bat, t);
|
||||
|
||||
const time = totalDistanceWithReturn / aircraft.speed;
|
||||
const hours = Math.floor(time);
|
||||
const minutes = Math.floor((time - hours) * 60);
|
||||
|
||||
return (
|
||||
<div className='distance-container' style={{ display: 'flex', flexDirection: 'row', gap: '10px' }}>
|
||||
<p className='info-block'>{totalDistanceWithReturn.toFixed(2)}{t.km} {t.calc}</p>
|
||||
{hours >= 1 &&
|
||||
<p className='info-block'>{hours}{t.hour} </p>
|
||||
}
|
||||
<p className='info-block'>{minutes}{t.minutes} </p>
|
||||
<p className='info-block' style={{ color: status.color }}>{status.message}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TotalDistance;
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useState } from 'react';
|
||||
import { useLanguage } from './LanguageContext';
|
||||
import Button from '@mui/material/Button';
|
||||
import { translations } from '../constants/translations';
|
||||
|
||||
interface WindEffectProps {
|
||||
onWindChange: (wind: { direction: number; speed: number }) => void;
|
||||
}
|
||||
|
||||
const WindEffect = ({ onWindChange }: WindEffectProps) => {
|
||||
const { targetLanguage } = useLanguage();
|
||||
const t = translations[targetLanguage];
|
||||
|
||||
const [windDirection, setWindDirection] = useState(0);
|
||||
const [windSpeed, setWindSpeed] = useState(0);
|
||||
|
||||
const handleWindDirectionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setWindDirection(Number(event.target.value));
|
||||
};
|
||||
|
||||
const handleWindSpeedChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setWindSpeed(Number(event.target.value));
|
||||
};
|
||||
|
||||
const applyWindParameters = () => {
|
||||
onWindChange({ direction: windDirection, speed: windSpeed });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>{t.setWind}</h3>
|
||||
<div>
|
||||
<label htmlFor="windDirection">Wind Direction (degrees):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="windDirection"
|
||||
value={windDirection}
|
||||
onChange={handleWindDirectionChange}
|
||||
min="0"
|
||||
max="360"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="windSpeed">Wind Speed (m/s or km/h):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="windSpeed"
|
||||
value={windSpeed}
|
||||
onChange={handleWindSpeedChange}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
<Button className='btn' variant="contained" onClick={applyWindParameters}>
|
||||
{t.setWind}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WindEffect;
|
||||
@@ -0,0 +1,63 @@
|
||||
.flight-plan-board {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.left-board,
|
||||
.right-board {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.right-board {
|
||||
flex: 1;
|
||||
border: 1px solid #333333;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.full-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.leaflet-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.custom-icon {
|
||||
z-index: 1 !important;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.mobile-message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.flight-plan-board {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
gap: 12px;
|
||||
background-color: #c8b6b6;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { LatLng } from 'leaflet';
|
||||
import { newGuid } from '../utils';
|
||||
import MapView from './MapView';
|
||||
import AltitudeDialog from './AltitudeDialog';
|
||||
import './flightPlan.css';
|
||||
import JsonEditorDialog from './JsonEditorDialog';
|
||||
import { COORDINATE_PRECISION } from '../config';
|
||||
import L from 'leaflet';
|
||||
import LeftBoard from './LeftBoard';
|
||||
import { actionModes } from '../constants/actionModes';
|
||||
import { mockGetAirplaneParams } from '../services/AircraftService';
|
||||
import { RotatePhoneIcon } from '../icons/PhoneIcon';
|
||||
import { purposes } from '../constants/purposes';
|
||||
import type { FlightPoint, CalculatedPointInfo, MapRectangle, AircraftParams, LatLngPosition } from '../types';
|
||||
|
||||
interface FlightPlanProps {
|
||||
initialRectangles?: MapRectangle[];
|
||||
}
|
||||
|
||||
export function FlightPlan({ initialRectangles = [] }: FlightPlanProps) {
|
||||
const [aircraft, setAircraft] = useState<AircraftParams | null>(null);
|
||||
const [points, setPoints] = useState<FlightPoint[]>([]);
|
||||
const [calculatedPointInfo, setCalculatedPointInfo] = useState<CalculatedPointInfo[]>([]);
|
||||
const [currentPosition, setCurrentPosition] = useState<LatLngPosition>(new LatLng(47.242, 35.024));
|
||||
const [mapCenter, setMapCenter] = useState<LatLngPosition>(new LatLng(47.242, 35.024));
|
||||
const [rectangles, setRectangles] = useState<MapRectangle[]>(initialRectangles);
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [altitude, setAltitude] = useState(300);
|
||||
const [latitude, setLatitude] = useState<number>(parseFloat(new LatLng(47.242, 35.024).lat.toFixed(COORDINATE_PRECISION)));
|
||||
const [longitude, setLongitude] = useState<number>(parseFloat(new LatLng(47.242, 35.024).lng.toFixed(COORDINATE_PRECISION)));
|
||||
const [meta, setMeta] = useState<string[]>([purposes[0].value, purposes[1].value]);
|
||||
const [rectangleColor, setRectangleColor] = useState('red');
|
||||
const [pendingPosition, setPendingPosition] = useState<LatLngPosition | null>(null);
|
||||
const [insertIndex, setInsertIndex] = useState<number | null>(null);
|
||||
const [jsonDialogOpen, setJsonDialogOpen] = useState(false);
|
||||
const [jsonText, setJsonText] = useState('');
|
||||
const [initialAltitude, setInitialAltitude] = useState(1000);
|
||||
const [currentAltitude, setCurrentAltitude] = useState(initialAltitude);
|
||||
const [actionMode, setActionMode] = useState<string>(actionModes.points);
|
||||
|
||||
useEffect(() => {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const pos = new LatLng(position.coords.latitude, position.coords.longitude);
|
||||
setCurrentPosition(pos);
|
||||
setMapCenter(pos);
|
||||
},
|
||||
() => {
|
||||
const pos = new LatLng(47.242, 35.024);
|
||||
setCurrentPosition(pos);
|
||||
setMapCenter(pos);
|
||||
}
|
||||
);
|
||||
|
||||
mockGetAirplaneParams().then((air) => {
|
||||
setAircraft(air);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDialogClose = () => {
|
||||
setOpenDialog(false);
|
||||
setPendingPosition(null);
|
||||
setInsertIndex(null);
|
||||
};
|
||||
|
||||
const handleAltitudeSubmit = () => {
|
||||
if (pendingPosition !== null) {
|
||||
const newPoint: FlightPoint = {
|
||||
id: newGuid(),
|
||||
position: { lat: parseFloat(String(latitude)), lng: parseFloat(String(longitude)) },
|
||||
altitude: parseFloat(String(altitude)),
|
||||
meta,
|
||||
};
|
||||
|
||||
if (insertIndex !== null) {
|
||||
const updatedPoints = [...points];
|
||||
updatedPoints.splice(insertIndex, 0, newPoint);
|
||||
setPoints(updatedPoints);
|
||||
} else {
|
||||
setPoints(prevPoints => [...prevPoints, newPoint]);
|
||||
}
|
||||
|
||||
setAltitude(300);
|
||||
setLatitude(parseFloat(currentPosition.lat.toFixed(COORDINATE_PRECISION)));
|
||||
setLongitude(parseFloat(currentPosition.lng.toFixed(COORDINATE_PRECISION)));
|
||||
setMeta([purposes[0].value, purposes[1].value]);
|
||||
setPendingPosition(null);
|
||||
setInsertIndex(null);
|
||||
}
|
||||
setOpenDialog(false);
|
||||
};
|
||||
|
||||
const updatePosition = (newPosition: LatLngPosition) => {
|
||||
setCurrentPosition(newPosition);
|
||||
setMapCenter(newPosition);
|
||||
setLatitude(parseFloat(newPosition.lat.toFixed(COORDINATE_PRECISION)));
|
||||
setLongitude(parseFloat(newPosition.lng.toFixed(COORDINATE_PRECISION)));
|
||||
};
|
||||
|
||||
const updateMapCenter = (newCenter: LatLngPosition) => {
|
||||
setMapCenter(newCenter);
|
||||
};
|
||||
|
||||
const handlePolylineClick = (e: L.LeafletMouseEvent) => {
|
||||
const clickLatLng = e.latlng;
|
||||
|
||||
if (!clickLatLng || typeof clickLatLng.lat !== 'number' || typeof clickLatLng.lng !== 'number') {
|
||||
console.error('Invalid clickLatLng:', clickLatLng);
|
||||
return;
|
||||
}
|
||||
|
||||
let closestIndex = -1;
|
||||
let minDistance = Infinity;
|
||||
|
||||
points.forEach((point, index) => {
|
||||
if (index < points.length - 1) {
|
||||
const segmentStart = points[index]?.position;
|
||||
const segmentEnd = points[index + 1]?.position;
|
||||
|
||||
if (segmentStart && segmentEnd &&
|
||||
typeof segmentStart.lat === 'number' && typeof segmentStart.lng === 'number' &&
|
||||
typeof segmentEnd.lat === 'number' && typeof segmentEnd.lng === 'number') {
|
||||
|
||||
const distance = L.LineUtil.pointToSegmentDistance(
|
||||
L.point(clickLatLng.lng, clickLatLng.lat),
|
||||
L.point(segmentStart.lng, segmentStart.lat),
|
||||
L.point(segmentEnd.lng, segmentEnd.lat)
|
||||
);
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (closestIndex !== -1) {
|
||||
const alt = (points[closestIndex].altitude + points[closestIndex + 1].altitude) / 2;
|
||||
|
||||
const newPoint: FlightPoint = {
|
||||
id: newGuid(),
|
||||
position: {
|
||||
lat: parseFloat(clickLatLng.lat.toFixed(COORDINATE_PRECISION)),
|
||||
lng: parseFloat(clickLatLng.lng.toFixed(COORDINATE_PRECISION)),
|
||||
},
|
||||
altitude: parseFloat(String(alt)),
|
||||
meta: [purposes[0].value, purposes[1].value],
|
||||
};
|
||||
const pointInfo: CalculatedPointInfo = {
|
||||
bat: 100,
|
||||
time: 0,
|
||||
};
|
||||
|
||||
const updatedPoints = [...points];
|
||||
updatedPoints.splice(closestIndex + 1, 0, newPoint);
|
||||
const updatedInfo = [...calculatedPointInfo];
|
||||
updatedInfo.splice(closestIndex + 1, 0, pointInfo);
|
||||
|
||||
setPoints(updatedPoints);
|
||||
setCalculatedPointInfo(updatedInfo);
|
||||
}
|
||||
};
|
||||
|
||||
interface ProcessedRectangle {
|
||||
northWest: { lat: number; lon: number };
|
||||
southEast: { lat: number; lon: number };
|
||||
fence_type: string;
|
||||
}
|
||||
|
||||
const processRectangle = (rectangle: MapRectangle): ProcessedRectangle | null => {
|
||||
if (!rectangle || !rectangle.bounds || !rectangle.color) {
|
||||
console.error('Invalid rectangle:', rectangle);
|
||||
return null;
|
||||
}
|
||||
|
||||
const bounds = rectangle.bounds as L.LatLngBounds;
|
||||
const southWest = bounds.getSouthWest?.() ?? (bounds as unknown as { _southWest: L.LatLng })._southWest;
|
||||
const northEast = bounds.getNorthEast?.() ?? (bounds as unknown as { _northEast: L.LatLng })._northEast;
|
||||
|
||||
const northWest = { lat: northEast.lat, lon: southWest.lng };
|
||||
const southEast = { lat: southWest.lat, lon: northEast.lng };
|
||||
|
||||
const fenceType = rectangle.color === 'red' ? 'EXCLUSION' : rectangle.color === 'green' ? 'INCLUSION' : 'UNKNOWN';
|
||||
return {
|
||||
northWest,
|
||||
southEast,
|
||||
fence_type: fenceType,
|
||||
};
|
||||
};
|
||||
|
||||
const isRectangleDuplicate = (existingRectangles: ProcessedRectangle[], newRectangle: ProcessedRectangle) => {
|
||||
const newPointsString = JSON.stringify({ nw: newRectangle.northWest, se: newRectangle.southEast });
|
||||
return existingRectangles.some(rectangle => {
|
||||
const existingPointsString = JSON.stringify({ nw: rectangle.northWest, se: rectangle.southEast });
|
||||
return existingPointsString === newPointsString;
|
||||
});
|
||||
};
|
||||
|
||||
const exportMap = () => {
|
||||
const processedRectangles = rectangles.map(processRectangle).filter((rectangle): rectangle is ProcessedRectangle => rectangle !== null);
|
||||
|
||||
const uniqueRectangles: ProcessedRectangle[] = [];
|
||||
processedRectangles.forEach(rectangle => {
|
||||
if (!isRectangleDuplicate(uniqueRectangles, rectangle)) {
|
||||
uniqueRectangles.push(rectangle);
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
geofences: {
|
||||
polygons: uniqueRectangles.map(rect => ({
|
||||
northWest: rect.northWest,
|
||||
southEast: rect.southEast,
|
||||
})),
|
||||
},
|
||||
action_points: points.map(point => ({
|
||||
lat: point.position.lat,
|
||||
lon: point.position.lng,
|
||||
})),
|
||||
};
|
||||
|
||||
setJsonText(JSON.stringify(data, null, 2));
|
||||
setJsonDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleJsonDialogClose = () => {
|
||||
setJsonDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleJsonSave = (updatedJson: string) => {
|
||||
try {
|
||||
const data = JSON.parse(updatedJson);
|
||||
|
||||
if (data.action_points && Array.isArray(data.action_points)) {
|
||||
const importedPoints: FlightPoint[] = data.action_points.map((ap: Record<string, unknown>) => ({
|
||||
id: newGuid(),
|
||||
position: {
|
||||
lat: (ap.point as Record<string, number>)?.lat || (ap.lat as number),
|
||||
lng: (ap.point as Record<string, number>)?.lon || (ap.lon as number),
|
||||
},
|
||||
altitude: parseFloat(String(ap.height || 300)),
|
||||
meta: (ap.action_specific as Record<string, string[]>)?.targets || [purposes[0].value, purposes[1].value],
|
||||
}));
|
||||
setPoints(importedPoints);
|
||||
}
|
||||
|
||||
if (data.geofences?.polygons && Array.isArray(data.geofences.polygons)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const importedRectangles: MapRectangle[] = data.geofences.polygons.map((polygon: any) => {
|
||||
const bounds = L.latLngBounds(
|
||||
[polygon.southEast.lat, polygon.northWest.lon],
|
||||
[polygon.northWest.lat, polygon.southEast.lon]
|
||||
);
|
||||
const color = polygon.fence_type === 'EXCLUSION' ? 'red' : 'green';
|
||||
return {
|
||||
id: newGuid(),
|
||||
bounds,
|
||||
color,
|
||||
};
|
||||
});
|
||||
setRectangles(importedRectangles);
|
||||
}
|
||||
|
||||
if (data.operational_height?.currentAltitude) {
|
||||
setCurrentAltitude(data.operational_height.currentAltitude);
|
||||
}
|
||||
|
||||
setJsonDialogOpen(false);
|
||||
} catch {
|
||||
alert('Invalid JSON format');
|
||||
}
|
||||
};
|
||||
|
||||
const editAsJson = () => {
|
||||
const processedRectangles = rectangles.map(processRectangle).filter((rectangle): rectangle is ProcessedRectangle => rectangle !== null);
|
||||
|
||||
const uniqueRectangles: ProcessedRectangle[] = [];
|
||||
processedRectangles.forEach(rectangle => {
|
||||
if (!isRectangleDuplicate(uniqueRectangles, rectangle)) {
|
||||
uniqueRectangles.push(rectangle);
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
operational_height: { currentAltitude },
|
||||
geofences: {
|
||||
polygons: uniqueRectangles,
|
||||
},
|
||||
action_points: points.map(point => ({
|
||||
point: { lat: point.position.lat, lon: point.position.lng },
|
||||
height: Number(point.altitude),
|
||||
action: 'search',
|
||||
action_specific: {
|
||||
targets: point.meta,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
setJsonText(JSON.stringify(data, null, 2));
|
||||
setJsonDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'flight-plan-board'}>
|
||||
<LeftBoard
|
||||
points={points}
|
||||
setPoints={setPoints}
|
||||
calculatedPointInfo={calculatedPointInfo}
|
||||
setCalculatedPointInfo={setCalculatedPointInfo}
|
||||
aircraft={aircraft}
|
||||
initialAltitude={initialAltitude}
|
||||
setInitialAltitude={setInitialAltitude}
|
||||
currentAltitude={currentAltitude}
|
||||
actionMode={actionMode}
|
||||
setActionMode={setActionMode}
|
||||
setRectangleColor={setRectangleColor}
|
||||
editAsJson={editAsJson}
|
||||
exportMap={exportMap}
|
||||
updatePosition={updatePosition}
|
||||
currentPosition={mapCenter}
|
||||
/>
|
||||
|
||||
<MapView
|
||||
points={points}
|
||||
setPoints={setPoints}
|
||||
calculatedPointInfo={calculatedPointInfo}
|
||||
setCalculatedPointInfo={setCalculatedPointInfo}
|
||||
initialAltitude={initialAltitude}
|
||||
currentPosition={currentPosition}
|
||||
updatePosition={updatePosition}
|
||||
updateMapCenter={updateMapCenter}
|
||||
handlePolylineClick={handlePolylineClick}
|
||||
rectangles={rectangles}
|
||||
setRectangles={setRectangles}
|
||||
rectangleColor={rectangleColor}
|
||||
actionMode={actionMode}
|
||||
/>
|
||||
|
||||
<AltitudeDialog
|
||||
openDialog={openDialog}
|
||||
handleDialogClose={handleDialogClose}
|
||||
handleAltitudeSubmit={handleAltitudeSubmit}
|
||||
altitude={altitude}
|
||||
setAltitude={setAltitude}
|
||||
latitude={latitude}
|
||||
setLatitude={setLatitude}
|
||||
longitude={longitude}
|
||||
setLongitude={setLongitude}
|
||||
meta={meta}
|
||||
setMeta={setMeta}
|
||||
/>
|
||||
|
||||
<JsonEditorDialog
|
||||
open={jsonDialogOpen}
|
||||
jsonText={jsonText}
|
||||
onClose={handleJsonDialogClose}
|
||||
onSave={handleJsonSave}
|
||||
/>
|
||||
</div>
|
||||
<div className='mobile-message'>
|
||||
<RotatePhoneIcon width='100px' height='100px' color='#000000' />
|
||||
Please switch to the landscape mode
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
interface SatelliteMapIconProps {
|
||||
width?: string;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
export function SatelliteMapIcon({
|
||||
width = '100%',
|
||||
height = '100%',
|
||||
}: SatelliteMapIconProps) {
|
||||
return (
|
||||
<svg width={width} height={height} viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_11_18)">
|
||||
<path d="M0 40H40V0H0V40Z" fill="#43B05B" />
|
||||
<path d="M40 29.6552V20L32.2759 12.4827L40 4.68969V0H35.3103L14.8276 20.4827V0H8.89656V8.89656H0V14.8276H8.89656V26.4138L7.72414 27.5173L0 35.3103V40H4.68969L27.4483 17.2414L40 29.6552Z" fill="#F0CE49" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_11_18">
|
||||
<rect width={width} height={height} fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
interface RotatePhoneIconProps {
|
||||
width?: string;
|
||||
height?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function RotatePhoneIcon({
|
||||
width = '80px',
|
||||
height = '80px',
|
||||
color = '#1976d2',
|
||||
}: RotatePhoneIconProps) {
|
||||
return (
|
||||
<svg width={width} height={height} viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_61_2)">
|
||||
<path d="M41.2215 17.1275C40.4263 17.0233 40.0965 17.4373 39.9922 17.6229C39.888 17.8056 39.6968 18.3039 40.1937 18.9378L42.782 22.2659C43.3875 22.8727 44.3584 22.9327 45.0394 22.4058L48.2047 19.963C48.6329 19.636 48.7428 19.2862 48.7587 19.0535C48.7929 18.5038 48.3631 18.0427 47.6607 17.9514L46.1073 17.7529C45.9217 17.7258 45.7261 17.5016 45.7146 17.2947C45.4805 12.7901 43.5887 8.54395 40.3861 5.34012C37.9818 2.93568 34.9592 1.25821 31.6455 0.482857C29.2854 -0.0725654 26.8683 -0.149608 24.4625 0.255761C19.9179 1.02529 15.866 3.43824 13.0562 7.05043C12.8206 7.35457 12.6393 7.78279 13.2132 8.35687C13.319 8.46258 13.4589 8.58681 13.6159 8.70954C13.9657 8.98083 14.7753 9.60478 15.2892 8.94649C17.7263 5.81402 21.1316 3.78807 24.9465 3.14411C26.9625 2.80279 28.9928 2.86565 30.983 3.3297C33.7643 3.98217 36.3029 5.39268 38.3203 7.41161C40.9445 10.0343 42.5293 13.5096 42.7835 17.1945L42.6309 17.3087L41.2215 17.1275Z" fill={color} />
|
||||
<path d="M53.6543 30.9968H30.9017V34.9788H50.9244C51.3727 34.9788 51.7354 35.3428 51.7354 35.7912V51.9505C51.7354 52.3988 51.3727 52.7628 50.9244 52.7628H30.9017V56.7476H53.6556C55.2877 56.7476 56.6082 55.4242 56.6082 53.7964V33.9465C56.6083 32.3159 55.2862 30.9968 53.6543 30.9968Z" fill={color} />
|
||||
<path d="M26.1915 60C27.8206 60 29.1427 58.6779 29.1427 57.0474V19.1834C29.1427 17.5515 27.8191 16.2309 26.1915 16.2309H6.34152C4.7096 16.2309 3.39181 17.553 3.39181 19.1834V57.0474C3.39181 58.6794 4.71109 60 6.34152 60H26.1915ZM16.2173 58.1968C15.1893 58.1968 14.3584 57.3659 14.3584 56.3393C14.3584 55.3128 15.1893 54.4804 16.2173 54.4804C17.2424 54.4804 18.0748 55.3128 18.0748 56.3393C18.0733 57.3659 17.241 58.1968 16.2173 58.1968ZM7.37234 21.9134C7.37234 21.4652 7.73635 21.1023 8.18472 21.1023H24.3439C24.7923 21.1023 25.1563 21.465 25.1563 21.9134V51.0652C25.1563 51.5135 24.7923 51.8762 24.3439 51.8762H8.18622C7.738 51.8762 7.37384 51.5135 7.37384 51.0652V21.9134H7.37234Z" fill={color} />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_61_2">
|
||||
<rect width={width} height={height} fill="none" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { FaMapPin } from 'react-icons/fa';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { divIcon } from 'leaflet';
|
||||
import L from 'leaflet';
|
||||
|
||||
export const defaultIcon = new L.Icon({
|
||||
iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
});
|
||||
|
||||
const pointIconSVG = renderToStaticMarkup(<FaMapPin style={{ fontSize: '24px', color: '#ff4500', zIndex: '1' }} />);
|
||||
export const pointIconRed = divIcon({
|
||||
className: 'custom-icon',
|
||||
html: `<div style="display: flex; align-items: center; justify-content: center;">${pointIconSVG}</div>`,
|
||||
iconSize: [30, 30],
|
||||
iconAnchor: [15, 30],
|
||||
popupAnchor: [0, -30],
|
||||
});
|
||||
|
||||
const pointIconGreenSVG = renderToStaticMarkup(<FaMapPin style={{ fontSize: '24px', color: '#1ed013', zIndex: '1' }} />);
|
||||
export const pointIconGreen = divIcon({
|
||||
className: 'custom-icon',
|
||||
html: `<div style="display: flex; align-items: center; justify-content: center;">${pointIconGreenSVG}</div>`,
|
||||
iconSize: [30, 30],
|
||||
iconAnchor: [15, 30],
|
||||
popupAnchor: [0, -30],
|
||||
});
|
||||
|
||||
const pointIconBlueSVG = renderToStaticMarkup(<FaMapPin style={{ fontSize: '24px', color: '#0000ff', zIndex: '1' }} />);
|
||||
export const pointIconBlue = divIcon({
|
||||
className: 'custom-icon',
|
||||
html: `<div style="display: flex; align-items: center; justify-content: center;">${pointIconBlueSVG}</div>`,
|
||||
iconSize: [30, 30],
|
||||
iconAnchor: [15, 30],
|
||||
popupAnchor: [0, -30],
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
interface IconProps {
|
||||
className?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function HideSidebarIcon({
|
||||
width = '100%',
|
||||
height = '100%',
|
||||
color = '#1976d2',
|
||||
}: IconProps) {
|
||||
return (
|
||||
<svg width={width} height={height} viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1.5" y="1.5" width="37" height="37" rx="4.5" stroke={color} strokeWidth="3" />
|
||||
<line x1="13.5" y1="40" x2="13.5" stroke={color} strokeWidth="3" />
|
||||
<path d="M24.2928 19.2929C23.9023 19.6834 23.9023 20.3166 24.2928 20.7071L30.6568 27.0711C31.0473 27.4616 31.6804 27.4616 32.071 27.0711C32.4615 26.6805 32.4615 26.0474 32.071 25.6569L26.4141 20L32.071 14.3431C32.4615 13.9526 32.4615 13.3195 32.071 12.9289C31.6804 12.5384 31.0473 12.5384 30.6568 12.9289L24.2928 19.2929ZM25 19H24.9999V21H25V19Z" fill={color} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ShowSidebarIcon({
|
||||
width = '100%',
|
||||
height = '100%',
|
||||
color = '#1976d2',
|
||||
}: IconProps) {
|
||||
return (
|
||||
<svg width={width} height={height} viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1.5" y="1.5" width="37" height="37" rx="4.5" stroke={color} strokeWidth="3" />
|
||||
<line x1="13.5" y1="40" x2="13.5" stroke={color} strokeWidth="3" />
|
||||
<path d="M10.5304 20.5303C10.8233 20.2374 10.8233 19.7626 10.5304 19.4697L5.75746 14.6967C5.46457 14.4038 4.98969 14.4038 4.6968 14.6967C4.40391 14.9896 4.40391 15.4645 4.6968 15.7574L8.93944 20L4.6968 24.2426C4.40391 24.5355 4.40391 25.0104 4.6968 25.3033C4.98969 25.5962 5.46457 25.5962 5.75746 25.3033L10.5304 20.5303ZM10 20.75H10.0001V19.25H10V20.75Z" fill={color} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function DashedAreaIcon({
|
||||
className,
|
||||
width,
|
||||
height,
|
||||
color = '#1976d2',
|
||||
}: IconProps) {
|
||||
return (
|
||||
<svg className={className} width={width} height={height} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 1C2.46957 1 1.96086 1.21071 1.58579 1.58579C1.21071 1.96086 1 2.46957 1 3M17 1C17.5304 1 18.0391 1.21071 18.4142 1.58579C18.7893 1.96086 19 2.46957 19 3M19 17C19 17.5304 18.7893 18.0391 18.4142 18.4142C18.0391 18.7893 17.5304 19 17 19M3 19C2.46957 19 1.96086 18.7893 1.58579 18.4142C1.21071 18.0391 1 17.5304 1 17M7 1H8M7 19H8M12 1H13M12 19H13M1 7V8M19 7V8M1 12V13M19 12V13" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar{
|
||||
display: none;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 299.9 317.1" style="enable-background:new 0 0 299.9 317.1;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#2668B0;}
|
||||
.st1{fill:#DEC354;}
|
||||
.st2{fill:url(#SVGID_1_);}
|
||||
.st3{fill:url(#SVGID_00000165956440124760579960000011888077842116744085_);}
|
||||
.st4{fill:url(#SVGID_00000085942576275962151710000009003104917087656334_);}
|
||||
.st5{fill:url(#SVGID_00000065057939898373943770000009142597344814158991_);}
|
||||
.st6{fill:url(#SVGID_00000042707575094020397520000008210262669309136559_);}
|
||||
.st7{fill:url(#SVGID_00000132081218125063996810000014374475908356398251_);}
|
||||
.st8{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2_00000152245310683770285410000018119804670128610436_">
|
||||
<polygon class="st0" points="0.1,169.5 12.1,166.5 12.1,51.2 0,20.1 "/>
|
||||
<polygon class="st1" points="241,169.1 229.1,166.1 229,50.7 241.1,19.7 "/>
|
||||
<polygon class="st0" points="119.6,153.6 107.7,158.3 107.7,18 119.8,0 "/>
|
||||
<polygon class="st1" points="119.5,153.8 131.8,158.3 131.9,18 119.8,0 "/>
|
||||
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="120.3286" y1="259.6066" x2="120.3286" y2="78.8283" gradientTransform="matrix(1 0 0 -1 0 320)">
|
||||
<stop offset="3.000000e-02" style="stop-color:#FFFFFF"/>
|
||||
<stop offset="0.1" style="stop-color:#EDEDED"/>
|
||||
<stop offset="0.26" style="stop-color:#C0BEBF"/>
|
||||
<stop offset="0.48" style="stop-color:#767474"/>
|
||||
<stop offset="0.7" style="stop-color:#231F20"/>
|
||||
</linearGradient>
|
||||
<polygon class="st2" points="0.1,169.5 47,153.1 82.2,166.3 102.5,157.3 119.5,149.8 157.9,166.7 194,153.3 240.6,168.8 121.1,255
|
||||
"/>
|
||||
|
||||
<linearGradient id="SVGID_00000114788463727787464510000017845518411005303225_" gradientUnits="userSpaceOnUse" x1="58.437" y1="149.7064" x2="68.2775" y2="175.7949" gradientTransform="matrix(1 0 0 -1 0 320)">
|
||||
<stop offset="0.27" style="stop-color:#231F20"/>
|
||||
<stop offset="0.28" style="stop-color:#2C2829"/>
|
||||
<stop offset="0.37" style="stop-color:#767474"/>
|
||||
<stop offset="0.44" style="stop-color:#B1AFB0"/>
|
||||
<stop offset="0.51" style="stop-color:#DBDADA"/>
|
||||
<stop offset="0.56" style="stop-color:#F5F5F5"/>
|
||||
<stop offset="0.59" style="stop-color:#FFFFFF"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_00000114788463727787464510000017845518411005303225_);" points="47,153.1 42,154.8 76.9,169.3
|
||||
82.1,166.4 "/>
|
||||
|
||||
<linearGradient id="SVGID_00000102533530282145531860000014371344717116336017_" gradientUnits="userSpaceOnUse" x1="-45.5872" y1="149.6743" x2="-35.8351" y2="175.5215" gradientTransform="matrix(-1 0 0 -1 136.44 320)">
|
||||
<stop offset="0.27" style="stop-color:#231F20"/>
|
||||
<stop offset="0.28" style="stop-color:#2C2829"/>
|
||||
<stop offset="0.37" style="stop-color:#767474"/>
|
||||
<stop offset="0.44" style="stop-color:#B1AFB0"/>
|
||||
<stop offset="0.51" style="stop-color:#DBDADA"/>
|
||||
<stop offset="0.56" style="stop-color:#F5F5F5"/>
|
||||
<stop offset="0.59" style="stop-color:#FFFFFF"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_00000102533530282145531860000014371344717116336017_);" points="194,153.3 199,155 163.6,169.4
|
||||
157.9,166.7 "/>
|
||||
|
||||
<linearGradient id="SVGID_00000075841790156840098560000006699003196656470950_" gradientUnits="userSpaceOnUse" x1="119.6532" y1="175.4803" x2="119.6532" y2="164.7475" gradientTransform="matrix(1 0 0 -1 0 320)">
|
||||
<stop offset="3.000000e-02" style="stop-color:#FFFFFF"/>
|
||||
<stop offset="0.44" style="stop-color:#9B9A9A"/>
|
||||
<stop offset="0.83" style="stop-color:#454142"/>
|
||||
<stop offset="1" style="stop-color:#231F20"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_00000075841790156840098560000006699003196656470950_);" points="119.5,149.8 133.7,156.1
|
||||
119.5,154.4 105.6,156 "/>
|
||||
|
||||
<linearGradient id="SVGID_00000080899814652598392920000002338730188455222667_" gradientUnits="userSpaceOnUse" x1="19.1991" y1="43.9763" x2="111.4936" y2="184.9104" gradientTransform="matrix(1 0 0 -1 0 320)">
|
||||
<stop offset="0.1" style="stop-color:#FFFFFF"/>
|
||||
<stop offset="0.17" style="stop-color:#FCFCFC"/>
|
||||
<stop offset="0.2" style="stop-color:#F5F4F4"/>
|
||||
<stop offset="0.23" style="stop-color:#E7E7E7"/>
|
||||
<stop offset="0.26" style="stop-color:#D4D4D4"/>
|
||||
<stop offset="0.28" style="stop-color:#BCBBBB"/>
|
||||
<stop offset="0.3" style="stop-color:#9E9C9D"/>
|
||||
<stop offset="0.32" style="stop-color:#7A7878"/>
|
||||
<stop offset="0.33" style="stop-color:#524F50"/>
|
||||
<stop offset="0.35" style="stop-color:#252122"/>
|
||||
<stop offset="0.35" style="stop-color:#231F20"/>
|
||||
<stop offset="0.37" style="stop-color:#262324"/>
|
||||
<stop offset="0.39" style="stop-color:#322F30"/>
|
||||
<stop offset="0.4" style="stop-color:#464344"/>
|
||||
<stop offset="0.42" style="stop-color:#625F60"/>
|
||||
<stop offset="0.44" style="stop-color:#868484"/>
|
||||
<stop offset="0.46" style="stop-color:#B2B0B0"/>
|
||||
<stop offset="0.47" style="stop-color:#E5E4E4"/>
|
||||
<stop offset="0.47" style="stop-color:#E7E6E6"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_00000080899814652598392920000002338730188455222667_);" points="0.1,169.5 0.9,169.3 122.5,253.9
|
||||
121.1,255 "/>
|
||||
|
||||
<linearGradient id="SVGID_00000182519911580066729170000000492446570726535336_" gradientUnits="userSpaceOnUse" x1="-393.4447" y1="33.4059" x2="-285.4408" y2="198.3462" gradientTransform="matrix(-1 0 0 -1 -163.84 320)">
|
||||
<stop offset="0.1" style="stop-color:#FFFFFF"/>
|
||||
<stop offset="0.17" style="stop-color:#FCFCFC"/>
|
||||
<stop offset="0.2" style="stop-color:#F5F4F4"/>
|
||||
<stop offset="0.23" style="stop-color:#E7E7E7"/>
|
||||
<stop offset="0.26" style="stop-color:#D4D4D4"/>
|
||||
<stop offset="0.28" style="stop-color:#BCBBBB"/>
|
||||
<stop offset="0.3" style="stop-color:#9E9C9D"/>
|
||||
<stop offset="0.32" style="stop-color:#7A7878"/>
|
||||
<stop offset="0.33" style="stop-color:#524F50"/>
|
||||
<stop offset="0.35" style="stop-color:#252122"/>
|
||||
<stop offset="0.35" style="stop-color:#231F20"/>
|
||||
<stop offset="0.37" style="stop-color:#262324"/>
|
||||
<stop offset="0.39" style="stop-color:#322F30"/>
|
||||
<stop offset="0.4" style="stop-color:#464344"/>
|
||||
<stop offset="0.42" style="stop-color:#625F60"/>
|
||||
<stop offset="0.44" style="stop-color:#868484"/>
|
||||
<stop offset="0.46" style="stop-color:#B2B0B0"/>
|
||||
<stop offset="0.47" style="stop-color:#E5E4E4"/>
|
||||
<stop offset="0.47" style="stop-color:#E7E6E6"/>
|
||||
</linearGradient>
|
||||
<polygon style="fill:url(#SVGID_00000182519911580066729170000000492446570726535336_);" points="241.1,169 240.4,168.7 119.7,254
|
||||
121.1,255 "/>
|
||||
</g>
|
||||
<g id="Layer_3">
|
||||
<g>
|
||||
<path class="st8" d="M175.3,195.5h-1.6v-10.9l19.7,8.5v-8.5h1.6v10.9l-19.7-8.5V195.5L175.3,195.5L175.3,195.5z"/>
|
||||
<path class="st8" d="M150.7,195.6c-0.6,0-1.2-0.1-1.7-0.3c-0.5-0.2-1-0.5-1.4-0.9c-0.4-0.4-0.7-0.9-0.9-1.4
|
||||
c-0.2-0.5-0.3-1.1-0.3-1.7v-2.4c0-0.6,0.1-1.2,0.3-1.7c0.2-0.5,0.5-1,0.9-1.4c0.4-0.4,0.9-0.7,1.4-0.9s1.1-0.3,1.7-0.3h12.6
|
||||
c0.6,0,1.2,0.1,1.7,0.3s1,0.5,1.4,0.9c0.4,0.4,0.7,0.9,0.9,1.4c0.2,0.5,0.3,1.1,0.3,1.7v2.4c0,0.6-0.1,1.2-0.3,1.7s-0.5,1-0.9,1.4
|
||||
c-0.4,0.4-0.9,0.7-1.4,0.9c-0.5,0.2-1.1,0.3-1.7,0.3H150.7z M150.7,186.1c-0.4,0-0.7,0.1-1.1,0.2c-0.3,0.1-0.6,0.3-0.9,0.6
|
||||
c-0.2,0.2-0.5,0.5-0.6,0.9s-0.2,0.7-0.2,1.1v2.4c0,0.4,0.1,0.7,0.2,1.1c0.1,0.3,0.3,0.6,0.6,0.9c0.3,0.2,0.5,0.5,0.9,0.6
|
||||
c0.3,0.1,0.7,0.2,1.1,0.2h12.6c0.4,0,0.7-0.1,1.1-0.2c0.3-0.1,0.6-0.3,0.9-0.6c0.2-0.3,0.5-0.5,0.6-0.9c0.1-0.3,0.2-0.7,0.2-1.1
|
||||
v-2.4c0-0.4-0.1-0.7-0.2-1.1c-0.1-0.3-0.3-0.6-0.6-0.9c-0.3-0.2-0.5-0.4-0.9-0.6c-0.3-0.2-0.7-0.2-1.1-0.2H150.7z"/>
|
||||
<path class="st8" d="M137.2,195.6v-13h1.9v13L137.2,195.6L137.2,195.6z"/>
|
||||
<path class="st8" d="M57,184.5l11.9,11.1H51.4l1.5-1.6h12l-7.8-7.3l-9.5,8.9h-2.3L57,184.5L57,184.5L57,184.5z"/>
|
||||
<path class="st8" d="M96,184.6v1.6L78.7,194H96v1.6H75v-1.6l17.4-7.8H75v-1.6H96L96,184.6z"/>
|
||||
<path class="st8" d="M116.1,182.6l13.8,13h-20.4l1.8-1.8h14l-9.1-8.6L105,195.6h-2.7L116.1,182.6L116.1,182.6L116.1,182.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import { FlightPlan } from './flightPlanning/flightPlan';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import 'leaflet-draw/dist/leaflet.draw.css';
|
||||
import { LanguageProvider } from './flightPlanning/LanguageContext';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root')!);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<LanguageProvider>
|
||||
<FlightPlan />
|
||||
</LanguageProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { AircraftParams } from '../types';
|
||||
|
||||
export const mockGetAirplaneParams = (): Promise<AircraftParams> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
type: 'Plane',
|
||||
downang: 40,
|
||||
upang: 45,
|
||||
weight: 3.4,
|
||||
speed: 80,
|
||||
frontalArea: 0.12,
|
||||
dragCoefficient: 0.45,
|
||||
batteryCapacity: 315,
|
||||
thrustWatts: [
|
||||
{ thrust: 500, watts: 55.5 },
|
||||
{ thrust: 750, watts: 91.02 },
|
||||
{ thrust: 1000, watts: 137.64 },
|
||||
{ thrust: 1250, watts: 191 },
|
||||
{ thrust: 1500, watts: 246 },
|
||||
{ thrust: 1750, watts: 308 },
|
||||
{ thrust: 2000, watts: 381 },
|
||||
],
|
||||
propellerEfficiency: 0.95,
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { WeatherData } from '../types';
|
||||
|
||||
export const getWeatherData = async (lat: number, lon: number): Promise<WeatherData | null> => {
|
||||
const apiKey = '335799082893fad97fa36118b131f919';
|
||||
const url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=metric`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
return {
|
||||
windSpeed: data.wind.speed,
|
||||
windAngle: data.wind.deg,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { mockGetAirplaneParams } from './AircraftService';
|
||||
import { getWeatherData } from './WeatherService';
|
||||
import type { ThrustWattEntry } from '../types';
|
||||
|
||||
export const calculateBatteryPercentUsed = async (
|
||||
groundSpeed: number,
|
||||
time: number,
|
||||
position: { lat: number; lon: number },
|
||||
): Promise<number> => {
|
||||
const airplaneParams = await mockGetAirplaneParams();
|
||||
const weatherData = await getWeatherData(position.lat, position.lon);
|
||||
|
||||
const airDensity = 1.05;
|
||||
const groundSpeedInMs = groundSpeed / 3.6;
|
||||
|
||||
const headwind = (weatherData?.windSpeed ?? 0) * Math.cos(Math.PI / 180 * (weatherData?.windAngle ?? 0));
|
||||
const effectiveAirspeed = groundSpeedInMs + headwind;
|
||||
|
||||
const drag = dragForce(effectiveAirspeed, airDensity, airplaneParams.dragCoefficient, airplaneParams.frontalArea);
|
||||
|
||||
const effectivePowerConsumption = calcThrust(drag, airplaneParams.thrustWatts, airplaneParams.propellerEfficiency, airplaneParams.weight);
|
||||
|
||||
const energyUsed = effectivePowerConsumption * time;
|
||||
const batteryPercentUsed = (energyUsed / airplaneParams.batteryCapacity) * 100;
|
||||
|
||||
return Math.min(batteryPercentUsed, 100);
|
||||
};
|
||||
|
||||
function dragForce(airspeed: number, airDensity: number, dragCoefficient: number, frontalArea: number): number {
|
||||
return 0.5 * airDensity * (airspeed ** 2) * dragCoefficient * frontalArea;
|
||||
}
|
||||
|
||||
function calcThrust(
|
||||
drag: number,
|
||||
thrustWatts: ThrustWattEntry[],
|
||||
propellerEfficiency: number,
|
||||
weight: number,
|
||||
): number {
|
||||
let closest: ThrustWattEntry | null = null;
|
||||
const adjustedDrag = drag + (weight * 9.8 * 0.05);
|
||||
for (const item of thrustWatts) {
|
||||
const thrustInNewtons = (item.thrust / 1000) * 9.8;
|
||||
if (thrustInNewtons > adjustedDrag) {
|
||||
if (!closest || thrustInNewtons < (closest.thrust / 1000) * 9.8) {
|
||||
closest = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
const watts = closest ? closest.watts : thrustWatts[thrustWatts.length - 1].watts;
|
||||
return watts / propellerEfficiency;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { FlightPoint } from '../types';
|
||||
|
||||
export const calculateDistance = (
|
||||
point1: FlightPoint,
|
||||
point2: FlightPoint,
|
||||
aircraftType: string,
|
||||
initialAltitude: number,
|
||||
downang: number,
|
||||
upang: number,
|
||||
): number => {
|
||||
if (!point1?.position || !point2?.position) {
|
||||
console.error('Invalid points provided:', { point1, point2 });
|
||||
return 0;
|
||||
}
|
||||
|
||||
const R = 6371;
|
||||
const { lat: lat1, lng: lon1 } = point1.position;
|
||||
const { lat: lat2, lng: lon2 } = point2.position;
|
||||
const alt1 = point1.altitude || 0;
|
||||
const alt2 = point2.altitude || 0;
|
||||
|
||||
const toRad = (value: number) => (value * Math.PI) / 180;
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lon2 - lon1);
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRad(lat1)) *
|
||||
Math.cos(toRad(lat2)) *
|
||||
Math.sin(dLon / 2) *
|
||||
Math.sin(dLon / 2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
const horizontalDistance = R * c;
|
||||
|
||||
const initialAltitudeKm = initialAltitude / 1000;
|
||||
const altitude1Km = alt1 / 1000;
|
||||
const altitude2Km = alt2 / 1000;
|
||||
|
||||
const descentAngleRad = toRad(downang || 0.01);
|
||||
const ascentAngleRad = toRad(upang || 0.01);
|
||||
|
||||
if (aircraftType === 'Plane') {
|
||||
const ascentDistanceInclined = Math.max(0, (initialAltitudeKm - altitude1Km) / Math.sin(ascentAngleRad));
|
||||
const descentDistanceInclined = Math.max(0, (initialAltitudeKm - altitude2Km) / Math.sin(descentAngleRad));
|
||||
|
||||
const horizontalAscentProjection = Math.max(0, ascentDistanceInclined * Math.cos(ascentAngleRad));
|
||||
const horizontalDescentProjection = Math.max(0, descentDistanceInclined * Math.cos(descentAngleRad));
|
||||
|
||||
const adjustedHorizontalDistance = horizontalDistance - (horizontalDescentProjection + horizontalAscentProjection);
|
||||
const totalDistance = adjustedHorizontalDistance + Math.max(0, descentDistanceInclined) + Math.max(0, ascentDistanceInclined);
|
||||
return totalDistance;
|
||||
} else if (aircraftType === 'VTOL') {
|
||||
const ascentDistanceVertical = Math.abs(initialAltitudeKm - altitude1Km);
|
||||
const horizontalFlightDistance = horizontalDistance;
|
||||
const descentDistanceVertical = Math.abs(initialAltitudeKm - altitude2Km);
|
||||
const totalDistance = ascentDistanceVertical + horizontalFlightDistance + descentDistanceVertical;
|
||||
return totalDistance;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -0,0 +1,176 @@
|
||||
const testData = {
|
||||
"geofences": {
|
||||
"polygons": [
|
||||
{
|
||||
"northWest": {
|
||||
"lat": 48.28022277841604,
|
||||
"lon": 37.37548828125001
|
||||
},
|
||||
"southEast": {
|
||||
"lat": 48.2720540660028,
|
||||
"lon": 37.3901653289795
|
||||
}
|
||||
},
|
||||
{
|
||||
"northWest": {
|
||||
"lat": 48.2614270732573,
|
||||
"lon": 37.35239982604981
|
||||
},
|
||||
"southEast": {
|
||||
"lat": 48.24988342757033,
|
||||
"lon": 37.37943649291993
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"action_points": [
|
||||
{ "lat": 48.276067180586544, "lon": 37.38445758819581 },
|
||||
{ "lat": 48.27074009522731, "lon": 37.374029159545906 },
|
||||
{ "lat": 48.263312668696855, "lon": 37.37707614898682 },
|
||||
{ "lat": 48.26539817051818, "lon": 37.36587524414063 },
|
||||
{ "lat": 48.25851283439989, "lon": 37.35952377319337 },
|
||||
{ "lat": 48.254426906081555, "lon": 37.374801635742195 },
|
||||
{ "lat": 48.25914140977405, "lon": 37.39068031311036 },
|
||||
{ "lat": 48.25354110233028, "lon": 37.401752471923835 },
|
||||
{ "lat": 48.25902712391726, "lon": 37.416257858276374 },
|
||||
{ "lat": 48.26828345053738, "lon": 37.402009963989265 }
|
||||
]
|
||||
};
|
||||
|
||||
const newGuid = () => Math.random().toString(36).substring(2, 15);
|
||||
|
||||
const purposes = [
|
||||
{ value: 'artillery' },
|
||||
{ value: 'tank' }
|
||||
];
|
||||
|
||||
describe('JSON Import Functionality', () => {
|
||||
describe('Action Points Import', () => {
|
||||
it('should correctly import action points with lat/lon format', () => {
|
||||
const importedPoints = testData.action_points.map(ap => ({
|
||||
id: newGuid(),
|
||||
position: { lat: (ap as Record<string, unknown> & { point?: { lat: number; lon: number } }).point?.lat || ap.lat, lng: (ap as Record<string, unknown> & { point?: { lat: number; lon: number } }).point?.lon || ap.lon },
|
||||
altitude: parseFloat(String((ap as Record<string, unknown>).height || 300)),
|
||||
meta: (ap as Record<string, unknown> & { action_specific?: { targets: string[] } }).action_specific?.targets || [purposes[0].value, purposes[1].value]
|
||||
}));
|
||||
|
||||
expect(importedPoints).toHaveLength(10);
|
||||
expect(importedPoints[0].position.lat).toBe(48.276067180586544);
|
||||
expect(importedPoints[0].position.lng).toBe(37.38445758819581);
|
||||
expect(importedPoints[0].altitude).toBe(300);
|
||||
expect(importedPoints[0].meta).toEqual(['artillery', 'tank']);
|
||||
});
|
||||
|
||||
it('should handle point.lat/point.lon format', () => {
|
||||
const dataWithPointFormat = {
|
||||
action_points: [{
|
||||
point: { lat: 48.276, lon: 37.384 },
|
||||
height: 500
|
||||
}]
|
||||
};
|
||||
|
||||
const importedPoints = dataWithPointFormat.action_points.map(ap => ({
|
||||
id: newGuid(),
|
||||
position: { lat: ap.point?.lat || 0, lng: ap.point?.lon || 0 },
|
||||
altitude: parseFloat(String(ap.height || 300)),
|
||||
meta: [purposes[0].value, purposes[1].value]
|
||||
}));
|
||||
|
||||
expect(importedPoints[0].position.lat).toBe(48.276);
|
||||
expect(importedPoints[0].position.lng).toBe(37.384);
|
||||
expect(importedPoints[0].altitude).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Geofences Import', () => {
|
||||
it('should correctly convert northWest/southEast to Leaflet bounds', () => {
|
||||
const polygon = testData.geofences.polygons[0];
|
||||
|
||||
const bounds = {
|
||||
_southWest: { lat: polygon.southEast.lat, lng: polygon.northWest.lon },
|
||||
_northEast: { lat: polygon.northWest.lat, lng: polygon.southEast.lon }
|
||||
};
|
||||
|
||||
expect(bounds._southWest.lat).toBe(48.2720540660028);
|
||||
expect(bounds._southWest.lng).toBe(37.37548828125001);
|
||||
expect(bounds._northEast.lat).toBe(48.28022277841604);
|
||||
expect(bounds._northEast.lng).toBe(37.3901653289795);
|
||||
});
|
||||
|
||||
it('should correctly import all geofences with default color', () => {
|
||||
const importedRectangles = testData.geofences.polygons.map((polygon) => {
|
||||
const bounds = {
|
||||
_southWest: { lat: polygon.southEast.lat, lng: polygon.northWest.lon },
|
||||
_northEast: { lat: polygon.northWest.lat, lng: polygon.southEast.lon }
|
||||
};
|
||||
const color = (polygon as Record<string, unknown>).fence_type === "EXCLUSION" ? "red" : "green";
|
||||
return {
|
||||
id: newGuid(),
|
||||
bounds: bounds,
|
||||
color: color
|
||||
};
|
||||
});
|
||||
|
||||
expect(importedRectangles).toHaveLength(2);
|
||||
expect(importedRectangles[0].color).toBe('green');
|
||||
expect(importedRectangles[1].color).toBe('green');
|
||||
});
|
||||
|
||||
it('should correctly import geofences with fence_type', () => {
|
||||
const dataWithFenceType = {
|
||||
geofences: {
|
||||
polygons: [
|
||||
{
|
||||
northWest: { lat: 48.28, lon: 37.375 },
|
||||
southEast: { lat: 48.27, lon: 37.390 },
|
||||
fence_type: "EXCLUSION"
|
||||
},
|
||||
{
|
||||
northWest: { lat: 48.26, lon: 37.352 },
|
||||
southEast: { lat: 48.25, lon: 37.379 },
|
||||
fence_type: "INCLUSION"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const importedRectangles = dataWithFenceType.geofences.polygons.map((polygon) => {
|
||||
const bounds = {
|
||||
_southWest: { lat: polygon.southEast.lat, lng: polygon.northWest.lon },
|
||||
_northEast: { lat: polygon.northWest.lat, lng: polygon.southEast.lon }
|
||||
};
|
||||
const color = polygon.fence_type === "EXCLUSION" ? "red" : "green";
|
||||
return {
|
||||
id: newGuid(),
|
||||
bounds: bounds,
|
||||
color: color
|
||||
};
|
||||
});
|
||||
|
||||
expect(importedRectangles[0].color).toBe('red');
|
||||
expect(importedRectangles[1].color).toBe('green');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bounds Validation', () => {
|
||||
it('should ensure southWest is bottom-left and northEast is top-right', () => {
|
||||
const polygon = {
|
||||
northWest: { lat: 50.0, lon: 10.0 },
|
||||
southEast: { lat: 40.0, lon: 20.0 }
|
||||
};
|
||||
|
||||
const bounds = {
|
||||
_southWest: { lat: polygon.southEast.lat, lng: polygon.northWest.lon },
|
||||
_northEast: { lat: polygon.northWest.lat, lng: polygon.southEast.lon }
|
||||
};
|
||||
|
||||
expect(bounds._southWest.lat).toBeLessThan(bounds._northEast.lat);
|
||||
expect(bounds._southWest.lng).toBeLessThan(bounds._northEast.lng);
|
||||
|
||||
expect(bounds._southWest.lat).toBe(40.0);
|
||||
expect(bounds._southWest.lng).toBe(10.0);
|
||||
expect(bounds._northEast.lat).toBe(50.0);
|
||||
expect(bounds._northEast.lng).toBe(20.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
import type L from 'leaflet';
|
||||
|
||||
export interface LatLngPosition {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
|
||||
export interface FlightPoint {
|
||||
id: string;
|
||||
position: LatLngPosition;
|
||||
altitude: number;
|
||||
meta: string[];
|
||||
}
|
||||
|
||||
export interface CalculatedPointInfo {
|
||||
bat: number;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface MapRectangle {
|
||||
layer?: L.Layer;
|
||||
color: string;
|
||||
bounds: L.LatLngBounds | L.LatLngBoundsLiteral;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface ThrustWattEntry {
|
||||
thrust: number;
|
||||
watts: number;
|
||||
}
|
||||
|
||||
export interface AircraftParams {
|
||||
type: string;
|
||||
downang: number;
|
||||
upang: number;
|
||||
weight: number;
|
||||
speed: number;
|
||||
frontalArea: number;
|
||||
dragCoefficient: number;
|
||||
batteryCapacity: number;
|
||||
thrustWatts: ThrustWattEntry[];
|
||||
propellerEfficiency: number;
|
||||
}
|
||||
|
||||
export interface WeatherData {
|
||||
windSpeed: number;
|
||||
windAngle: number;
|
||||
}
|
||||
|
||||
export interface Purpose {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface Language {
|
||||
code: string;
|
||||
flag: string;
|
||||
}
|
||||
|
||||
export interface MovingPointInfo {
|
||||
x: number;
|
||||
y: number;
|
||||
latlng: L.LatLng;
|
||||
}
|
||||
|
||||
export const ActionMode = {
|
||||
points: 'points',
|
||||
workArea: 'workArea',
|
||||
prohibitedArea: 'prohibitedArea',
|
||||
} as const;
|
||||
|
||||
export type ActionModeValue = (typeof ActionMode)[keyof typeof ActionMode];
|
||||
|
||||
export const MapType = {
|
||||
classic: 'classic',
|
||||
satellite: 'satellite',
|
||||
} as const;
|
||||
|
||||
export type MapTypeValue = (typeof MapType)[keyof typeof MapType];
|
||||
|
||||
export interface FlightStatusTranslations {
|
||||
good: string;
|
||||
caution: string;
|
||||
low: string;
|
||||
}
|
||||
|
||||
export interface OptionsTranslations {
|
||||
artillery: string;
|
||||
tank: string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface TranslationStrings {
|
||||
language: string;
|
||||
aircraft: string;
|
||||
label: string;
|
||||
point: string;
|
||||
height: string;
|
||||
edit: string;
|
||||
currentPos: string;
|
||||
return: string;
|
||||
addPoints: string;
|
||||
workArea: string;
|
||||
prohibitedArea: string;
|
||||
location: string;
|
||||
currentLocation: string;
|
||||
exportData: string;
|
||||
exportPlaneData: string;
|
||||
exportMapData: string;
|
||||
editAsJson: string;
|
||||
importFromJson: string;
|
||||
export: string;
|
||||
import: string;
|
||||
operations: string;
|
||||
rectangleColor: string;
|
||||
red: string;
|
||||
green: string;
|
||||
initialAltitude: string;
|
||||
setAltitude: string;
|
||||
setPoint: string;
|
||||
removePoint: string;
|
||||
windSpeed: string;
|
||||
windDirection: string;
|
||||
setWind: string;
|
||||
title: string;
|
||||
distanceLabel: string;
|
||||
fuelRequiredLabel: string;
|
||||
maxFuelLabel: string;
|
||||
flightStatus: FlightStatusTranslations;
|
||||
calc: string;
|
||||
error: string;
|
||||
km: string;
|
||||
metres: string;
|
||||
litres: string;
|
||||
hour: string;
|
||||
minutes: string;
|
||||
battery: string;
|
||||
titleAdd: string;
|
||||
titleEdit: string;
|
||||
description: string;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
altitude: string;
|
||||
purpose: string;
|
||||
cancel: string;
|
||||
submitAdd: string;
|
||||
submitEdit: string;
|
||||
options: OptionsTranslations;
|
||||
invalid: string;
|
||||
editm: string;
|
||||
save: string;
|
||||
}
|
||||
|
||||
export type TranslationsMap = Record<string, TranslationStrings>;
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as L from 'leaflet';
|
||||
|
||||
declare module 'leaflet' {
|
||||
function polylineDecorator(
|
||||
paths: L.Polyline | L.Polygon | L.LatLngExpression[] | L.LatLngExpression[][],
|
||||
options?: {
|
||||
patterns: Array<{
|
||||
offset?: string | number;
|
||||
endOffset?: string | number;
|
||||
repeat?: string | number;
|
||||
symbol: L.Symbol.ArrowHead | L.Symbol.Marker | L.Symbol.Dash;
|
||||
}>;
|
||||
}
|
||||
): L.FeatureGroup;
|
||||
|
||||
namespace Symbol {
|
||||
function arrowHead(options?: {
|
||||
pixelSize?: number;
|
||||
polygon?: boolean;
|
||||
pathOptions?: L.PathOptions;
|
||||
headAngle?: number;
|
||||
}): ArrowHead;
|
||||
|
||||
function marker(options?: {
|
||||
rotate?: boolean;
|
||||
markerOptions?: L.MarkerOptions;
|
||||
}): Marker;
|
||||
|
||||
function dash(options?: {
|
||||
pixelSize?: number;
|
||||
pathOptions?: L.PathOptions;
|
||||
}): Dash;
|
||||
|
||||
interface ArrowHead {}
|
||||
interface Marker {}
|
||||
interface Dash {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
declare module 'react-world-flags' {
|
||||
import type { FC, HTMLAttributes } from 'react';
|
||||
|
||||
interface FlagProps extends HTMLAttributes<HTMLImageElement> {
|
||||
code: string;
|
||||
fallback?: React.ReactNode;
|
||||
height?: string | number;
|
||||
width?: string | number;
|
||||
}
|
||||
|
||||
const Flag: FC<FlagProps>;
|
||||
export default Flag;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function newGuid(): string {
|
||||
return 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'
|
||||
.replace(/[xy]/g, function (c) {
|
||||
const r = Math.random() * 16 | 0,
|
||||
v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_SATELLITE_TILE_URL?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/setupTests.ts"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/utils.ts","./src/vite-env.d.ts","./src/constants/actionmodes.ts","./src/constants/languages.ts","./src/constants/maptypes.ts","./src/constants/purposes.ts","./src/constants/tileurls.ts","./src/constants/translations.ts","./src/flightplanning/aircraft.ts","./src/flightplanning/altitudechart.tsx","./src/flightplanning/altitudedialog.tsx","./src/flightplanning/drawcontrol.tsx","./src/flightplanning/jsoneditordialog.tsx","./src/flightplanning/languagecontext.tsx","./src/flightplanning/languageswitcher.tsx","./src/flightplanning/leftboard.tsx","./src/flightplanning/mappoint.tsx","./src/flightplanning/mapview.tsx","./src/flightplanning/minimap.tsx","./src/flightplanning/pointslist.tsx","./src/flightplanning/totaldistance.tsx","./src/flightplanning/windeffect.tsx","./src/flightplanning/flightplan.tsx","./src/icons/mapicons.tsx","./src/icons/phoneicon.tsx","./src/icons/pointicons.tsx","./src/icons/sidebaricons.tsx","./src/services/aircraftservice.ts","./src/services/weatherservice.ts","./src/services/calculatebatteryusage.ts","./src/services/calculatedistance.ts","./src/types/index.ts","./src/types/leaflet-polylinedecorator.d.ts","./src/types/react-world-flags.d.ts"],"version":"5.9.3"}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user