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 0000000..e462213 Binary files /dev/null and b/mission-planner/public/favicon.ico differ diff --git a/mission-planner/public/leaflet.js b/mission-planner/public/leaflet.js new file mode 100644 index 0000000..a3bf693 --- /dev/null +++ b/mission-planner/public/leaflet.js @@ -0,0 +1,6 @@ +/* @preserve + * Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com + * (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n=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.getMaxZoom()&&1 +
+
+ + ); +} + +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, + }, +});