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;e×',S(i,"click",function(t){O(t),this.close()},this))},_updateLayout:function(){var t=this._contentNode,e=t.style,i=(e.width="",e.whiteSpace="nowrap",t.offsetWidth),i=Math.min(i,this.options.maxWidth),i=(i=Math.max(i,this.options.minWidth),e.width=i+1+"px",e.whiteSpace="",e.height="",t.offsetHeight),n=this.options.maxHeight,o="leaflet-popup-scrolled";(n&&ns.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 (
+
+ );
+}
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 (
+
+ );
+};
+
+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'
+ />
+
+
+
+
+
+
+
+
+ );
+}
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}
+
+
+
+
+
+
+
+
+
+ {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,
+ },
+});