mirror of
https://github.com/azaion/ui.git
synced 2026-04-22 05:26: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