From 2f9c4efc8e654c9651c58a660e187dffed4b2d9e Mon Sep 17 00:00:00 2001 From: Oleksandr Bezdieniezhnykh Date: Mon, 6 Apr 2026 05:51:31 +0300 Subject: [PATCH] embed mission-planner --- mission-planner | 1 - mission-planner/.env.example | 1 + mission-planner/.gitignore | 19 + mission-planner/README.md | 70 +++ mission-planner/bun.lock | 485 ++++++++++++++++++ mission-planner/env/deploy.cmd | 2 + mission-planner/env/nginx.sh | 38 ++ mission-planner/index.html | 21 + mission-planner/package.json | 38 ++ mission-planner/public/favicon.ico | Bin 0 -> 16958 bytes mission-planner/public/leaflet.js | 6 + mission-planner/public/logo128.png | Bin 0 -> 5590 bytes mission-planner/public/logo256.png | Bin 0 -> 12310 bytes mission-planner/public/manifest.json | 25 + mission-planner/public/robots.txt | 3 + mission-planner/src/App.css | 38 ++ mission-planner/src/App.tsx | 12 + mission-planner/src/config.ts | 2 + mission-planner/src/constants/actionModes.ts | 5 + mission-planner/src/constants/languages.ts | 12 + mission-planner/src/constants/maptypes.ts | 4 + mission-planner/src/constants/purposes.ts | 12 + mission-planner/src/constants/tileUrls.ts | 4 + mission-planner/src/constants/translations.ts | 138 +++++ .../src/flightPlanning/Aircraft.ts | 43 ++ .../src/flightPlanning/AltitudeChart.tsx | 53 ++ .../src/flightPlanning/AltitudeDialog.tsx | 132 +++++ .../src/flightPlanning/DrawControl.tsx | 105 ++++ .../src/flightPlanning/JsonEditorDialog.tsx | 75 +++ .../src/flightPlanning/LanguageContext.tsx | 34 ++ .../src/flightPlanning/LanguageSwitcher.css | 29 ++ .../src/flightPlanning/LanguageSwitcher.tsx | 31 ++ .../src/flightPlanning/LeftBoard.css | 233 +++++++++ .../src/flightPlanning/LeftBoard.tsx | 240 +++++++++ .../src/flightPlanning/MapPoint.tsx | 159 ++++++ .../src/flightPlanning/MapView.css | 74 +++ .../src/flightPlanning/MapView.tsx | 414 +++++++++++++++ .../src/flightPlanning/MiniMap.tsx | 30 ++ .../src/flightPlanning/Minimap.css | 17 + .../src/flightPlanning/PointsList.css | 54 ++ .../src/flightPlanning/PointsList.tsx | 189 +++++++ .../src/flightPlanning/TotalDistance.css | 16 + .../src/flightPlanning/TotalDistance.tsx | 92 ++++ .../src/flightPlanning/WindEffect.tsx | 60 +++ .../src/flightPlanning/flightPlan.css | 63 +++ .../src/flightPlanning/flightPlan.tsx | 369 +++++++++++++ mission-planner/src/icons/MapIcons.tsx | 23 + mission-planner/src/icons/PhoneIcon.tsx | 26 + mission-planner/src/icons/PointIcons.tsx | 38 ++ mission-planner/src/icons/SidebarIcons.tsx | 47 ++ mission-planner/src/index.css | 17 + mission-planner/src/logo.svg | 128 +++++ mission-planner/src/main.tsx | 17 + .../src/services/AircraftService.ts | 28 + .../src/services/WeatherService.ts | 17 + .../src/services/calculateBatteryUsage.ts | 51 ++ .../src/services/calculateDistance.ts | 62 +++ mission-planner/src/setupTests.ts | 1 + mission-planner/src/test/jsonImport.test.ts | 176 +++++++ mission-planner/src/types/index.ts | 154 ++++++ .../src/types/leaflet-polylinedecorator.d.ts | 38 ++ .../src/types/react-world-flags.d.ts | 13 + mission-planner/src/utils.ts | 8 + mission-planner/src/vite-env.d.ts | 9 + mission-planner/tsconfig.app.json | 24 + mission-planner/tsconfig.app.tsbuildinfo | 1 + mission-planner/tsconfig.json | 6 + mission-planner/vite.config.ts | 9 + 68 files changed, 4340 insertions(+), 1 deletion(-) delete mode 160000 mission-planner create mode 100644 mission-planner/.env.example create mode 100644 mission-planner/.gitignore create mode 100644 mission-planner/README.md create mode 100644 mission-planner/bun.lock create mode 100644 mission-planner/env/deploy.cmd create mode 100644 mission-planner/env/nginx.sh create mode 100644 mission-planner/index.html create mode 100644 mission-planner/package.json create mode 100644 mission-planner/public/favicon.ico create mode 100644 mission-planner/public/leaflet.js create mode 100644 mission-planner/public/logo128.png create mode 100644 mission-planner/public/logo256.png create mode 100644 mission-planner/public/manifest.json create mode 100644 mission-planner/public/robots.txt create mode 100644 mission-planner/src/App.css create mode 100644 mission-planner/src/App.tsx create mode 100644 mission-planner/src/config.ts create mode 100644 mission-planner/src/constants/actionModes.ts create mode 100644 mission-planner/src/constants/languages.ts create mode 100644 mission-planner/src/constants/maptypes.ts create mode 100644 mission-planner/src/constants/purposes.ts create mode 100644 mission-planner/src/constants/tileUrls.ts create mode 100644 mission-planner/src/constants/translations.ts create mode 100644 mission-planner/src/flightPlanning/Aircraft.ts create mode 100644 mission-planner/src/flightPlanning/AltitudeChart.tsx create mode 100644 mission-planner/src/flightPlanning/AltitudeDialog.tsx create mode 100644 mission-planner/src/flightPlanning/DrawControl.tsx create mode 100644 mission-planner/src/flightPlanning/JsonEditorDialog.tsx create mode 100644 mission-planner/src/flightPlanning/LanguageContext.tsx create mode 100644 mission-planner/src/flightPlanning/LanguageSwitcher.css create mode 100644 mission-planner/src/flightPlanning/LanguageSwitcher.tsx create mode 100644 mission-planner/src/flightPlanning/LeftBoard.css create mode 100644 mission-planner/src/flightPlanning/LeftBoard.tsx create mode 100644 mission-planner/src/flightPlanning/MapPoint.tsx create mode 100644 mission-planner/src/flightPlanning/MapView.css create mode 100644 mission-planner/src/flightPlanning/MapView.tsx create mode 100644 mission-planner/src/flightPlanning/MiniMap.tsx create mode 100644 mission-planner/src/flightPlanning/Minimap.css create mode 100644 mission-planner/src/flightPlanning/PointsList.css create mode 100644 mission-planner/src/flightPlanning/PointsList.tsx create mode 100644 mission-planner/src/flightPlanning/TotalDistance.css create mode 100644 mission-planner/src/flightPlanning/TotalDistance.tsx create mode 100644 mission-planner/src/flightPlanning/WindEffect.tsx create mode 100644 mission-planner/src/flightPlanning/flightPlan.css create mode 100644 mission-planner/src/flightPlanning/flightPlan.tsx create mode 100644 mission-planner/src/icons/MapIcons.tsx create mode 100644 mission-planner/src/icons/PhoneIcon.tsx create mode 100644 mission-planner/src/icons/PointIcons.tsx create mode 100644 mission-planner/src/icons/SidebarIcons.tsx create mode 100644 mission-planner/src/index.css create mode 100644 mission-planner/src/logo.svg create mode 100644 mission-planner/src/main.tsx create mode 100644 mission-planner/src/services/AircraftService.ts create mode 100644 mission-planner/src/services/WeatherService.ts create mode 100644 mission-planner/src/services/calculateBatteryUsage.ts create mode 100644 mission-planner/src/services/calculateDistance.ts create mode 100644 mission-planner/src/setupTests.ts create mode 100644 mission-planner/src/test/jsonImport.test.ts create mode 100644 mission-planner/src/types/index.ts create mode 100644 mission-planner/src/types/leaflet-polylinedecorator.d.ts create mode 100644 mission-planner/src/types/react-world-flags.d.ts create mode 100644 mission-planner/src/utils.ts create mode 100644 mission-planner/src/vite-env.d.ts create mode 100644 mission-planner/tsconfig.app.json create mode 100644 mission-planner/tsconfig.app.tsbuildinfo create mode 100644 mission-planner/tsconfig.json create mode 100644 mission-planner/vite.config.ts diff --git a/mission-planner b/mission-planner deleted file mode 160000 index e12f244..0000000 --- a/mission-planner +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e12f24408289bb93ae69ca402297b8af5a3892e4 diff --git a/mission-planner/.env.example b/mission-planner/.env.example new file mode 100644 index 0000000..94df430 --- /dev/null +++ b/mission-planner/.env.example @@ -0,0 +1 @@ +VITE_SATELLITE_TILE_URL=https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x} diff --git a/mission-planner/.gitignore b/mission-planner/.gitignore new file mode 100644 index 0000000..240188f --- /dev/null +++ b/mission-planner/.gitignore @@ -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 diff --git a/mission-planner/README.md b/mission-planner/README.md new file mode 100644 index 0000000..58beeac --- /dev/null +++ b/mission-planner/README.md @@ -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) diff --git a/mission-planner/bun.lock b/mission-planner/bun.lock new file mode 100644 index 0000000..47cbd16 --- /dev/null +++ b/mission-planner/bun.lock @@ -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=="], + } +} diff --git a/mission-planner/env/deploy.cmd b/mission-planner/env/deploy.cmd new file mode 100644 index 0000000..b68c7a8 --- /dev/null +++ b/mission-planner/env/deploy.cmd @@ -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 \ No newline at end of file diff --git a/mission-planner/env/nginx.sh b/mission-planner/env/nginx.sh new file mode 100644 index 0000000..0c96f48 --- /dev/null +++ b/mission-planner/env/nginx.sh @@ -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 \ No newline at end of file diff --git a/mission-planner/index.html b/mission-planner/index.html new file mode 100644 index 0000000..da280b7 --- /dev/null +++ b/mission-planner/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + Azaion Mission Planner + + + +
+ + + diff --git a/mission-planner/package.json b/mission-planner/package.json new file mode 100644 index 0000000..004da7b --- /dev/null +++ b/mission-planner/package.json @@ -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" + } +} diff --git a/mission-planner/public/favicon.ico b/mission-planner/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e4622134bf20259dfb727914f1f95cab208763a0 GIT binary patch literal 16958 zcmeHM30PIf6`scf+-s9I{Y+C=R8TQ4aYIya3o0VpQ&6_Iu_+=bi>SyhhzcSCN@NiM z1r@~=Sp|7-MDts#r8Y^}=P%*voxd(OS{9zYA<^*~AG`MA4gL45d55x;ce~hF@}b^K70tVKN^!bObjY zIhs2=EmOtB#NUXFjE*lXC^Giw$d1$x(7YHD%|= zu%RL*IFzAFt7(+6OkC&$Ib|7*blTa~Hlv(33SckTP( zCZ>G4#Y}ZnRLr;FePkLjjIw3yko5U?!a}Xm<5FZU@TM53`Wxn9y z?!{~u*@=?3ZWl+a-#8t1co*wkSm!3gM(TvS#unrY6i4A>&1q@rAEu=2w8Ng-cS~Zj zC^j}h=Qapj9mK#{B=wTNigg$oxhd+V-C}zcty9uXqo=M*8V~t`YFqO5uhY{rlEHmM zef=d-R#vu7Y=AtKO&mXk9XwPj4qCI;618$wnIBq|sj@I0q z+4EFMiOJs-6&>6J?gI`K958q*d*9EtK|(?jD=sb(uU;Kw9Ur%)9DB|W@H;+c^jIM# z(;iJeKqc9O@>=sT;{d8N7urzlrgx<0D6=u#%(N9$PyMK^rj14h%`M5I_w-H!>Pzpa z)V4pxT*@tjgV!}>XYZ@b%G_h~`4?Y`Z{GZp)z;SiWIND#1#w-RpOsd$d1=XEURruYsHgta_QZ*k+-lYwK6lQ1ju<1@Fg!d0^6U8N(`Vao zh95mz#^0DYnIAk@g7M}E{y%=)8|br!=jZL`IA8IZGptp}6}~JhJ04kHexh$+V30vf zOx!cuAR!@nFoXJ2j^&Xtz$z4u4MW5>$>s}0r!1~YIL<>ut|jE#+tfgL`9UiZhEj~D!n z_8kKG9`bpfk&(#@3yTqlErdE6(+&I*6Swlzlr)~3o5!zS{fJ+=a)sN#KIG5j$Mfge z@XU-XegSizvLjV^CYcn=7TYf2p&{Xz<0*n|i;E9(tZSZ>lq~eAI9oyaAkWV);D_J? zZYw$Fs@4892S0-9Y5t~7o&G>Q=}q+niW8R8X8|*{=CM`AVq1HTGaY|({=)YD zI42;E&F3!5mJ4;ni~K-a9zE08B%AJOJdIw+jUITWdTKjs=4`dUf57d6g2KZ&IeDWG z9V#`#`f0ZTI2#~d^uyUA9CLqtxvTsA8J1SuVw$Be2949%u$VSOm`5tLWozoS>pJ5e zF#)l`{od~M%)h`6{)kC^E?@qW=*pE3*w&=2OD%e!To}b#?8)lBznx{5|Q7eoEy%hb8vvMGI{C!VZAqAN8ZSK=;UxRUMJ@ zgg$EIm@Uhex*|T`nVhlLjQtVSBC%%`JwdWSjXn#d4x`p>mfa*yyo>Jd}F}_DU zXxNjLU0+>Y^KZnkCgfi#iX9Z|aQ1$peh*%!Xdq-u!UFXD%W|yOH-*m0L zy70K)NM7b=%8&S(srD%+1j}3{9i}2WzxGykPG960ube$w z5nWYTeFeCMJmenMtQy|}un&0f70cbVWs{uy0O@}daxS<^mW3=$$(2gZU*(dzzjI0b z^{*svehR8)v~YJt6~Uuj8c&(hMv2H1-k#x($X;Ji2h=Oqc`RLNxWZ)H-s8u$DG z$P`>EQ>gH+5%qiUJz*+z?v4INfC&2xtEf1~&Q(1$O-+Is8fXO_6OCV1}o)}zK!(FQW#^IOy?LXs8 zE>}KHRQvm?s%i%QV&s~=V1s`9^9%aM#>5%pTeFDt*1kH*Ut7k{xhI$&3ZJDeA-v*aP*@AOWio_}jh($Bx%|8=$eI~@NzOQ8qi zpML%8-RtX{;$t}^!cAY|JCQe<2=Cd1?E2%c|apT`KI>&zf|B{0SqGU zFEIaC!8`LY{AuoefWErmPHTze0AwBX2;6Oe`+#QtY3@G;hP3I}*-KxL*CxBW z`O|msp}-~JCP4rHZU!=emq|xmT_hXmHvYs5dK1P6u^wZ@<@`2ZYb5G#^7U`zztBpVTfFbabuE*2JZ{trkSPTCD6#oBT*#7_j z_n^yS(yc2l8mA4St{6`thG-@=this.min.x&&i.x<=this.max.x&&e.y>=this.min.y&&i.y<=this.max.y},intersects:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>=e.x&&n.x<=i.x,t=t.y>=e.y&&n.y<=i.y;return o&&t},overlaps:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>e.x&&n.xe.y&&n.y=n.lat&&i.lat<=o.lat&&e.lng>=n.lng&&i.lng<=o.lng},intersects:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>=e.lat&&n.lat<=i.lat,t=t.lng>=e.lng&&n.lng<=i.lng;return o&&t},overlaps:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>e.lat&&n.late.lng&&n.lng","http://www.w3.org/2000/svg"===(Wt.firstChild&&Wt.firstChild.namespaceURI));function y(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var b={ie:pt,ielt9:mt,edge:n,webkit:ft,android:gt,android23:vt,androidStock:yt,opera:xt,chrome:wt,gecko:bt,safari:Pt,phantom:Lt,opera12:o,win:Tt,ie3d:Mt,webkit3d:zt,gecko3d:_t,any3d:Ct,mobile:Zt,mobileWebkit:St,mobileWebkit3d:Et,msPointer:kt,pointer:Ot,touch:Bt,touchNative:At,mobileOpera:It,mobileGecko:Rt,retina:Nt,passiveEvents:Dt,canvas:jt,svg:Ht,vml:!Ht&&function(){try{var t=document.createElement("div"),e=(t.innerHTML='',t.firstChild);return e.style.behavior="url(#default#VML)",e&&"object"==typeof e.adj}catch(t){return!1}}(),inlineSvg:Wt,mac:0===navigator.platform.indexOf("Mac"),linux:0===navigator.platform.indexOf("Linux")},Ft=b.msPointer?"MSPointerDown":"pointerdown",Ut=b.msPointer?"MSPointerMove":"pointermove",Vt=b.msPointer?"MSPointerUp":"pointerup",qt=b.msPointer?"MSPointerCancel":"pointercancel",Gt={touchstart:Ft,touchmove:Ut,touchend:Vt,touchcancel:qt},Kt={touchstart:function(t,e){e.MSPOINTER_TYPE_TOUCH&&e.pointerType===e.MSPOINTER_TYPE_TOUCH&&O(e);ee(t,e)},touchmove:ee,touchend:ee,touchcancel:ee},Yt={},Xt=!1;function Jt(t,e,i){return"touchstart"!==e||Xt||(document.addEventListener(Ft,$t,!0),document.addEventListener(Ut,Qt,!0),document.addEventListener(Vt,te,!0),document.addEventListener(qt,te,!0),Xt=!0),Kt[e]?(i=Kt[e].bind(this,i),t.addEventListener(Gt[e],i,!1),i):(console.warn("wrong event specified:",e),u)}function $t(t){Yt[t.pointerId]=t}function Qt(t){Yt[t.pointerId]&&(Yt[t.pointerId]=t)}function te(t){delete Yt[t.pointerId]}function ee(t,e){if(e.pointerType!==(e.MSPOINTER_TYPE_MOUSE||"mouse")){for(var i in e.touches=[],Yt)e.touches.push(Yt[i]);e.changedTouches=[e],t(e)}}var ie=200;function ne(t,i){t.addEventListener("dblclick",i);var n,o=0;function e(t){var e;1!==t.detail?n=t.detail:"mouse"===t.pointerType||t.sourceCapabilities&&!t.sourceCapabilities.firesTouchEvents||((e=Ne(t)).some(function(t){return t instanceof HTMLLabelElement&&t.attributes.for})&&!e.some(function(t){return t instanceof HTMLInputElement||t instanceof HTMLSelectElement})||((e=Date.now())-o<=ie?2===++n&&i(function(t){var e,i,n={};for(i in t)e=t[i],n[i]=e&&e.bind?e.bind(t):e;return(t=n).type="dblclick",n.detail=2,n.isTrusted=!1,n._simulated=!0,n}(t)):n=1,o=e))}return t.addEventListener("click",e),{dblclick:i,simDblclick:e}}var oe,se,re,ae,he,le,ue=we(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),ce=we(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===ce||"OTransition"===ce?ce+"End":"transitionend";function _e(t){return"string"==typeof t?document.getElementById(t):t}function pe(t,e){var i=t.style[e]||t.currentStyle&&t.currentStyle[e];return"auto"===(i=i&&"auto"!==i||!document.defaultView?i:(t=document.defaultView.getComputedStyle(t,null))?t[e]:null)?null:i}function P(t,e,i){t=document.createElement(t);return t.className=e||"",i&&i.appendChild(t),t}function T(t){var e=t.parentNode;e&&e.removeChild(t)}function me(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function fe(t){var e=t.parentNode;e&&e.lastChild!==t&&e.appendChild(t)}function ge(t){var e=t.parentNode;e&&e.firstChild!==t&&e.insertBefore(t,e.firstChild)}function ve(t,e){return void 0!==t.classList?t.classList.contains(e):0<(t=xe(t)).length&&new RegExp("(^|\\s)"+e+"(\\s|$)").test(t)}function M(t,e){var i;if(void 0!==t.classList)for(var n=F(e),o=0,s=n.length;othis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,e){this._enforcingBounds=!0;var i=this.getCenter(),t=this._limitCenter(i,this._zoom,g(t));return i.equals(t)||this.panTo(t,e),this._enforcingBounds=!1,this},panInside:function(t,e){var i=m((e=e||{}).paddingTopLeft||e.padding||[0,0]),n=m(e.paddingBottomRight||e.padding||[0,0]),o=this.project(this.getCenter()),t=this.project(t),s=this.getPixelBounds(),i=_([s.min.add(i),s.max.subtract(n)]),s=i.getSize();return i.contains(t)||(this._enforcingBounds=!0,n=t.subtract(i.getCenter()),i=i.extend(t).getSize().subtract(s),o.x+=n.x<0?-i.x:i.x,o.y+=n.y<0?-i.y:i.y,this.panTo(this.unproject(o),e),this._enforcingBounds=!1),this},invalidateSize:function(t){if(!this._loaded)return this;t=l({animate:!1,pan:!0},!0===t?{animate:!0}:t);var e=this.getSize(),i=(this._sizeChanged=!0,this._lastCenter=null,this.getSize()),n=e.divideBy(2).round(),o=i.divideBy(2).round(),n=n.subtract(o);return n.x||n.y?(t.animate&&t.pan?this.panBy(n):(t.pan&&this._rawPanBy(n),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(a(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:e,newSize:i})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){var e,i;return t=this._locateOptions=l({timeout:1e4,watch:!1},t),"geolocation"in navigator?(e=a(this._handleGeolocationResponse,this),i=a(this._handleGeolocationError,this),t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t)):this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var e;this._container._leaflet_id&&(e=t.code,t=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout"),this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+t+"."}))},_handleGeolocationResponse:function(t){if(this._container._leaflet_id){var e,i,n=new v(t.coords.latitude,t.coords.longitude),o=n.toBounds(2*t.coords.accuracy),s=this._locateOptions,r=(s.setView&&(e=this.getBoundsZoom(o),this.setView(n,s.maxZoom?Math.min(e,s.maxZoom):e)),{latlng:n,bounds:o,timestamp:t.timestamp});for(i in t.coords)"number"==typeof t.coords[i]&&(r[i]=t.coords[i]);this.fire("locationfound",r)}},addHandler:function(t,e){return e&&(e=this[t]=new e(this),this._handlers.push(e),this.options[t]&&e.enable()),this},remove:function(){if(this._initEvents(!0),this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}for(var t in void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),T(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(r(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload"),this._layers)this._layers[t].remove();for(t in this._panes)T(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,e){e=P("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),e||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter.clone():this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new s(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,e,i){t=g(t),i=m(i||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),t=t.getSouthEast(),i=this.getSize().subtract(i),t=_(this.project(t,n),this.project(r,n)).getSize(),r=b.any3d?this.options.zoomSnap:1,a=i.x/t.x,i=i.y/t.y,t=e?Math.max(a,i):Math.min(a,i),n=this.getScaleZoom(t,n);return r&&(n=Math.round(n/(r/100))*(r/100),n=e?Math.ceil(n/r)*r:Math.floor(n/r)*r),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new p(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,e){t=this._getTopLeftPoint(t,e);return new f(t,t.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,e){var i=this.options.crs;return e=void 0===e?this._zoom:e,i.scale(t)/i.scale(e)},getScaleZoom:function(t,e){var i=this.options.crs,t=(e=void 0===e?this._zoom:e,i.zoom(t*i.scale(e)));return isNaN(t)?1/0:t},project:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.latLngToPoint(w(t),e)},unproject:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.pointToLatLng(m(t),e)},layerPointToLatLng:function(t){t=m(t).add(this.getPixelOrigin());return this.unproject(t)},latLngToLayerPoint:function(t){return this.project(w(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(w(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(g(t))},distance:function(t,e){return this.options.crs.distance(w(t),w(e))},containerPointToLayerPoint:function(t){return m(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return m(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){t=this.containerPointToLayerPoint(m(t));return this.layerPointToLatLng(t)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(w(t)))},mouseEventToContainerPoint:function(t){return De(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){t=this._container=_e(t);if(!t)throw new Error("Map container not found.");if(t._leaflet_id)throw new Error("Map container is already initialized.");S(t,"scroll",this._onScroll,this),this._containerId=h(t)},_initLayout:function(){var t=this._container,e=(this._fadeAnimated=this.options.fadeAnimation&&b.any3d,M(t,"leaflet-container"+(b.touch?" leaflet-touch":"")+(b.retina?" leaflet-retina":"")+(b.ielt9?" leaflet-oldie":"")+(b.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":"")),pe(t,"position"));"absolute"!==e&&"relative"!==e&&"fixed"!==e&&"sticky"!==e&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),Z(this._mapPane,new p(0,0)),this.createPane("tilePane"),this.createPane("overlayPane"),this.createPane("shadowPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(M(t.markerPane,"leaflet-zoom-hide"),M(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,e,i){Z(this._mapPane,new p(0,0));var n=!this._loaded,o=(this._loaded=!0,e=this._limitZoom(e),this.fire("viewprereset"),this._zoom!==e);this._moveStart(o,i)._move(t,e)._moveEnd(o),this.fire("viewreset"),n&&this.fire("load")},_moveStart:function(t,e){return t&&this.fire("zoomstart"),e||this.fire("movestart"),this},_move:function(t,e,i,n){void 0===e&&(e=this._zoom);var o=this._zoom!==e;return this._zoom=e,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),n?i&&i.pinch&&this.fire("zoom",i):((o||i&&i.pinch)&&this.fire("zoom",i),this.fire("move",i)),this},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return r(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){Z(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={};var e=t?k:S;e((this._targets[h(this._container)]=this)._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup",this._handleDOMEvent,this),this.options.trackResize&&e(window,"resize",this._onResize,this),b.any3d&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){r(this._resizeRequest),this._resizeRequest=x(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,e){for(var i,n=[],o="mouseout"===e||"mouseover"===e,s=t.target||t.srcElement,r=!1;s;){if((i=this._targets[h(s)])&&("click"===e||"preclick"===e)&&this._draggableMoved(i)){r=!0;break}if(i&&i.listens(e,!0)){if(o&&!We(s,t))break;if(n.push(i),o)break}if(s===this._container)break;s=s.parentNode}return n=n.length||r||o||!this.listens(e,!0)?n:[this]},_isClickDisabled:function(t){for(;t&&t!==this._container;){if(t._leaflet_disable_click)return!0;t=t.parentNode}},_handleDOMEvent:function(t){var e,i=t.target||t.srcElement;!this._loaded||i._leaflet_disable_events||"click"===t.type&&this._isClickDisabled(i)||("mousedown"===(e=t.type)&&Me(i),this._fireDOMEvent(t,e))},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,i){"click"===t.type&&((a=l({},t)).type="preclick",this._fireDOMEvent(a,a.type,i));var n=this._findEventTargets(t,e);if(i){for(var o=[],s=0;sthis.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),n=this._getCenterOffset(t)._divideBy(1-1/n);if(!0!==i.animate&&!this.getSize().contains(n))return!1;x(function(){this._moveStart(!0,i.noMoveStart||!1)._animateZoom(t,e,!0)},this)}return!0},_animateZoom:function(t,e,i,n){this._mapPane&&(i&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=e,M(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:e,noUpdate:n}),this._tempFireZoomEvent||(this._tempFireZoomEvent=this._zoom!==this._animateToZoom),this._move(this._animateToCenter,this._animateToZoom,void 0,!0),setTimeout(a(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&z(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom,void 0,!0),this._tempFireZoomEvent&&this.fire("zoom"),delete this._tempFireZoomEvent,this.fire("move"),this._moveEnd(!0))}});function Ue(t){return new B(t)}var B=et.extend({options:{position:"topright"},initialize:function(t){c(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var e=this._map;return e&&e.removeControl(this),this.options.position=t,e&&e.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var e=this._container=this.onAdd(t),i=this.getPosition(),t=t._controlCorners[i];return M(e,"leaflet-control"),-1!==i.indexOf("bottom")?t.insertBefore(e,t.firstChild):t.appendChild(e),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(T(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0",e=document.createElement("div");return e.innerHTML=t,e.firstChild},_addItem:function(t){var e,i=document.createElement("label"),n=this._map.hasLayer(t.layer),n=(t.overlay?((e=document.createElement("input")).type="checkbox",e.className="leaflet-control-layers-selector",e.defaultChecked=n):e=this._createRadioElement("leaflet-base-layers_"+h(this),n),this._layerControlInputs.push(e),e.layerId=h(t.layer),S(e,"click",this._onInputClick,this),document.createElement("span")),o=(n.innerHTML=" "+t.name,document.createElement("span"));return i.appendChild(o),o.appendChild(e),o.appendChild(n),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(i),this._checkDisabledLayers(),i},_onInputClick:function(){if(!this._preventClick){var t,e,i=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=i.length-1;0<=s;s--)t=i[s],e=this._getLayer(t.layerId).layer,t.checked?n.push(e):t.checked||o.push(e);for(s=0;se.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expandSafely:function(){var t=this._section,e=(this._preventClick=!0,S(t,"click",O),this.expand(),this);setTimeout(function(){k(t,"click",O),e._preventClick=!1})}})),qe=B.extend({options:{position:"topleft",zoomInText:'',zoomInTitle:"Zoom in",zoomOutText:'',zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=P("div",e+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,e+"-in",i,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,e+"-out",i,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,e,i,n,o){i=P("a",i,n);return i.innerHTML=t,i.href="#",i.title=e,i.setAttribute("role","button"),i.setAttribute("aria-label",e),Ie(i),S(i,"click",Re),S(i,"click",o,this),S(i,"click",this._refocusOnMap,this),i},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";z(this._zoomInButton,e),z(this._zoomOutButton,e),this._zoomInButton.setAttribute("aria-disabled","false"),this._zoomOutButton.setAttribute("aria-disabled","false"),!this._disabled&&t._zoom!==t.getMinZoom()||(M(this._zoomOutButton,e),this._zoomOutButton.setAttribute("aria-disabled","true")),!this._disabled&&t._zoom!==t.getMaxZoom()||(M(this._zoomInButton,e),this._zoomInButton.setAttribute("aria-disabled","true"))}}),Ge=(A.mergeOptions({zoomControl:!0}),A.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new qe,this.addControl(this.zoomControl))}),B.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var e="leaflet-control-scale",i=P("div",e),n=this.options;return this._addScales(n,e+"-line",i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=P("div",e,i)),t.imperial&&(this._iScale=P("div",e,i))},_update:function(){var t=this._map,e=t.getSize().y/2,t=t.distance(t.containerPointToLatLng([0,e]),t.containerPointToLatLng([this.options.maxWidth,e]));this._updateScales(t)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var e=this._getRoundNum(t);this._updateScale(this._mScale,e<1e3?e+" m":e/1e3+" km",e/t)},_updateImperial:function(t){var e,i,t=3.2808399*t;5280'+(b.inlineSvg?' ':"")+"Leaflet"},initialize:function(t){c(this,t),this._attributions={}},onAdd:function(t){for(var e in(t.attributionControl=this)._container=P("div","leaflet-control-attribution"),Ie(this._container),t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return this._update(),t.on("layeradd",this._addAttribution,this),this._container},onRemove:function(t){t.off("layeradd",this._addAttribution,this)},_addAttribution:function(t){t.layer.getAttribution&&(this.addAttribution(t.layer.getAttribution()),t.layer.once("remove",function(){this.removeAttribution(t.layer.getAttribution())},this))},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t,e=[];for(t in this._attributions)this._attributions[t]&&e.push(t);var i=[];this.options.prefix&&i.push(this.options.prefix),e.length&&i.push(e.join(", ")),this._container.innerHTML=i.join(' ')}}}),n=(A.mergeOptions({attributionControl:!0}),A.addInitHook(function(){this.options.attributionControl&&(new Ke).addTo(this)}),B.Layers=Ve,B.Zoom=qe,B.Scale=Ge,B.Attribution=Ke,Ue.layers=function(t,e,i){return new Ve(t,e,i)},Ue.zoom=function(t){return new qe(t)},Ue.scale=function(t){return new Ge(t)},Ue.attribution=function(t){return new Ke(t)},et.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}})),ft=(n.addTo=function(t,e){return t.addHandler(e,this),this},{Events:e}),Ye=b.touch?"touchstart mousedown":"mousedown",Xe=it.extend({options:{clickTolerance:3},initialize:function(t,e,i,n){c(this,n),this._element=t,this._dragStartTarget=e||t,this._preventOutline=i},enable:function(){this._enabled||(S(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Xe._dragging===this&&this.finishDrag(!0),k(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){var e,i;this._enabled&&(this._moved=!1,ve(this._element,"leaflet-zoom-anim")||(t.touches&&1!==t.touches.length?Xe._dragging===this&&this.finishDrag():Xe._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((Xe._dragging=this)._preventOutline&&Me(this._element),Le(),re(),this._moving||(this.fire("down"),i=t.touches?t.touches[0]:t,e=Ce(this._element),this._startPoint=new p(i.clientX,i.clientY),this._startPos=Pe(this._element),this._parentScale=Ze(e),i="mousedown"===t.type,S(document,i?"mousemove":"touchmove",this._onMove,this),S(document,i?"mouseup":"touchend touchcancel",this._onUp,this)))))},_onMove:function(t){var e;this._enabled&&(t.touches&&1e&&(i.push(t[n]),o=n);oe.max.x&&(i|=2),t.ye.max.y&&(i|=8),i}function ri(t,e,i,n){var o=e.x,e=e.y,s=i.x-o,r=i.y-e,a=s*s+r*r;return 0this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()t.y!=n.y>t.y&&t.x<(n.x-i.x)*(t.y-i.y)/(n.y-i.y)+i.x&&(l=!l);return l||yi.prototype._containsPoint.call(this,t,!0)}});var wi=ci.extend({initialize:function(t,e){c(this,e),this._layers={},t&&this.addData(t)},addData:function(t){var e,i,n,o=d(t)?t:t.features;if(o){for(e=0,i=o.length;es.x&&(r=i.x+a-s.x+o.x),i.x-r-n.x<(a=0)&&(r=i.x-n.x),i.y+e+o.y>s.y&&(a=i.y+e-s.y+o.y),i.y-a-n.y<0&&(a=i.y-n.y),(r||a)&&(this.options.keepInView&&(this._autopanning=!0),t.fire("autopanstart").panBy([r,a]))))},_getAnchor:function(){return m(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}})),Ii=(A.mergeOptions({closePopupOnClick:!0}),A.include({openPopup:function(t,e,i){return this._initOverlay(Bi,t,e,i).openOn(this),this},closePopup:function(t){return(t=arguments.length?t:this._popup)&&t.close(),this}}),o.include({bindPopup:function(t,e){return this._popup=this._initOverlay(Bi,this._popup,t,e),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t){return this._popup&&(this instanceof ci||(this._popup._source=this),this._popup._prepareOpen(t||this._latlng)&&this._popup.openOn(this._map)),this},closePopup:function(){return this._popup&&this._popup.close(),this},togglePopup:function(){return this._popup&&this._popup.toggle(this),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var e;this._popup&&this._map&&(Re(t),e=t.layer||t.target,this._popup._source!==e||e instanceof fi?(this._popup._source=e,this.openPopup(t.latlng)):this._map.hasLayer(this._popup)?this.closePopup():this.openPopup(t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}}),Ai.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,opacity:.9},onAdd:function(t){Ai.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&(this.addEventParent(this._source),this._source.fire("tooltipopen",{tooltip:this},!0))},onRemove:function(t){Ai.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&(this.removeEventParent(this._source),this._source.fire("tooltipclose",{tooltip:this},!0))},getEvents:function(){var t=Ai.prototype.getEvents.call(this);return this.options.permanent||(t.preclick=this.close),t},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=P("div",t),this._container.setAttribute("role","tooltip"),this._container.setAttribute("id","leaflet-tooltip-"+h(this))},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var e,i=this._map,n=this._container,o=i.latLngToContainerPoint(i.getCenter()),i=i.layerPointToContainerPoint(t),s=this.options.direction,r=n.offsetWidth,a=n.offsetHeight,h=m(this.options.offset),l=this._getAnchor(),i="top"===s?(e=r/2,a):"bottom"===s?(e=r/2,0):(e="center"===s?r/2:"right"===s?0:"left"===s?r:i.xthis.options.maxZoom||nthis.options.maxZoom||void 0!==this.options.minZoom&&oi.max.x)||!e.wrapLat&&(t.yi.max.y))return!1}return!this.options.bounds||(e=this._tileCoordsToBounds(t),g(this.options.bounds).overlaps(e))},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var e=this._map,i=this.getTileSize(),n=t.scaleBy(i),i=n.add(i);return[e.unproject(n,t.z),e.unproject(i,t.z)]},_tileCoordsToBounds:function(t){t=this._tileCoordsToNwSe(t),t=new s(t[0],t[1]);return t=this.options.noWrap?t:this._map.wrapLatLngBounds(t)},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var t=t.split(":"),e=new p(+t[0],+t[1]);return e.z=+t[2],e},_removeTile:function(t){var e=this._tiles[t];e&&(T(e.el),delete this._tiles[t],this.fire("tileunload",{tile:e.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){M(t,"leaflet-tile");var e=this.getTileSize();t.style.width=e.x+"px",t.style.height=e.y+"px",t.onselectstart=u,t.onmousemove=u,b.ielt9&&this.options.opacity<1&&C(t,this.options.opacity)},_addTile:function(t,e){var i=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),a(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&x(a(this._tileReady,this,t,null,o)),Z(o,i),this._tiles[n]={el:o,coords:t,current:!0},e.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,e,i){e&&this.fire("tileerror",{error:e,tile:i,coords:t});var n=this._tileCoordsToKey(t);(i=this._tiles[n])&&(i.loaded=+new Date,this._map._fadeAnimated?(C(i.el,0),r(this._fadeFrame),this._fadeFrame=x(this._updateOpacity,this)):(i.active=!0,this._pruneTiles()),e||(M(i.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:i.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),b.ielt9||!this._map._fadeAnimated?x(this._pruneTiles,this):setTimeout(a(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var e=new p(this._wrapX?H(t.x,this._wrapX):t.x,this._wrapY?H(t.y,this._wrapY):t.y);return e.z=t.z,e},_pxBoundsToTileRange:function(t){var e=this.getTileSize();return new f(t.min.unscaleBy(e).floor(),t.max.unscaleBy(e).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var Di=Ni.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1,referrerPolicy:!1},initialize:function(t,e){this._url=t,(e=c(this,e)).detectRetina&&b.retina&&0')}}catch(t){}return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}(),zt={_initContainer:function(){this._container=P("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(Wi.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var e=t._container=Vi("shape");M(e,"leaflet-vml-shape "+(this.options.className||"")),e.coordsize="1 1",t._path=Vi("path"),e.appendChild(t._path),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){var e=t._container;this._container.appendChild(e),t.options.interactive&&t.addInteractiveTarget(e)},_removePath:function(t){var e=t._container;T(e),t.removeInteractiveTarget(e),delete this._layers[h(t)]},_updateStyle:function(t){var e=t._stroke,i=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(e=e||(t._stroke=Vi("stroke")),o.appendChild(e),e.weight=n.weight+"px",e.color=n.color,e.opacity=n.opacity,n.dashArray?e.dashStyle=d(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):e.dashStyle="",e.endcap=n.lineCap.replace("butt","flat"),e.joinstyle=n.lineJoin):e&&(o.removeChild(e),t._stroke=null),n.fill?(i=i||(t._fill=Vi("fill")),o.appendChild(i),i.color=n.fillColor||n.color,i.opacity=n.fillOpacity):i&&(o.removeChild(i),t._fill=null)},_updateCircle:function(t){var e=t._point.round(),i=Math.round(t._radius),n=Math.round(t._radiusY||i);this._setPath(t,t._empty()?"M0 0":"AL "+e.x+","+e.y+" "+i+","+n+" 0,23592600")},_setPath:function(t,e){t._path.v=e},_bringToFront:function(t){fe(t._container)},_bringToBack:function(t){ge(t._container)}},qi=b.vml?Vi:ct,Gi=Wi.extend({_initContainer:function(){this._container=qi("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=qi("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){T(this._container),k(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_update:function(){var t,e,i;this._map._animatingZoom&&this._bounds||(Wi.prototype._update.call(this),e=(t=this._bounds).getSize(),i=this._container,this._svgSize&&this._svgSize.equals(e)||(this._svgSize=e,i.setAttribute("width",e.x),i.setAttribute("height",e.y)),Z(i,t.min),i.setAttribute("viewBox",[t.min.x,t.min.y,e.x,e.y].join(" ")),this.fire("update"))},_initPath:function(t){var e=t._path=qi("path");t.options.className&&M(e,t.options.className),t.options.interactive&&M(e,"leaflet-interactive"),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){T(t._path),t.removeInteractiveTarget(t._path),delete this._layers[h(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var e=t._path,t=t.options;e&&(t.stroke?(e.setAttribute("stroke",t.color),e.setAttribute("stroke-opacity",t.opacity),e.setAttribute("stroke-width",t.weight),e.setAttribute("stroke-linecap",t.lineCap),e.setAttribute("stroke-linejoin",t.lineJoin),t.dashArray?e.setAttribute("stroke-dasharray",t.dashArray):e.removeAttribute("stroke-dasharray"),t.dashOffset?e.setAttribute("stroke-dashoffset",t.dashOffset):e.removeAttribute("stroke-dashoffset")):e.setAttribute("stroke","none"),t.fill?(e.setAttribute("fill",t.fillColor||t.color),e.setAttribute("fill-opacity",t.fillOpacity),e.setAttribute("fill-rule",t.fillRule||"evenodd")):e.setAttribute("fill","none"))},_updatePoly:function(t,e){this._setPath(t,dt(t._parts,e))},_updateCircle:function(t){var e=t._point,i=Math.max(Math.round(t._radius),1),n="a"+i+","+(Math.max(Math.round(t._radiusY),1)||i)+" 0 1,0 ",e=t._empty()?"M0 0":"M"+(e.x-i)+","+e.y+n+2*i+",0 "+n+2*-i+",0 ";this._setPath(t,e)},_setPath:function(t,e){t._path.setAttribute("d",e)},_bringToFront:function(t){fe(t._path)},_bringToBack:function(t){ge(t._path)}});function Ki(t){return b.svg||b.vml?new Gi(t):null}b.vml&&Gi.include(zt),A.include({getRenderer:function(t){t=(t=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer)||(this._renderer=this._createRenderer());return this.hasLayer(t)||this.addLayer(t),t},_getPaneRenderer:function(t){var e;return"overlayPane"!==t&&void 0!==t&&(void 0===(e=this._paneRenderers[t])&&(e=this._createRenderer({pane:t}),this._paneRenderers[t]=e),e)},_createRenderer:function(t){return this.options.preferCanvas&&Ui(t)||Ki(t)}});var Yi=xi.extend({initialize:function(t,e){xi.prototype.initialize.call(this,this._boundsToLatLngs(t),e)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=g(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Gi.create=qi,Gi.pointsToPath=dt,wi.geometryToLayer=bi,wi.coordsToLatLng=Li,wi.coordsToLatLngs=Ti,wi.latLngToCoords=Mi,wi.latLngsToCoords=zi,wi.getFeature=Ci,wi.asFeature=Zi,A.mergeOptions({boxZoom:!0});var _t=n.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){S(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){k(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){T(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),re(),Le(),this._startPoint=this._map.mouseEventToContainerPoint(t),S(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=P("div","leaflet-zoom-box",this._container),M(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var t=new f(this._point,this._startPoint),e=t.getSize();Z(this._box,t.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(T(this._box),z(this._container,"leaflet-crosshair")),ae(),Te(),k(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){1!==t.which&&1!==t.button||(this._finish(),this._moved&&(this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(a(this._resetState,this),0),t=new s(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point)),this._map.fitBounds(t).fire("boxzoomend",{boxZoomBounds:t})))},_onKeyDown:function(t){27===t.keyCode&&(this._finish(),this._clearDeferredResetState(),this._resetState())}}),Ct=(A.addInitHook("addHandler","boxZoom",_t),A.mergeOptions({doubleClickZoom:!0}),n.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var e=this._map,i=e.getZoom(),n=e.options.zoomDelta,i=t.originalEvent.shiftKey?i-n:i+n;"center"===e.options.doubleClickZoom?e.setZoom(i):e.setZoomAround(t.containerPoint,i)}})),Zt=(A.addInitHook("addHandler","doubleClickZoom",Ct),A.mergeOptions({dragging:!0,inertia:!0,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0}),n.extend({addHooks:function(){var t;this._draggable||(t=this._map,this._draggable=new Xe(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))),M(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){z(this._map._container,"leaflet-grab"),z(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t,e=this._map;e._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity?(t=g(this._map.options.maxBounds),this._offsetLimit=_(this._map.latLngToContainerPoint(t.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(t.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))):this._offsetLimit=null,e.fire("movestart").fire("dragstart"),e.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){var e,i;this._map.options.inertia&&(e=this._lastTime=+new Date,i=this._lastPos=this._draggable._absPos||this._draggable._newPos,this._positions.push(i),this._times.push(e),this._prunePositions(e)),this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1e.max.x&&(t.x=this._viscousLimit(t.x,e.max.x)),t.y>e.max.y&&(t.y=this._viscousLimit(t.y,e.max.y)),this._draggable._newPos=this._draggable._startPos.add(t))},_onPreDragWrap:function(){var t=this._worldWidth,e=Math.round(t/2),i=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-e+i)%t+e-i,n=(n+e+i)%t-e-i,t=Math.abs(o+i)e.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 literal 0 HcmV?d00001 diff --git a/mission-planner/public/manifest.json b/mission-planner/public/manifest.json new file mode 100644 index 0000000..fe032d0 --- /dev/null +++ b/mission-planner/public/manifest.json @@ -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" +} diff --git a/mission-planner/public/robots.txt b/mission-planner/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/mission-planner/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/mission-planner/src/App.css b/mission-planner/src/App.css new file mode 100644 index 0000000..74b5e05 --- /dev/null +++ b/mission-planner/src/App.css @@ -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); + } +} diff --git a/mission-planner/src/App.tsx b/mission-planner/src/App.tsx new file mode 100644 index 0000000..0c9596f --- /dev/null +++ b/mission-planner/src/App.tsx @@ -0,0 +1,12 @@ +import './App.css'; + +function App() { + return ( +
+
+
+
+ ); +} + +export default App; diff --git a/mission-planner/src/config.ts b/mission-planner/src/config.ts new file mode 100644 index 0000000..ef77089 --- /dev/null +++ b/mission-planner/src/config.ts @@ -0,0 +1,2 @@ +export const COORDINATE_PRECISION = 8; +export const GOOGLE_GEOCODE_KEY = 'AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys'; diff --git a/mission-planner/src/constants/actionModes.ts b/mission-planner/src/constants/actionModes.ts new file mode 100644 index 0000000..4185991 --- /dev/null +++ b/mission-planner/src/constants/actionModes.ts @@ -0,0 +1,5 @@ +export const actionModes = { + points: 'points', + workArea: 'workArea', + prohibitedArea: 'prohibitedArea', +} as const; diff --git a/mission-planner/src/constants/languages.ts b/mission-planner/src/constants/languages.ts new file mode 100644 index 0000000..ac15cad --- /dev/null +++ b/mission-planner/src/constants/languages.ts @@ -0,0 +1,12 @@ +import type { Language } from '../types'; + +export const languages: Language[] = [ + { + code: 'en', + flag: 'US', + }, + { + code: 'ua', + flag: 'UA', + }, +]; diff --git a/mission-planner/src/constants/maptypes.ts b/mission-planner/src/constants/maptypes.ts new file mode 100644 index 0000000..330713c --- /dev/null +++ b/mission-planner/src/constants/maptypes.ts @@ -0,0 +1,4 @@ +export const mapTypes = { + classic: 'classic', + satellite: 'satellite', +} as const; diff --git a/mission-planner/src/constants/purposes.ts b/mission-planner/src/constants/purposes.ts new file mode 100644 index 0000000..f835c42 --- /dev/null +++ b/mission-planner/src/constants/purposes.ts @@ -0,0 +1,12 @@ +import type { Purpose } from '../types'; + +export const purposes: Purpose[] = [ + { + value: 'tank', + label: 'Tank', + }, + { + value: 'artillery', + label: 'Artillery', + }, +]; diff --git a/mission-planner/src/constants/tileUrls.ts b/mission-planner/src/constants/tileUrls.ts new file mode 100644 index 0000000..36ebc77 --- /dev/null +++ b/mission-planner/src/constants/tileUrls.ts @@ -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}', +}; diff --git a/mission-planner/src/constants/translations.ts b/mission-planner/src/constants/translations.ts new file mode 100644 index 0000000..5488396 --- /dev/null +++ b/mission-planner/src/constants/translations.ts @@ -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: 'Зберегти', + }, +}; diff --git a/mission-planner/src/flightPlanning/Aircraft.ts b/mission-planner/src/flightPlanning/Aircraft.ts new file mode 100644 index 0000000..52c0b40 --- /dev/null +++ b/mission-planner/src/flightPlanning/Aircraft.ts @@ -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; + } +} diff --git a/mission-planner/src/flightPlanning/AltitudeChart.tsx b/mission-planner/src/flightPlanning/AltitudeChart.tsx new file mode 100644 index 0000000..22fbaed --- /dev/null +++ b/mission-planner/src/flightPlanning/AltitudeChart.tsx @@ -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 ; +} diff --git a/mission-planner/src/flightPlanning/AltitudeDialog.tsx b/mission-planner/src/flightPlanning/AltitudeDialog.tsx new file mode 100644 index 0000000..eeb5f23 --- /dev/null +++ b/mission-planner/src/flightPlanning/AltitudeDialog.tsx @@ -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) => { + const { value } = event.target; + setMeta(typeof value === 'string' ? value.split(',') : value); + }; + + const handleLatitudeChange = (event: React.ChangeEvent) => { + const value = event.target.value; + if (!isNaN(Number(value))) { + const roundedValue = parseFloat(parseFloat(value).toFixed(COORDINATE_PRECISION)); + setLatitude(roundedValue); + } + }; + + const handleLongitudeChange = (event: React.ChangeEvent) => { + 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 ( + + {isEditMode ? t.titleEdit : t.titleAdd} + + + {t.description} + + + + setAltitude(Number(e.target.value))} + /> + + + {t.purpose} + + + + + + + + + ); +} diff --git a/mission-planner/src/flightPlanning/DrawControl.tsx b/mission-planner/src/flightPlanning/DrawControl.tsx new file mode 100644 index 0000000..8dfb723 --- /dev/null +++ b/mission-planner/src/flightPlanning/DrawControl.tsx @@ -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>; + controlChange: (control: L.Control.Draw) => void; +} + +export default function DrawControl({ color, rectangles, setRectangles, controlChange }: DrawControlProps) { + const map = useMap(); + const drawControlRef = useRef(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; +} diff --git a/mission-planner/src/flightPlanning/JsonEditorDialog.tsx b/mission-planner/src/flightPlanning/JsonEditorDialog.tsx new file mode 100644 index 0000000..0b5a352 --- /dev/null +++ b/mission-planner/src/flightPlanning/JsonEditorDialog.tsx @@ -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) => { + 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 ( + + {t.edit} + + + + + + + + + ); +}; + +export default JsonEditorDialog; diff --git a/mission-planner/src/flightPlanning/LanguageContext.tsx b/mission-planner/src/flightPlanning/LanguageContext.tsx new file mode 100644 index 0000000..146f641 --- /dev/null +++ b/mission-planner/src/flightPlanning/LanguageContext.tsx @@ -0,0 +1,34 @@ +import { createContext, useContext, useState, type ReactNode } from 'react'; + +interface LanguageContextType { + targetLanguage: string; + toggleLanguage: (lang: string) => void; +} + +const LanguageContext = createContext(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 ( + + {children} + + ); +}; diff --git a/mission-planner/src/flightPlanning/LanguageSwitcher.css b/mission-planner/src/flightPlanning/LanguageSwitcher.css new file mode 100644 index 0000000..6e565b7 --- /dev/null +++ b/mission-planner/src/flightPlanning/LanguageSwitcher.css @@ -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; + } +} \ No newline at end of file diff --git a/mission-planner/src/flightPlanning/LanguageSwitcher.tsx b/mission-planner/src/flightPlanning/LanguageSwitcher.tsx new file mode 100644 index 0000000..bb8ca36 --- /dev/null +++ b/mission-planner/src/flightPlanning/LanguageSwitcher.tsx @@ -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 ( +
+ {t.language} + +
+ ); +}; + +export default LanguageSwitcher; diff --git a/mission-planner/src/flightPlanning/LeftBoard.css b/mission-planner/src/flightPlanning/LeftBoard.css new file mode 100644 index 0000000..2604b5f --- /dev/null +++ b/mission-planner/src/flightPlanning/LeftBoard.css @@ -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; + } +} \ No newline at end of file diff --git a/mission-planner/src/flightPlanning/LeftBoard.tsx b/mission-planner/src/flightPlanning/LeftBoard.tsx new file mode 100644 index 0000000..9d8a475 --- /dev/null +++ b/mission-planner/src/flightPlanning/LeftBoard.tsx @@ -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>; + calculatedPointInfo: CalculatedPointInfo[]; + setCalculatedPointInfo: React.Dispatch>; + 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) => { + 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 => { + 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 ? +
+
+
+ + +
+ {t.initialAltitude} + + setInitialAltitude(Number(e.target.value))} + /> + +
+
+ + +
+ +
+
+ + + +
+ +
+ {t.location}: + + setLocationInput(e.target.value)} + onKeyDown={handleLocationSearch} + /> + +
+ {t.currentLocation}: {currentPosition?.lat.toFixed(6)}, {currentPosition?.lng.toFixed(6)} +
+
+
+ + + + + + + + + +
+ {t.operations}: +
+ +
+ + +
+
+ : +
+ + + + +
+ } + + ); +} diff --git a/mission-planner/src/flightPlanning/MapPoint.tsx b/mission-planner/src/flightPlanning/MapPoint.tsx new file mode 100644 index 0000000..164619a --- /dev/null +++ b/mission-planner/src/flightPlanning/MapPoint.tsx @@ -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>; + setPoints: React.Dispatch>; + setMovingPoint: React.Dispatch>; + 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(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) => { + const value = e.target.value; + const updatedPoints = [...points]; + updatedPoints[index] = { + ...updatedPoints[index], + meta: typeof value === 'string' ? value.split(',') : value, + }; + setPoints(updatedPoints); + }; + + return ( + handleDrag(event), + dragend: (event) => handleDragEnd(event), + move: (e) => setPointPosition(e), + moveend: () => { setMovingPoint(null); }, + }} + ref={markerRef} + > + + + {t.point} {index + 1} +
+ {t.height} + handleChangeAltitude(value as number)} + min={0} + max={3000} + step={1} + valueLabelDisplay="auto" + className='popup-slider' + /> +
+ + {t.purpose} + + + +
+
+ ); +} diff --git a/mission-planner/src/flightPlanning/MapView.css b/mission-planner/src/flightPlanning/MapView.css new file mode 100644 index 0000000..d80669c --- /dev/null +++ b/mission-planner/src/flightPlanning/MapView.css @@ -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; + } +} \ No newline at end of file diff --git a/mission-planner/src/flightPlanning/MapView.tsx b/mission-planner/src/flightPlanning/MapView.tsx new file mode 100644 index 0000000..4954d6f --- /dev/null +++ b/mission-planner/src/flightPlanning/MapView.tsx @@ -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; + updateMapCenter: (center: L.LatLng) => void; +} + +function MapEvents({ points, rectangles, rectangleColor, handlePolylineClick, containerRef, updateMapCenter }: MapEventsProps) { + const map = useMap(); + const polylineRef = useRef(null); + const arrowLayerRef = useRef(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>; + calculatedPointInfo: CalculatedPointInfo[]; + setCalculatedPointInfo: React.Dispatch>; + initialAltitude: number; + currentPosition: LatLngPosition; + updatePosition: (pos: LatLngPosition) => void; + updateMapCenter: (center: L.LatLng) => void; + handlePolylineClick: (e: L.LeafletMouseEvent) => void; + rectangles: MapRectangle[]; + setRectangles: React.Dispatch>; + 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(null); + const [openDialog, setOpenDialog] = useState(false); + const [editingPoint, setEditingPoint] = useState(null); + const [isEditMode, setIsEditMode] = useState(false); + const [mapType, setMapType] = useState(mapTypes.satellite); + const [draggablePoints, setDraggablePoints] = useState(points); + const [altitude, setAltitude] = useState(300); + const [latitude, setLatitude] = useState(0); + const [longitude, setLongitude] = useState(0); + const [meta, setMeta] = useState([purposes[0].value, purposes[1].value]); + const [drawControl, setDrawControl] = useState(null); + const [movingPoint, setMovingPoint] = useState(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 ( +
+ + + + {mapType === mapTypes.classic + ? + + : + + } + + + + {movingPoint && + + } + + {draggablePoints.map((point, index) => ( + + ))} + + {draggablePoints.length > 0 && ( + + )} + + {currentPosition && ( + { + updatePosition((event.target as L.Marker).getLatLng()); + }, + dblclick: handleMarkerDoubleClick, + }} + > + {t.currentPos} + + )} + {rectangles.map((rect, index) => + validBounds(rect.bounds) ? ( + + ) : null + )} + + + + + + + + setOpenDialog(false)} + handleAltitudeSubmit={handleAltitudeSubmit} + altitude={altitude} + setAltitude={setAltitude} + latitude={latitude} + setLatitude={setLatitude} + longitude={longitude} + setLongitude={setLongitude} + meta={meta} + setMeta={setMeta} + isEditMode={isEditMode} + /> +
+ ); +} diff --git a/mission-planner/src/flightPlanning/MiniMap.tsx b/mission-planner/src/flightPlanning/MiniMap.tsx new file mode 100644 index 0000000..5ec5324 --- /dev/null +++ b/mission-planner/src/flightPlanning/MiniMap.tsx @@ -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 ( +
+ + {mapType === mapTypes.classic + ? + : + } + + + +
+ ); +} diff --git a/mission-planner/src/flightPlanning/Minimap.css b/mission-planner/src/flightPlanning/Minimap.css new file mode 100644 index 0000000..033f904 --- /dev/null +++ b/mission-planner/src/flightPlanning/Minimap.css @@ -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; + } +} diff --git a/mission-planner/src/flightPlanning/PointsList.css b/mission-planner/src/flightPlanning/PointsList.css new file mode 100644 index 0000000..08a0179 --- /dev/null +++ b/mission-planner/src/flightPlanning/PointsList.css @@ -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; + } +} \ No newline at end of file diff --git a/mission-planner/src/flightPlanning/PointsList.tsx b/mission-planner/src/flightPlanning/PointsList.tsx new file mode 100644 index 0000000..8ddfe63 --- /dev/null +++ b/mission-planner/src/flightPlanning/PointsList.tsx @@ -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>; + setPoints: React.Dispatch>; + 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(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 => { + 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 ( + <> + + {(provided) => ( +
+ {points.map((point, index) => ( + + {(provided) => ( +
+
+ {t.point} {index < 9 ? `0${index + 1}` : index + 1}: + {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} +
+
+
+
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+ + {openDialog && currentPoint && ( + + 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} + /> + )} + + ); +} diff --git a/mission-planner/src/flightPlanning/TotalDistance.css b/mission-planner/src/flightPlanning/TotalDistance.css new file mode 100644 index 0000000..6d73900 --- /dev/null +++ b/mission-planner/src/flightPlanning/TotalDistance.css @@ -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; + } +} \ No newline at end of file diff --git a/mission-planner/src/flightPlanning/TotalDistance.tsx b/mission-planner/src/flightPlanning/TotalDistance.tsx new file mode 100644 index 0000000..4221595 --- /dev/null +++ b/mission-planner/src/flightPlanning/TotalDistance.tsx @@ -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
{t.error}
; + } + + 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 ( +
+

{totalDistanceWithReturn.toFixed(2)}{t.km} {t.calc}

+ {hours >= 1 && +

{hours}{t.hour}

+ } +

{minutes}{t.minutes}

+

{status.message}

+
+ ); +}; + +export default TotalDistance; diff --git a/mission-planner/src/flightPlanning/WindEffect.tsx b/mission-planner/src/flightPlanning/WindEffect.tsx new file mode 100644 index 0000000..08388b7 --- /dev/null +++ b/mission-planner/src/flightPlanning/WindEffect.tsx @@ -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) => { + setWindDirection(Number(event.target.value)); + }; + + const handleWindSpeedChange = (event: React.ChangeEvent) => { + setWindSpeed(Number(event.target.value)); + }; + + const applyWindParameters = () => { + onWindChange({ direction: windDirection, speed: windSpeed }); + }; + + return ( +
+

{t.setWind}

+
+ + +
+
+ + +
+ +
+ ); +}; + +export default WindEffect; diff --git a/mission-planner/src/flightPlanning/flightPlan.css b/mission-planner/src/flightPlanning/flightPlan.css new file mode 100644 index 0000000..aae3d35 --- /dev/null +++ b/mission-planner/src/flightPlanning/flightPlan.css @@ -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; + } +} \ No newline at end of file diff --git a/mission-planner/src/flightPlanning/flightPlan.tsx b/mission-planner/src/flightPlanning/flightPlan.tsx new file mode 100644 index 0000000..b2cfe3b --- /dev/null +++ b/mission-planner/src/flightPlanning/flightPlan.tsx @@ -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(null); + const [points, setPoints] = useState([]); + const [calculatedPointInfo, setCalculatedPointInfo] = useState([]); + const [currentPosition, setCurrentPosition] = useState(new LatLng(47.242, 35.024)); + const [mapCenter, setMapCenter] = useState(new LatLng(47.242, 35.024)); + const [rectangles, setRectangles] = useState(initialRectangles); + const [openDialog, setOpenDialog] = useState(false); + const [altitude, setAltitude] = useState(300); + const [latitude, setLatitude] = useState(parseFloat(new LatLng(47.242, 35.024).lat.toFixed(COORDINATE_PRECISION))); + const [longitude, setLongitude] = useState(parseFloat(new LatLng(47.242, 35.024).lng.toFixed(COORDINATE_PRECISION))); + const [meta, setMeta] = useState([purposes[0].value, purposes[1].value]); + const [rectangleColor, setRectangleColor] = useState('red'); + const [pendingPosition, setPendingPosition] = useState(null); + const [insertIndex, setInsertIndex] = useState(null); + const [jsonDialogOpen, setJsonDialogOpen] = useState(false); + const [jsonText, setJsonText] = useState(''); + const [initialAltitude, setInitialAltitude] = useState(1000); + const [currentAltitude, setCurrentAltitude] = useState(initialAltitude); + const [actionMode, setActionMode] = useState(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) => ({ + id: newGuid(), + position: { + lat: (ap.point as Record)?.lat || (ap.lat as number), + lng: (ap.point as Record)?.lon || (ap.lon as number), + }, + altitude: parseFloat(String(ap.height || 300)), + meta: (ap.action_specific as Record)?.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 ( + <> +
+ + + + + + + +
+
+ + Please switch to the landscape mode +
+ + ); +} diff --git a/mission-planner/src/icons/MapIcons.tsx b/mission-planner/src/icons/MapIcons.tsx new file mode 100644 index 0000000..9a80ae7 --- /dev/null +++ b/mission-planner/src/icons/MapIcons.tsx @@ -0,0 +1,23 @@ +interface SatelliteMapIconProps { + width?: string; + height?: string; +} + +export function SatelliteMapIcon({ + width = '100%', + height = '100%', +}: SatelliteMapIconProps) { + return ( + + + + + + + + + + + + ); +} diff --git a/mission-planner/src/icons/PhoneIcon.tsx b/mission-planner/src/icons/PhoneIcon.tsx new file mode 100644 index 0000000..f4a448a --- /dev/null +++ b/mission-planner/src/icons/PhoneIcon.tsx @@ -0,0 +1,26 @@ +interface RotatePhoneIconProps { + width?: string; + height?: string; + color?: string; +} + +export function RotatePhoneIcon({ + width = '80px', + height = '80px', + color = '#1976d2', +}: RotatePhoneIconProps) { + return ( + + + + + + + + + + + + + ); +} diff --git a/mission-planner/src/icons/PointIcons.tsx b/mission-planner/src/icons/PointIcons.tsx new file mode 100644 index 0000000..df92e06 --- /dev/null +++ b/mission-planner/src/icons/PointIcons.tsx @@ -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(); +export const pointIconRed = divIcon({ + className: 'custom-icon', + html: `
${pointIconSVG}
`, + iconSize: [30, 30], + iconAnchor: [15, 30], + popupAnchor: [0, -30], +}); + +const pointIconGreenSVG = renderToStaticMarkup(); +export const pointIconGreen = divIcon({ + className: 'custom-icon', + html: `
${pointIconGreenSVG}
`, + iconSize: [30, 30], + iconAnchor: [15, 30], + popupAnchor: [0, -30], +}); + +const pointIconBlueSVG = renderToStaticMarkup(); +export const pointIconBlue = divIcon({ + className: 'custom-icon', + html: `
${pointIconBlueSVG}
`, + iconSize: [30, 30], + iconAnchor: [15, 30], + popupAnchor: [0, -30], +}); diff --git a/mission-planner/src/icons/SidebarIcons.tsx b/mission-planner/src/icons/SidebarIcons.tsx new file mode 100644 index 0000000..f3300fb --- /dev/null +++ b/mission-planner/src/icons/SidebarIcons.tsx @@ -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 ( + + + + + + ); +} + +export function ShowSidebarIcon({ + width = '100%', + height = '100%', + color = '#1976d2', +}: IconProps) { + return ( + + + + + + ); +} + +export function DashedAreaIcon({ + className, + width, + height, + color = '#1976d2', +}: IconProps) { + return ( + + + + ); +} diff --git a/mission-planner/src/index.css b/mission-planner/src/index.css new file mode 100644 index 0000000..4e39dc0 --- /dev/null +++ b/mission-planner/src/index.css @@ -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; +} diff --git a/mission-planner/src/logo.svg b/mission-planner/src/logo.svg new file mode 100644 index 0000000..bd5ce5f --- /dev/null +++ b/mission-planner/src/logo.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mission-planner/src/main.tsx b/mission-planner/src/main.tsx new file mode 100644 index 0000000..dc0617c --- /dev/null +++ b/mission-planner/src/main.tsx @@ -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( + + + + + +); diff --git a/mission-planner/src/services/AircraftService.ts b/mission-planner/src/services/AircraftService.ts new file mode 100644 index 0000000..9b3eec8 --- /dev/null +++ b/mission-planner/src/services/AircraftService.ts @@ -0,0 +1,28 @@ +import type { AircraftParams } from '../types'; + +export const mockGetAirplaneParams = (): Promise => { + 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); + }); +}; diff --git a/mission-planner/src/services/WeatherService.ts b/mission-planner/src/services/WeatherService.ts new file mode 100644 index 0000000..19831e3 --- /dev/null +++ b/mission-planner/src/services/WeatherService.ts @@ -0,0 +1,17 @@ +import type { WeatherData } from '../types'; + +export const getWeatherData = async (lat: number, lon: number): Promise => { + 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; + } +}; diff --git a/mission-planner/src/services/calculateBatteryUsage.ts b/mission-planner/src/services/calculateBatteryUsage.ts new file mode 100644 index 0000000..ed250d7 --- /dev/null +++ b/mission-planner/src/services/calculateBatteryUsage.ts @@ -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 => { + 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; +} diff --git a/mission-planner/src/services/calculateDistance.ts b/mission-planner/src/services/calculateDistance.ts new file mode 100644 index 0000000..d0fb9cd --- /dev/null +++ b/mission-planner/src/services/calculateDistance.ts @@ -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; +}; diff --git a/mission-planner/src/setupTests.ts b/mission-planner/src/setupTests.ts new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/mission-planner/src/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/mission-planner/src/test/jsonImport.test.ts b/mission-planner/src/test/jsonImport.test.ts new file mode 100644 index 0000000..90649ba --- /dev/null +++ b/mission-planner/src/test/jsonImport.test.ts @@ -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 & { point?: { lat: number; lon: number } }).point?.lat || ap.lat, lng: (ap as Record & { point?: { lat: number; lon: number } }).point?.lon || ap.lon }, + altitude: parseFloat(String((ap as Record).height || 300)), + meta: (ap as Record & { 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).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); + }); + }); +}); diff --git a/mission-planner/src/types/index.ts b/mission-planner/src/types/index.ts new file mode 100644 index 0000000..36af601 --- /dev/null +++ b/mission-planner/src/types/index.ts @@ -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; diff --git a/mission-planner/src/types/leaflet-polylinedecorator.d.ts b/mission-planner/src/types/leaflet-polylinedecorator.d.ts new file mode 100644 index 0000000..c74dcc6 --- /dev/null +++ b/mission-planner/src/types/leaflet-polylinedecorator.d.ts @@ -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 {} + } +} diff --git a/mission-planner/src/types/react-world-flags.d.ts b/mission-planner/src/types/react-world-flags.d.ts new file mode 100644 index 0000000..fa06832 --- /dev/null +++ b/mission-planner/src/types/react-world-flags.d.ts @@ -0,0 +1,13 @@ +declare module 'react-world-flags' { + import type { FC, HTMLAttributes } from 'react'; + + interface FlagProps extends HTMLAttributes { + code: string; + fallback?: React.ReactNode; + height?: string | number; + width?: string | number; + } + + const Flag: FC; + export default Flag; +} diff --git a/mission-planner/src/utils.ts b/mission-planner/src/utils.ts new file mode 100644 index 0000000..0ef038c --- /dev/null +++ b/mission-planner/src/utils.ts @@ -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); + }); +} diff --git a/mission-planner/src/vite-env.d.ts b/mission-planner/src/vite-env.d.ts new file mode 100644 index 0000000..4ea1d3f --- /dev/null +++ b/mission-planner/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_SATELLITE_TILE_URL?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/mission-planner/tsconfig.app.json b/mission-planner/tsconfig.app.json new file mode 100644 index 0000000..b6fa3ab --- /dev/null +++ b/mission-planner/tsconfig.app.json @@ -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"] +} diff --git a/mission-planner/tsconfig.app.tsbuildinfo b/mission-planner/tsconfig.app.tsbuildinfo new file mode 100644 index 0000000..f7d5cd0 --- /dev/null +++ b/mission-planner/tsconfig.app.tsbuildinfo @@ -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"} \ No newline at end of file diff --git a/mission-planner/tsconfig.json b/mission-planner/tsconfig.json new file mode 100644 index 0000000..426eda2 --- /dev/null +++ b/mission-planner/tsconfig.json @@ -0,0 +1,6 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/mission-planner/vite.config.ts b/mission-planner/vite.config.ts new file mode 100644 index 0000000..556f519 --- /dev/null +++ b/mission-planner/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + }, +});