From 022925d7d71f23b9dfb26137ffe6f4502d114df8 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 26 Mar 2025 14:38:31 -0600 Subject: [PATCH 1/2] ag visual farm added to the dev branch field related stuff not imported yet, and the geocoder has some bugs with the new version --- package-lock.json | 1325 +++++++++++- package.json | 10 +- src/bin/BinyardDisplay.tsx | 833 ++++++++ src/charts/MiniPie.tsx | 43 + src/common/DisplayDrawer.tsx | 151 ++ src/common/MapCard.tsx | 66 + src/common/MapGL.tsx | 22 +- src/device/DeviceActions.tsx | 10 +- src/device/DeviceViewer.tsx | 391 ++++ src/maps/MapBase.tsx | 704 ++++++ src/maps/MapMarkerSettings.tsx | 99 + src/maps/mapControllers/Geocoder.tsx | 146 ++ src/maps/mapControllers/drawController.tsx | 112 + src/maps/mapDrawers/BinDrawer.tsx | 148 ++ src/maps/mapDrawers/BinYardDrawer.tsx | 153 ++ src/maps/mapDrawers/DeviceDrawer.tsx | 153 ++ src/maps/mapDrawers/FieldDrawer.tsx | 326 +++ src/maps/mapDrawers/GrainBagDrawer.tsx | 148 ++ src/maps/mapLayers/geoMapLayer.tsx | 95 + src/maps/mapMarkers/Markers.tsx | 247 +++ .../fieldMarkers/FieldMarkerSettings.tsx | 213 ++ .../mapObjectControllers/AgMapController.tsx | 1904 +++++++++++++++++ src/maps/mapObjectTools/agMapTools.tsx | 419 ++++ src/maps/mapboxStyleOverrides.css | 284 +++ src/models/Bin.ts | 40 +- src/models/Device.ts | 30 +- src/models/FieldMarker.ts | 127 ++ src/models/GeometryMapping.ts | 16 + src/models/HomeMarker.ts | 45 + src/models/index.ts | 2 +- src/navigation/Router.tsx | 2 + src/navigation/SideNavigator.tsx | 19 + src/pages/Bin.tsx | 2 +- src/pages/FieldMap.tsx | 26 + src/products/AgIcons/FieldsIcon.tsx | 1 - src/providers/index.ts | 4 +- src/providers/pond/fieldMarkerAPI.tsx | 113 + src/providers/pond/homeMarkerAPI.tsx | 107 + src/providers/pond/pond.tsx | 10 +- src/weather/weather.tsx | 314 +++ src/weather/weatherIcon.tsx | 114 + 41 files changed, 8895 insertions(+), 79 deletions(-) create mode 100644 src/bin/BinyardDisplay.tsx create mode 100644 src/charts/MiniPie.tsx create mode 100644 src/common/DisplayDrawer.tsx create mode 100644 src/common/MapCard.tsx create mode 100644 src/device/DeviceViewer.tsx create mode 100644 src/maps/MapBase.tsx create mode 100644 src/maps/MapMarkerSettings.tsx create mode 100644 src/maps/mapControllers/Geocoder.tsx create mode 100644 src/maps/mapControllers/drawController.tsx create mode 100644 src/maps/mapDrawers/BinDrawer.tsx create mode 100644 src/maps/mapDrawers/BinYardDrawer.tsx create mode 100644 src/maps/mapDrawers/DeviceDrawer.tsx create mode 100644 src/maps/mapDrawers/FieldDrawer.tsx create mode 100644 src/maps/mapDrawers/GrainBagDrawer.tsx create mode 100644 src/maps/mapLayers/geoMapLayer.tsx create mode 100644 src/maps/mapMarkers/Markers.tsx create mode 100644 src/maps/mapMarkers/fieldMarkers/FieldMarkerSettings.tsx create mode 100644 src/maps/mapObjectControllers/AgMapController.tsx create mode 100644 src/maps/mapObjectTools/agMapTools.tsx create mode 100644 src/maps/mapboxStyleOverrides.css create mode 100644 src/models/FieldMarker.ts create mode 100644 src/models/HomeMarker.ts create mode 100644 src/pages/FieldMap.tsx create mode 100644 src/providers/pond/fieldMarkerAPI.tsx create mode 100644 src/providers/pond/homeMarkerAPI.tsx create mode 100644 src/weather/weather.tsx create mode 100644 src/weather/weatherIcon.tsx diff --git a/package-lock.json b/package-lock.json index 9ffbcf6..16398c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "@date-io/moment": "^3.2.0", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", + "@mapbox/mapbox-gl-draw": "^1.5.0", + "@mapbox/mapbox-gl-geocoder": "^5.0.3", "@mui/icons-material": "^6.1.6", "@mui/material": "^6.1.6", "@mui/styles": "^6.1.6", @@ -21,16 +23,19 @@ "@turf/area": "^7.2.0", "@turf/turf": "^7.2.0", "@types/classnames": "^2.3.0", + "@types/mapbox-gl": "^3.4.1", "axios": "^1.7.7", "dayjs": "^1.11.13", "deep-object-diff": "^1.1.9", "geojson": "^0.5.0", "json-2-csv": "^5.5.8", "lodash": "^4.17.21", + "mapbox-gl": "^3.10.0", "moment": "^2.30.1", "moment-timezone": "^0.5.46", "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", + "openweathermap-ts": "^1.2.10", "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#dev", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -46,13 +51,16 @@ "react-router-dom": "^6.28.0", "react-virtualized-auto-sizer": "^1.0.25", "recharts": "^2.15.1", - "victory": "^37.3.6" + "victory": "^37.3.6", + "weather-icons-react": "^1.2.0" }, "devDependencies": { "@babel/types": "^7.25.8", "@eslint/js": "^9.11.1", "@types/jsonexport": "^3.0.5", "@types/lodash": "^4.17.13", + "@types/mapbox__mapbox-gl-draw": "^1.4.8", + "@types/mapbox__mapbox-gl-geocoder": "^5.0.0", "@types/react": "^18.3.10", "@types/react-beautiful-dnd": "^13.1.8", "@types/react-color": "^3.0.13", @@ -2487,6 +2495,34 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/fusspot": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@mapbox/fusspot/-/fusspot-0.4.0.tgz", + "integrity": "sha512-6sys1vUlhNCqMvJOqPEPSi0jc9tg7aJ//oG1A16H3PXoIt9whtNngD7UzBHUVTH15zunR/vRvMtGNVsogm1KzA==", + "license": "BSD 2-Clause", + "dependencies": { + "is-plain-obj": "^1.1.0", + "xtend": "^4.0.1" + } + }, + "node_modules/@mapbox/geojson-area": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz", + "integrity": "sha512-bBqqFn1kIbLBfn7Yq1PzzwVkPYQr9lVUeT8Dhd0NL5n76PBuXzOcuLV7GOSbEB1ia8qWxH4COCvFpziEu/yReA==", + "license": "BSD-2-Clause", + "dependencies": { + "wgs84": "0.0.0" + } + }, + "node_modules/@mapbox/geojson-normalize": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-normalize/-/geojson-normalize-0.0.1.tgz", + "integrity": "sha512-82V7YHcle8lhgIGqEWwtXYN5cy0QM/OHq3ypGhQTbvHR57DF0vMHMjjVSQKFfVXBe/yWCBZTyOuzvK7DFFnx5Q==", + "license": "ISC", + "bin": { + "geojson-normalize": "geojson-normalize" + } + }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", @@ -2495,12 +2531,166 @@ "node": ">= 0.6" } }, + "node_modules/@mapbox/mapbox-gl-draw": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-draw/-/mapbox-gl-draw-1.5.0.tgz", + "integrity": "sha512-uchQbTa8wiv6GWWTbxW1g5b8H6VySz4t91SmduNH6jjWinPze7cjcmsPUEzhySXsYpYr2/50gRJLZz3bx7O88A==", + "license": "ISC", + "dependencies": { + "@mapbox/geojson-area": "^0.2.2", + "@mapbox/geojson-normalize": "^0.0.1", + "@mapbox/point-geometry": "^1.1.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^5.0.9" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/@mapbox/mapbox-gl-draw/node_modules/@mapbox/point-geometry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", + "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", + "license": "ISC" + }, + "node_modules/@mapbox/mapbox-gl-draw/node_modules/nanoid": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@mapbox/mapbox-gl-geocoder": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-geocoder/-/mapbox-gl-geocoder-5.0.3.tgz", + "integrity": "sha512-aeu2ZM+UKoMUGqqKy4UVVEKsIaNj2KSsiQ4p4YbNSAjZj2vcP33KSod+DPeRwhvoY+MU6KgyvdZ/1xdmH+C62g==", + "license": "ISC", + "dependencies": { + "@mapbox/mapbox-sdk": "^0.16.1", + "events": "^3.3.0", + "lodash.debounce": "^4.0.6", + "nanoid": "^3.1.31", + "subtag": "^0.5.0", + "suggestions": "^1.6.0", + "xtend": "^4.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@mapbox/mapbox-gl-supported": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-3.0.0.tgz", + "integrity": "sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg==", + "license": "BSD-3-Clause" + }, + "node_modules/@mapbox/mapbox-sdk": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-sdk/-/mapbox-sdk-0.16.1.tgz", + "integrity": "sha512-dyZrmg+UL/Gp5mGG3CDbcwGSUMYYrfbd9hdp0rcA3pHSf3A9eYoXO9nFiIk6SzBwBVMzHENJz84ZHdqM0MDncQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@mapbox/fusspot": "^0.4.0", + "@mapbox/parse-mapbox-token": "^0.2.0", + "@mapbox/polyline": "^1.0.0", + "eventemitter3": "^3.1.0", + "form-data": "^3.0.0", + "got": "^11.8.5", + "is-plain-obj": "^1.1.0", + "xtend": "^4.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@mapbox/mapbox-sdk/node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", + "license": "MIT" + }, + "node_modules/@mapbox/mapbox-sdk/node_modules/form-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.3.tgz", + "integrity": "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/parse-mapbox-token": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/parse-mapbox-token/-/parse-mapbox-token-0.2.0.tgz", + "integrity": "sha512-BjeuG4sodYaoTygwXIuAWlZV6zUv4ZriYAQhXikzx+7DChycMUQ9g85E79Htat+AsBg+nStFALehlOhClYm5cQ==", + "license": "BSD-2-Clause", + "dependencies": { + "base-64": "^0.1.0" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", + "license": "ISC" + }, + "node_modules/@mapbox/polyline": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-1.2.1.tgz", + "integrity": "sha512-sn0V18O3OzW4RCcPoUIVDWvEGQaBNH9a0y5lgqrf5hUycyw1CzrhEoxV5irzrMNXKCkw1xRsZXcaVbsVZggHXA==", + "dependencies": { + "meow": "^9.0.0" + }, + "bin": { + "polyline": "bin/polyline.bin.js" + } + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz", + "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==", + "license": "BSD-2-Clause" + }, "node_modules/@mapbox/unitbezier": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", "license": "BSD-2-Clause" }, + "node_modules/@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@maplibre/maplibre-gl-style-spec": { "version": "19.3.3", "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.3.tgz", @@ -3447,6 +3637,18 @@ "node": ">=14.18" } }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -3470,6 +3672,18 @@ "sourcemap-codec": "^1.4.8" } }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@turf/along": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@turf/along/-/along-7.2.0.tgz", @@ -5544,6 +5758,18 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/classnames": { "version": "2.3.0", "integrity": "sha512-3GsbOoDYteFShlrBTKzI2Eii4vPg/jAf7LXRIn0WQePKlmhpkV0KoTMuawA7gZJkrbPrZGwv9IEAfIWaOaQK8w==", @@ -5632,6 +5858,15 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, + "node_modules/@types/geojson-vt": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", + "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", @@ -5642,6 +5877,12 @@ "hoist-non-react-statics": "^3.3.0" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -5658,6 +5899,15 @@ "@types/node": "*" } }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.17.13", "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", @@ -5668,16 +5918,60 @@ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" }, + "node_modules/@types/mapbox__mapbox-gl-draw": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/mapbox__mapbox-gl-draw/-/mapbox__mapbox-gl-draw-1.4.8.tgz", + "integrity": "sha512-700zPikQXfFMB2vtkJdXSROiqS5F19guf6QdYqvlgCdaMxGmdlLITRq6/zFpzfVQDrgpWex5M8vLtbwjZfup8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*", + "mapbox-gl": "*" + } + }, + "node_modules/@types/mapbox__mapbox-gl-geocoder": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/mapbox__mapbox-gl-geocoder/-/mapbox__mapbox-gl-geocoder-5.0.0.tgz", + "integrity": "sha512-eGBWdFiP2QgmwndPyhwK6eBeOfyB8vRscp2C6Acqasx5dH8FvTo/VgXWCrCKFR3zkWek/H4w4/CwmBFOs7OLBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*", + "@types/mapbox-gl": "*" + } + }, + "node_modules/@types/mapbox__point-geometry": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", + "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", + "license": "MIT" + }, + "node_modules/@types/mapbox__vector-tile": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", + "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" + } + }, "node_modules/@types/mapbox-gl": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz", "integrity": "sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg==", - "dev": true, "license": "MIT", "dependencies": { "@types/geojson": "*" } }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.9.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz", @@ -5686,11 +5980,23 @@ "undici-types": "~6.19.8" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, + "node_modules/@types/pbf": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", + "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", @@ -5795,6 +6101,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -6317,6 +6641,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -6457,6 +6790,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, "node_modules/bignumber.js": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", @@ -6557,6 +6895,33 @@ "node": ">=8" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -6580,7 +6945,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6615,6 +6979,41 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "license": "MIT", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001701", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001701.tgz", @@ -6670,6 +7069,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cheap-ruler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz", + "integrity": "sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==", + "license": "ISC" + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -6685,6 +7090,18 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -6850,6 +7267,12 @@ "is-in-browser": "^1.0.2" } }, + "node_modules/csscolorparser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", + "license": "MIT" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -7074,12 +7497,73 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deeks": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/deeks/-/deeks-3.1.0.tgz", @@ -7126,6 +7610,15 @@ "node": ">=0.10.0" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -7207,7 +7700,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -7247,6 +7739,15 @@ "dev": true, "license": "ISC" }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -7325,7 +7826,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7335,7 +7835,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7352,7 +7851,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7365,7 +7863,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7638,6 +8135,15 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/expect-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", @@ -7967,6 +8473,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuzzy": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", + "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8018,11 +8532,16 @@ "quickselect": "^1.0.1" } }, + "node_modules/geojson-vt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", + "license": "ISC" + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -8054,7 +8573,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -8064,6 +8582,21 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -8095,7 +8628,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==", - "dev": true, "license": "MIT" }, "node_modules/glob": { @@ -8178,7 +8710,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8187,6 +8718,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -8200,6 +8756,21 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/grid-index": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", + "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", + "license": "ISC" + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -8256,7 +8827,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8269,7 +8839,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -8305,6 +8874,55 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/hyphenate-style-name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", @@ -8317,6 +8935,26 @@ "dev": true, "license": "ISC" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -8350,6 +8988,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -8663,6 +9310,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -8928,8 +9584,7 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -9093,15 +9748,29 @@ "node": ">= 12" } }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "dependencies": { "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -9216,6 +9885,15 @@ "dev": true, "license": "MIT" }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -9235,6 +9913,76 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mapbox-gl": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.10.0.tgz", + "integrity": "sha512-YnQxjlthuv/tidcxGYU2C8nRDVXMlAHa3qFhuOJeX4AfRP72OMRBf9ApL+M+k5VWcAXi2fcNOUVgphknjLumjA==", + "license": "SEE LICENSE IN LICENSE.txt", + "workspaces": [ + "src/style-spec", + "test/build/typings" + ], + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^3.0.0", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@types/geojson": "^7946.0.16", + "@types/geojson-vt": "^3.2.5", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", + "cheap-ruler": "^4.0.0", + "csscolorparser": "~1.0.3", + "earcut": "^3.0.0", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.3", + "grid-index": "^1.1.0", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^2.0.0", + "quickselect": "^3.0.0", + "serialize-to-js": "^3.1.2", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0", + "vt-pbf": "^3.1.3" + } + }, + "node_modules/mapbox-gl/node_modules/earcut": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz", + "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==", + "license": "ISC" + }, + "node_modules/mapbox-gl/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "node_modules/mapbox-gl/node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/marchingsquares": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/marchingsquares/-/marchingsquares-1.3.3.tgz", @@ -9251,7 +9999,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9263,6 +10010,44 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, + "node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -9304,6 +10089,24 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -9325,6 +10128,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -9370,11 +10187,16 @@ } } }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, "funding": [ { "type": "github", @@ -9395,6 +10217,48 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -9402,6 +10266,45 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/notistack": { "version": "3.0.1", "integrity": "sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==", @@ -9486,12 +10389,20 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" } }, + "node_modules/openweathermap-ts": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/openweathermap-ts/-/openweathermap-ts-1.2.10.tgz", + "integrity": "sha512-Zckv2aXN8ENSeAeroces2jJciLWb6aLNXEmvG6pmF+BcIMw2kwRo6++/AKUNoU5suOp47UWA6lllDV0TNm//OA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -9527,6 +10438,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9557,6 +10477,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9589,7 +10518,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -9643,6 +10571,19 @@ "node": ">= 14.16" } }, + "node_modules/pbf": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9741,6 +10682,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/potpack": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", + "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==", + "license": "ISC" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9810,11 +10757,27 @@ "pbts": "bin/pbts" } }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9844,6 +10807,18 @@ } ] }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/quickselect": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", @@ -10251,6 +11226,135 @@ "lodash": "^4.0.1" } }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "license": "ISC" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, "node_modules/recharts": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", @@ -10305,6 +11409,19 @@ "d3-timer": "^3.0.1" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", @@ -10457,6 +11574,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -10465,6 +11588,27 @@ "node": ">=4" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -10673,6 +11817,15 @@ "randombytes": "^2.1.0" } }, + "node_modules/serialize-to-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/serialize-to-js/-/serialize-to-js-3.1.2.tgz", + "integrity": "sha512-owllqNuDDEimQat7EPG0tH7JjO090xKNzUtYz6X+Sk2BXDnOCilDdNLwjWeFywG9xkJul1ULvtUQa9O4pUaY0w==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10944,6 +12097,38 @@ "dev": true, "license": "MIT" }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "license": "CC0-1.0" + }, "node_modules/splaytree-ts": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/splaytree-ts/-/splaytree-ts-1.0.2.tgz", @@ -11112,6 +12297,18 @@ "node": ">=10" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -11129,6 +12326,31 @@ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, + "node_modules/subtag": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/subtag/-/subtag-0.5.0.tgz", + "integrity": "sha512-CaIBcTSb/nyk4xiiSOtZYz1B+F12ZxW8NEp54CdT+84vmh/h4sUnHGC6+KQXUfED8u22PQjCYWfZny8d2ELXwg==", + "license": "ISC" + }, + "node_modules/suggestions": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/suggestions/-/suggestions-1.7.1.tgz", + "integrity": "sha512-gl5YPAhPYl07JZ5obiD9nTZsg4SyZswAQU/NNtnYiSnFkI3+ZHuXAiEsYm7AaZ71E0LXSFaGVaulGSWN3Gd71A==", + "license": "ISC", + "dependencies": { + "fuzzy": "^0.1.1", + "xtend": "^4.0.0" + } + }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -11385,6 +12607,15 @@ "is-lite": "^1.2.1" } }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ts-api-utils": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", @@ -11740,6 +12971,16 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/victory": { "version": "37.3.6", "resolved": "https://registry.npmjs.org/victory/-/victory-37.3.6.tgz", @@ -12480,6 +13721,23 @@ } } }, + "node_modules/vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, + "node_modules/weather-icons-react": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/weather-icons-react/-/weather-icons-react-1.2.0.tgz", + "integrity": "sha512-VlLyZb9rb0Ir/NvC6T3YOcnftzUMg8hDYMb9xf1pzzM5z5wKogL6q1wZeqE5e+oHSw0CVcTFtYCbSa1ZEwnDZQ==", + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -12487,6 +13745,12 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/wgs84": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/wgs84/-/wgs84-0.0.0.tgz", + "integrity": "sha512-ANHlY4Rb5kHw40D0NJ6moaVfOCMrp9Gpd1R/AIQYg2ko4/jzcJ+TVXYYF6kXJqQwITvEZP4yEthjM7U6rYlljQ==", + "license": "BSD-2-Clause" + }, "node_modules/whatwg-url": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", @@ -13002,9 +14266,17 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -13026,6 +14298,15 @@ "node": ">= 14" } }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 872e9c4..1bf2a15 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "@date-io/moment": "^3.2.0", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", + "@mapbox/mapbox-gl-draw": "^1.5.0", + "@mapbox/mapbox-gl-geocoder": "^5.0.3", "@mui/icons-material": "^6.1.6", "@mui/material": "^6.1.6", "@mui/styles": "^6.1.6", @@ -31,16 +33,19 @@ "@turf/area": "^7.2.0", "@turf/turf": "^7.2.0", "@types/classnames": "^2.3.0", + "@types/mapbox-gl": "^3.4.1", "axios": "^1.7.7", "dayjs": "^1.11.13", "deep-object-diff": "^1.1.9", "geojson": "^0.5.0", "json-2-csv": "^5.5.8", "lodash": "^4.17.21", + "mapbox-gl": "^3.10.0", "moment": "^2.30.1", "moment-timezone": "^0.5.46", "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", + "openweathermap-ts": "^1.2.10", "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#dev", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -56,13 +61,16 @@ "react-router-dom": "^6.28.0", "react-virtualized-auto-sizer": "^1.0.25", "recharts": "^2.15.1", - "victory": "^37.3.6" + "victory": "^37.3.6", + "weather-icons-react": "^1.2.0" }, "devDependencies": { "@babel/types": "^7.25.8", "@eslint/js": "^9.11.1", "@types/jsonexport": "^3.0.5", "@types/lodash": "^4.17.13", + "@types/mapbox__mapbox-gl-draw": "^1.4.8", + "@types/mapbox__mapbox-gl-geocoder": "^5.0.0", "@types/react": "^18.3.10", "@types/react-beautiful-dnd": "^13.1.8", "@types/react-color": "^3.0.13", diff --git a/src/bin/BinyardDisplay.tsx b/src/bin/BinyardDisplay.tsx new file mode 100644 index 0000000..6e055b0 --- /dev/null +++ b/src/bin/BinyardDisplay.tsx @@ -0,0 +1,833 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Button, + Checkbox, + Chip, + darken, + Divider, + FormControl, + FormControlLabel, + Grid2 as Grid, + ImageList as GridList, + ImageListItem as GridListTile, + InputLabel, + Menu, + MenuItem, + Select, + Skeleton, + Theme, + Typography, + useTheme +} from "@mui/material"; +import { green, yellow } from "@mui/material/colors"; +import { makeStyles } from "@mui/styles"; +import { ArrowBackIos, ArrowForwardIos, ExpandMore, MoreVert } from "@mui/icons-material"; +//import { Skeleton } from "@material-ui/lab"; +import BinInventoryChart, { GrainAmount } from "charts/BinInventoryChart"; +import BinUtilizationChart from "charts/BinUtilizationChart"; +//import QrCodeGenerator, { QrCodeKey } from "common/QrCodeGenerator"; +import GrainDescriber, { grainName } from "grain/GrainDescriber"; +import { useMobile, useWidth } from "hooks"; +import { Bin } from "models"; +import { pond } from "protobuf-ts/pond"; +import { useBinAPI, useGlobalState } from "providers"; +import React, { useEffect, useState, useCallback, SetStateAction } from "react"; +import { getGrainUnit, stringToMaterialColour } from "utils"; +//import BinsFansStatusTable from "./BinFansStatusTable"; +import BinsList from "./BinsList"; + +interface Props { + yardKey: string; + insert?: boolean; +} + +const useStyles = makeStyles((theme: Theme) => ({ + "@keyframes pulsate": { + to: { + boxShadow: "0 0 0 16px" + yellow["500"] + "00" + } + }, + green: { + color: green["500"], + "&:hover": { + color: green["600"] + } + }, + gridList: { + width: "100%", + flexWrap: "nowrap", + transform: "translateZ(0)" + }, + pulse: { + boxShadow: "0 0 0 0 " + yellow["500"] + "75", + animation: "$pulsate 1.75s infinite cubic-bezier(0.66, 0.33, 0, 1)" + }, + icon: { + padding: 6, + width: 36, + height: 36, + borderRadius: "18px", + background: "rgba(0,0,0,0)", + "&:hover": { + background: + "radial-gradient(closest-side, rgba(150, 150, 150, 0.5) 50%, rgba(150, 150, 150, 0.5))" + } + }, + accordion: { + background: darken(theme.palette.background.paper, 0.4) + } +})); + +export default function BinyardDisplay(props: Props) { + const { yardKey, insert } = props; + const classes = useStyles(); + const binAPI = useBinAPI(); + const theme = useTheme(); + const width = useWidth(); + const isMobile = useMobile(); + const [carouselIndex, setCarouselIndex] = useState(0); + const [binMenuAnchorEl, setBinMenuAnchorEl] = useState(null); + const [{ as }] = useGlobalState(); + const maxBins = 40; + const [binsLoading, setBinsLoading] = useState(false); + const [expandTotal, setExpandTotal] = useState(false); + const [displayedInventory, setDisplayedInventory] = useState([]); + const [displayedFertilizer, setDisplayedFertilizer] = useState([]); + const [expandUtilization, setExpandUtilization] = useState(false); + const [expandFanTable, setExpandFanTable] = useState(false); + const [displayGrain, setDisplayGrain] = useState(true); + const [displayFert, setDisplayFert] = useState(false); + const [displayEmpty, setDisplayEmpty] = useState(false); + const [yardBins, setYardBins] = useState([]); + const [grainBins, setGrainBins] = useState([]); + const [fertBins, setFertBins] = useState([]); + const [emptyBins, setEmptyBins] = useState([]); + const [binTotal, setBinTotal] = useState(0); + const [binMetrics, setBinMetrics] = useState(); + const [contentFilter, setContentFilter] = useState(""); + const [totalFanControllers, setTotalFanControllers] = useState(0); + const [totalFanControllersOn, setTotalFanControllersOn] = useState(0); + const [openQrGenerator, setOpenQrGenerator] = useState(false); + const [order, setOrder] = useState<"asc" | "desc">("asc"); + const [orderBy, setOrderBy] = useState("name"); + + //const [qrKeyList, setQrKeyList] = useState([]); + + const [scrollTranslations, setScrollTranslations] = useState({ + bins: 0, + empty: 0, + fertilizer: 0 + }); + + const loadBins = useCallback(() => { + setBinsLoading(true); + //load the bins based on the yard key + binAPI + .listBinsAndData(maxBins, 0, "asc", "name", undefined, as, undefined, [yardKey], ["binyard"]) + .then(resp => { + let yardBins: Bin[] = []; + let grainBins: Bin[] = []; + let fertBins: Bin[] = []; + let emptyBins: Bin[] = []; + //let qr: QrCodeKey[] = []; + //set the list of bins into the yard bins + resp.data.bins.forEach(b => { + let bin = Bin.any(b); + yardBins.push(bin); + if (bin.empty()) { + emptyBins.push(bin); + } else if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) { + fertBins.push(bin); + } else { + grainBins.push(bin); + } + //build qr keys here + // qr.push({ + // key: bin.key(), + // name: bin.name() + // }); + }); + setYardBins(yardBins); + setBinTotal(resp.data.total); + //setQrKeyList(qr); + setGrainBins(grainBins); + if (fertBins.length > 0) { + setDisplayFert(true); + } + if (emptyBins.length > 0) { + setDisplayEmpty(true); + } + setFertBins(fertBins); + setEmptyBins(emptyBins); + setBinsLoading(false); + + let metrics = pond.BinMetrics.fromObject(resp.data.metrics ?? {}); + let inventory: GrainAmount[] = []; + let fertInventory: GrainAmount[] = []; + if (metrics) { + metrics.grainInventory.forEach(grain => { + inventory.push({ + grain: grain.grainType, + bushelAmount: grain.bushelAmount, + grainName: grainName(grain.grainType) + }); + }); + metrics.customInventory.forEach(invObject => { + if (invObject.storageType === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN) { + inventory.push({ + grain: pond.Grain.GRAIN_CUSTOM, + bushelAmount: invObject.amount, + grainName: invObject.name + }); + } else if (invObject.storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER) { + fertInventory.push({ + grain: pond.Grain.GRAIN_CUSTOM, + bushelAmount: invObject.amount * 35.239, + grainName: invObject.name + }); + } + }); + } + setBinMetrics(metrics); + setDisplayedInventory(inventory); + setDisplayedFertilizer(fertInventory); + }); + }, [as, binAPI, yardKey]); + + const loadMoreBins = (filter: string, total?: number, offset?: number) => { + setBinsLoading(true); + binAPI + .listBins( + total ?? maxBins, + offset ?? 0, + order, + orderBy, + filter, + as, + undefined, + undefined, + [yardKey], + ["binyard"] + ) + .then(resp => { + let bins = yardBins; + //if the offset was 0 or undefined we started from the beginning so replace the yard bins with the new ones + if (!offset || offset === 0) { + bins = resp.data.bins.map(b => Bin.any(b)); + //else append the new ones on to the end of the existing yard bins + } else { + bins = bins.concat(resp.data.bins.map(b => Bin.any(b))); + } + //loop through the new list of bins setting the bins into the correct sections + let grainBins: Bin[] = []; + let fertBins: Bin[] = []; + let emptyBins: Bin[] = []; + //let qr: QrCodeKey[] = []; + //set the list of bins into the yard bins + resp.data.bins.forEach(b => { + let bin = Bin.any(b); + yardBins.push(bin); + if (bin.empty()) { + emptyBins.push(bin); + } else if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) { + fertBins.push(bin); + } else { + grainBins.push(bin); + } + //build qr keys here + // qr.push({ + // key: bin.key(), + // name: bin.name() + // }); + }); + setYardBins(bins); + setBinTotal(resp.data.total); + //setQrKeyList(qr); + setGrainBins(grainBins); + if (fertBins.length > 0) { + setDisplayFert(true); + } + if (emptyBins.length > 0) { + setDisplayEmpty(true); + } + setFertBins(fertBins); + setEmptyBins(emptyBins); + setBinsLoading(false); + }); + }; + + useEffect(() => { + loadBins(); + }, [yardKey, loadBins]); + + useEffect(() => { + let ebt = sessionStorage.getItem("expandBinTotal"); + if (ebt === "true") { + setExpandTotal(true); + } else { + setExpandTotal(false); + } + + let ebu = sessionStorage.getItem("expandBinUtilization"); + if (ebu === "true") { + setExpandUtilization(true); + } else { + setExpandUtilization(false); + } + + let fanTable = sessionStorage.getItem("expandFanTable"); + if (fanTable === "true") { + setExpandFanTable(true); + } else { + setExpandFanTable(false); + } + }, []); + + const duplicateBin = (bin: Bin) => { + binAPI.addBin(bin.settings).then(resp => { + loadBins(); + }); + }; + + const binsContent = () => { + if (!binsLoading && binTotal === 0) { + return ( + + No bins found + + Create a bin and it will appear here + + + ); + } + + return ( + + + {displayGrain && ( + + + + + + { + // //only triggered in the scroll view so this will never trigger in grid view + // if (yardBins.length < binTotal) { + // loadMoreBins(contentFilter); + // setScrollTranslations({ ...scrollTranslations, bins: newTranslation }); + // } + // }} + /> + + + )} + {displayFert && ( + + + + + + { + // //only triggered in the scroll view so this will never trigger in grid view + // if (yardBins.length < binTotal) { + // loadMoreBins(contentFilter); + // setScrollTranslations({ ...scrollTranslations, fertilizer: newTranslation }); + // } + // }} + /> + + + )} + {displayEmpty && ( + + + + + + { + // //only triggered in the scroll view so this will never trigger in grid view + // if (yardBins.length < binTotal) { + // loadMoreBins(contentFilter); + // setScrollTranslations({ ...scrollTranslations, empty: newTranslation }); + // } + // }} + /> + + + )} + + + ); + }; + + const binsHeader = () => { + return ( + { + setBinMenuAnchorEl(null); + }} + disableAutoFocusItem> + + + Order + + + + + + Order By + + + + + + { + setDisplayGrain(checked); + }} + /> + } + label={Grain} + /> + + + { + setDisplayFert(checked); + }} + /> + } + label={Fertilizer} + /> + + + { + setDisplayEmpty(checked); + }} + /> + } + label={Empty Bins} + /> + + {/* { + setOpenQrGenerator(true); + }}> + Generate QR Codes + */} + + ); + }; + + const binsByGrainType = () => { + return ( + + + + + + Bins - ({yardBins.length} of {binTotal}) + + {contentFilter && ( + + { + setContentFilter(""); + loadMoreBins(""); + }} + label={contentFilter} + /> + + )} + {yardBins.length < binTotal && ( + + )} + + + + { + let target = event.currentTarget; + setBinMenuAnchorEl(target); + }} + /> + + + {binsLoading ? : binsContent()} + {binsHeader()} + + ); + }; + + const desktopInventory = () => { + return ( + + + + { + let filter = ""; + if (grain !== pond.Grain.GRAIN_NONE && grain !== pond.Grain.GRAIN_CUSTOM) { + filter = pond.Grain[grain]; + } else { + filter = grainName; + } + setContentFilter(filter); + loadMoreBins(filter); + }} + //activeGrain={grainFilter} + /> + + + { + let filter = ""; + if (grain !== pond.Grain.GRAIN_NONE && grain !== pond.Grain.GRAIN_CUSTOM) { + filter = pond.Grain[grain]; + } else { + filter = grainName; + } + setContentFilter(filter); + loadMoreBins(filter); + }} + customUnit={"L"} + //activeGrain={grainFilter} + /> + + + + ); + }; + + const mobileViewCarousel = () => { + let length = 2; + let charts: JSX.Element[] = [ + { + let filter = ""; + if (grain !== pond.Grain.GRAIN_NONE && grain !== pond.Grain.GRAIN_CUSTOM) { + filter = pond.Grain[grain]; + } else { + filter = grainName; + } + setContentFilter(filter); + loadMoreBins(filter); + }} + //activeGrain={grainFilter} + />, + { + let filter = ""; + if (grain !== pond.Grain.GRAIN_NONE && grain !== pond.Grain.GRAIN_CUSTOM) { + filter = pond.Grain[grain]; + } else { + filter = grainName; + } + setContentFilter(filter); + loadMoreBins(filter); + }} + customUnit={"L"} + //activeGrain={grainFilter} + /> + ]; + return ( + + + + + + + + {charts[carouselIndex]} + + + + + + + + ); + }; + + const totalInventory = () => { + return ( + { + setExpandTotal(expanded); + sessionStorage.setItem("expandBinTotal", expanded.toString()); + }}> + }> + + Total Inventory + + + + {isMobile || insert ? mobileViewCarousel() : desktopInventory()} + + + ); + }; + + const binUtilizationList = () => { + const hasInventory = binMetrics && binMetrics.grainInventory.length > 0; + const useWeight = getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT; + return ( + { + setExpandUtilization(expanded); + sessionStorage.setItem("expandBinUtilization", expanded.toString()); + }}> + }> + + Bin Utilization + + + + {hasInventory ? ( + + + Click a chart to filter bins by grain + + + {binMetrics && + binMetrics.grainInventory.map((inv, key) => ( + + { + setContentFilter(pond.Grain[inv.grainType]); + loadMoreBins(pond.Grain[inv.grainType]); + }} + grainActive={contentFilter === pond.Grain[inv.grainType]} + /> + + ))} + {binMetrics && + binMetrics.customInventory.map((inv, key) => { + //default to bushel values + let amount = inv.amount; + let cap = inv.capacity; + let unit = " bu"; + if (inv.storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER) { + amount = amount * 35.239; + cap = cap * 35.239; + unit = " L"; + } + if (useWeight && inv.bushelsPerTonne > 1) { + amount = amount / inv.bushelsPerTonne; + cap = cap / inv.bushelsPerTonne; + unit = " mT"; + } + + return ( + + { + setContentFilter(inv.name); + loadMoreBins(inv.name); + }} + grainActive={contentFilter === inv.name} + customLabel={inv.name} + customColour={stringToMaterialColour(inv.name)} + /> + + ); + })} + + + ) : ( + + No active bins + + )} + + + ); + }; + + const binFanTable = () => { + return ( + { + setExpandFanTable(expanded); + sessionStorage.setItem("expandFanTable", expanded.toString()); + }}> + }> + + Fan Status{" "} + {totalFanControllers > 0 && !isMobile + ? " - " + totalFanControllersOn + " of " + totalFanControllers + " controllers on" + : ""} + + + + {/* { + setTotalFanControllers(total); + setTotalFanControllersOn(on); + }} + /> */} + + + ); + }; + + return ( + + + + + {binsByGrainType()} + + + {binsLoading ? : totalInventory()} + + + + + + {binsLoading ? : binUtilizationList()} + + + + + + {binsLoading ? : binFanTable()} + + + + + + + {/* { + setOpenQrGenerator(false); + }} + keyList={qrKeyList} + requiredUrlAffix={"bins"} + /> */} + + ); +} diff --git a/src/charts/MiniPie.tsx b/src/charts/MiniPie.tsx new file mode 100644 index 0000000..3208156 --- /dev/null +++ b/src/charts/MiniPie.tsx @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react"; +import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts"; + +interface Props { + max: number; + current: number; + colour: string; + size: number; +} + +interface Data { + key: string; + value: number; +} + +export default function MiniPie(props: Props) { + const [data, setData] = useState([]); + + useEffect(() => { + let data: Data[] = []; + data.push({ key: "current", value: props.current }); + setData(data); + }, [props]); + + return ( + + + + {data.map((entry: Data, index: number) => ( + + ))} + + + + ); +} diff --git a/src/common/DisplayDrawer.tsx b/src/common/DisplayDrawer.tsx new file mode 100644 index 0000000..cc8785c --- /dev/null +++ b/src/common/DisplayDrawer.tsx @@ -0,0 +1,151 @@ +import { + AppBar, + Box, + Button, + Drawer, + Grid2 as Grid, + IconButton, + Theme, + Typography +} from "@mui/material"; +import { ArrowBackIos, ArrowForwardIos, Close, Fullscreen } from "@mui/icons-material"; +import { useMobile } from "hooks"; +import DeleteIcon from "products/AgIcons/Delete"; +import GearMarkerIcon from "products/AgIcons/GearMarker"; +import { useState } from "react"; +import { makeStyles } from "@mui/styles"; + +interface Props { + open: boolean; + onClose: (refresh?: boolean) => void; + displayPrev: () => void; + displayNext: () => void; + title: string; + subheader?: JSX.Element; + drawerBody: JSX.Element; + removeElement?: () => void; + updateElement?: () => void; + objectActions?: JSX.Element; + width: string | number; +} + +const useStyles = makeStyles((theme: Theme) => ({ + drawerMobile: { + borderRadius: 30 + }, + drawerDesktop: { + position: "relative", + top: 64 + }, + header: { + position: "static", + background: theme.palette.background.default, + paddingLeft: 15, + paddingRight: 15, + paddingBottom: 15, + zIndex: 101 + }, + appBar: { + position: "static", + borderRadius: 30 + }, + drawerPaper: { + overflowX: "hidden" + } +})); + +export default function DisplayDrawer(props: Props) { + const { + open, + onClose, + displayPrev, + displayNext, + title, + drawerBody, + removeElement, + updateElement, + objectActions, + width + } = props; + const isMobile = useMobile(); + const classes = useStyles(); + const [fullScreen, setFullScreen] = useState(false); + const [drawerHeight, setDrawerHeight] = useState(60); + + const toggleFullScreen = () => { + if (!fullScreen) { + setDrawerHeight(94); + setFullScreen(true); + } else { + setDrawerHeight(60); + setFullScreen(false); + } + }; + + return ( + + + + + + onClose()}> + + + + + {updateElement && ( + + + + )} + {removeElement && ( + + + + )} + {objectActions && objectActions} + {isMobile && ( + toggleFullScreen()}> + + + )} + + + + + + + + + + + {title} + + + + + + + + + + {drawerBody} + + + ); +} diff --git a/src/common/MapCard.tsx b/src/common/MapCard.tsx new file mode 100644 index 0000000..bbbf204 --- /dev/null +++ b/src/common/MapCard.tsx @@ -0,0 +1,66 @@ +import { createStyles, Card, CardContent, Typography } from "@mui/material"; +import { withStyles, WithStyles } from "@mui/styles"; +import React from "react"; +//import { Theme } from "@material-ui/core/styles/createMuiTheme"; +//import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles"; +import MapGL, { Coordinate } from "./MapGL"; + +const styles = () => + createStyles({ + card: { + position: "relative", + height: "100%", + minHeight: "350px" + }, + cardContent: { + height: "100%", + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center" + }, + currentLocation: { + width: "100%" + } + }); + +interface Props extends WithStyles { + path: Array; +} + +interface State {} + +class MapCard extends React.Component { + render() { + const { classes, path } = this.props; + let paths: Map> = new Map().set("path", path); + let approx = "Approximate location: Unknown"; + if (path.length > 0) { + let sorted = path.sort((a: Coordinate, b: Coordinate) => { + if (a.timestamp < b.timestamp) return 1; + else if (b.timestamp < a.timestamp) return -1; + return 0; + }); + let loc = sorted[0].latitude + ", " + sorted[0].longitude; + approx = "Approximate location: " + loc; + } + return ( + // + + + + + {approx} + + + + // + ); + } +} + +export default withStyles(styles)(MapCard); diff --git a/src/common/MapGL.tsx b/src/common/MapGL.tsx index 436fb62..999bfcf 100644 --- a/src/common/MapGL.tsx +++ b/src/common/MapGL.tsx @@ -3,7 +3,7 @@ import { Beenhere } from "@mui/icons-material"; import { useThemeType } from "hooks"; import moment from "moment"; import React, { useEffect, useState } from "react"; -import ReactMapGL, { Marker } from "react-map-gl"; +import ReactMapGL, { Marker } from "react-map-gl/mapbox-legacy"; export interface Coordinate { latitude: number; @@ -87,16 +87,18 @@ export default function MapGL(props: Props) { return ( { - setViewport({ - longitude: e.viewState.longitude, - latitude: e.viewState.latitude, - zoom: e.viewState.zoom, - }); - }}> + // onViewStateChange={(e: any) => { + // setViewport({ + // longitude: e.viewState.longitude, + // latitude: e.viewState.latitude, + // zoom: e.viewState.zoom, + // }); + // }} + > {markers()} ); diff --git a/src/device/DeviceActions.tsx b/src/device/DeviceActions.tsx index 5a2c966..206c595 100644 --- a/src/device/DeviceActions.tsx +++ b/src/device/DeviceActions.tsx @@ -42,7 +42,7 @@ import NotificationButton from "common/NotificationButton"; // import InteractionSettings from "interactions/InteractionSettings"; import { cloneDeep } from "lodash"; // import { Component, Device, deviceScope, Interaction } from "models"; -import { Device, deviceScope } from "models"; +import { Component, Device, deviceScope } from "models"; // import { MatchParams } from "navigation/Routes"; // import { isShareableLink } from "pbHelpers/Device"; import { pond } from "protobuf-ts/pond"; @@ -96,7 +96,7 @@ const useStyles = makeStyles((_theme: Theme) => { interface Props { device: Device; isPaused: boolean; - // components: Component[]; + components: Component[]; // interactions: Interaction[]; refreshCallback: () => void; availablePositions: DeviceAvailabilityMap; @@ -129,7 +129,7 @@ export default function DeviceActions(props: Props) { const { device, isPaused, - // components, + components, // interactions, refreshCallback, availablePositions, @@ -383,8 +383,8 @@ export default function DeviceActions(props: Props) { closeDialogCallback={() => closeDialog("isDeviceSettingsDialogOpen")} refreshCallback={refreshCallback} canEdit={canWrite} - // components={components} - components={[]} + components={components} + //components={[]} /> ({ + title: { + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis" + }, + noComponents: { + marginTop: theme.spacing(16) + }, + addComponentText: { + ...theme.typography.subtitle2, + color: theme.palette.primary.main, + cursor: "pointer" + }, + deviceLoading: { + position: "relative", + height: "100%", + width: "100%", + display: "flex", + marginTop: theme.spacing(16), + alignItems: "center", + justifyContent: "center" + }, + rateLimit: { + marginTop: theme.spacing(1), + color: theme.palette.text.secondary + } + }) +); +interface Props { + device: DeviceModel; + isMobile?: boolean; +} + +export default function Device(props: Props) { + const classes = useStyles(); + const [{ user }] = useGlobalState(); + const { error } = useSnackbar(); + const deviceID = props.device.id(); + const deviceAPI = useDeviceAPI(); + const componentAPI = useComponentAPI(); + const interactionsAPI = useInteractionsAPI(); + //const usageAPI = useUsageAPI(); + const userAPI = useUserAPI(); + const [isLoading, setIsLoading] = useState(false); + const [device, setDevice] = useState(DeviceModel.create()); + const [components, setComponents] = useState>(new Map()); + const [interactions, setInteractions] = useState([]); + const [permissions, setPermissions] = useState([]); + const [preferences, setPreferences] = useState( + pond.UserPreferences.create() + ); + const [invalidDevice, setInvalidDevice] = useState(false); + const [isAddComponentDialogOpen, setIsAddComponentDialogOpen] = useState(false); + const [availablePositions, setAvailablePositions] = useState(new Map()); + const [availableOffsets, setAvailableOffsets] = useState(new Map()); + const [latitude, setLatitude] = useState(NaN); + const [longitude, setLongitude] = useState(NaN); + const [cellularUsage, setCellularUsage] = useState(0); + const [cellularStatus, setCellularStatus] = useState(""); + const rate = 30; + + const load = useCallback(() => { + if (user.id() === "") return; + setIsLoading(true); + //let devicePromise = deviceAPI.get(deviceID); + let userPromise = userAPI.getUser(user.id(), deviceScope(deviceID.toString())); + let componentsPromise = componentAPI.list( + deviceID, + undefined, + [deviceID.toString()], + ["device"], + true + ); + let interactionsPromise = interactionsAPI.listInteractionsByDevice(deviceID); + let groupPromise: Promise = Promise.resolve(undefined); + Promise.all([userPromise, componentsPromise, interactionsPromise, groupPromise]) + .then(([userRes, componentsRes, interactionsRes]) => { + //let rDevice = DeviceModel.any(deviceRes.data); + let rawComponents: Array = or(componentsRes.data.components, []); + let rComponents: Map = new Map(); + rawComponents.forEach((rawComponent: any) => { + let component = Component.any(rawComponent); + rComponents.set(component.key(), component); + }); + + setComponents(rComponents); + + setInteractions(interactionsRes); + setPermissions(userRes.permissions); + setPreferences(userRes.preferences); + + setDevice(props.device); + setLongitude(props.device.status.longitude ? props.device.status.longitude : NaN); + setLatitude(props.device.status.latitude ? props.device.status.latitude : NaN); + let available = FindAvailablePositions( + Array.from(rComponents.values()), + props.device.settings.product + ); + setAvailablePositions(available.GetAvailability()); + setAvailableOffsets(available.offsetAvailability); + }) + .catch((err: any) => { + setDevice(DeviceModel.create()); + setComponents(new Map()); + setInteractions([]); + setAvailablePositions(new Map()); + setAvailableOffsets(new Map()); + setPermissions([]); + setPreferences(pond.UserPreferences.create()); + setInvalidDevice(true); + error(err); + }) + .finally(() => setIsLoading(false)); + // usageAPI + // .getUsage(deviceID, moment().subtract(1, "days")) + // .then((res: any) => { + // let usage = res.data; + // if (usage) { + // let rCellularStatus = "active"; + // let rCellularUsage = 0; + // let sessions: any[] = []; + // if (usage.sessions) { + // sessions = usage.sessions.filter((session: any) => { + // return moment(session.begin).isAfter(moment().subtract(1, "days")); + // }); + // } + // sessions.forEach((session: any) => (rCellularUsage += Number(or(session.bytes, 0)))); + // setCellularStatus(rCellularStatus); + // setCellularUsage(rCellularUsage); + // } + // }) + // .catch(err => {}); + }, [componentAPI, props.device, deviceID, error, interactionsAPI, /*usageAPI*/, userAPI, user]); + + useEffect(() => { + load(); + }, [load]); + + const getOrderedComponents = () => { + if (preferences.childDisplayOrder.length === Array.from(components.values()).length) { + return Array.from(components.values()).sort((a, b: Component) => + sortComponents(a, b, preferences.childDisplayOrder) + ); + } else { + return Array.from(components.values()); + } + }; + + const displayedComponentsBlacklist = [ + quack.ComponentType.COMPONENT_TYPE_POWER, + quack.ComponentType.COMPONENT_TYPE_MODEM + ]; + + const displayedComponents = () => { + return [...components.values()].filter( + c => !displayedComponentsBlacklist.includes(c.settings.type) + ); + }; + + const handleComponentChanged = (component: Component) => { + let updatedComponents = cloneDeep(components); + let updatedComponent = cloneDeep(component); + if (updatedComponents.has(updatedComponent.key())) { + updatedComponents.set(updatedComponent.key(), updatedComponent); + setComponents(updatedComponents); + } + }; + + // const productCard = () => { + // return ; + // }; + + const componentCards = () => { + let orderedComponents = getOrderedComponents(); + let componentCards = []; + let hasGPS = false; + for (let i = 0; i < orderedComponents.length; i++) { + let c = orderedComponents[i]; + if (displayedComponentsBlacklist.includes(c.settings.type)) { + continue; + } + let id: quack.IComponentID = quack.ComponentID.fromObject({ + type: c.settings.type, + addressType: c.settings.addressType, + address: c.settings.address + }); + let filteredInteractions = interactions.filter(interaction => { + let isSource = false; + let isSink = false; + if (interaction.settings) { + isSource = sameComponentID(interaction.settings.source, id); + isSink = sameComponentID(interaction.settings.sink, id); + } + return isSource || isSink; + }); + if (id.type === quack.ComponentType.COMPONENT_TYPE_GPS) { + hasGPS = true; + } + componentCards.push( + + updatedComponent ? handleComponentChanged(updatedComponent) : load() + } + /> + ); + } + if (!hasGPS && !isNaN(latitude) && !isNaN(longitude)) { + let path: Array = []; + path.push({ latitude: latitude, longitude: longitude, timestamp: "" }); + componentCards.push(); + } + + return componentCards; + }; + + const getUsage = () => { + let usage = undefined; + if (cellularStatus && cellularStatus !== "") { + usage = { status: cellularStatus, bytes: cellularUsage }; + } + return usage; + }; + + const anyComponentsReportingFast = () => { + let fast = false; + components.forEach(c => { + if ( + c.settings.measurementPeriodMs > 0 && + c.settings.reportPeriodMs > 0 && + c.settings.reportPeriodMs < rate * 1000 + ) { + fast = true; + } + }); + return fast; + }; + + const componentsTab = () => { + return ( + + {anyComponentsReportingFast() && ( + + Live updates are rate limited to {rate}s to ensure a smooth browsing experience + + )} + + {isLoading ? ( + + + + ) : displayedComponents().length > 0 ? ( + + {componentCards()} + + ) : ( + + + No components found + + {permissions.includes(pond.Permission.PERMISSION_WRITE) && ( + + + setIsAddComponentDialogOpen(true)}> + {" Add "} + + a component to {device.name()}. + + setIsAddComponentDialogOpen(false)} + availablePositions={availablePositions} + availableOffsets={availableOffsets} + refreshCallback={load} + canEdit={permissions.includes(pond.Permission.PERMISSION_WRITE)} + /> + + )} + + )} + + ); + }; + + const isPaused = () => { + let paused = false; + if (cellularStatus && cellularStatus !== "") { + if (cellularStatus.toLowerCase().includes("pause")) { + paused = true; + } + } + return paused; + }; + + const toggleNotificationPreference = () => { + let updatedPreferences = cloneDeep(preferences); + updatedPreferences.notify = !preferences.notify; + deviceAPI + .updatePreferences(deviceID, updatedPreferences) + .then(() => setPreferences(updatedPreferences)) + .catch(() => { + error( + "Error occured while " + + (preferences.notify ? "enabling" : "disabling") + + " notifications" + ); + }); + }; + + // if (invalidDevice) { + // return ; + // } else { + return ( + + + + + + + + + + {componentsTab()} + + ); + //} +} diff --git a/src/maps/MapBase.tsx b/src/maps/MapBase.tsx new file mode 100644 index 0000000..314d32a --- /dev/null +++ b/src/maps/MapBase.tsx @@ -0,0 +1,704 @@ +import { useEffect, useRef, useState } from "react"; +import "mapbox-gl/dist/mapbox-gl.css"; +import { + Box, + Button, + DialogActions, + DialogContent, + DialogTitle, + Grid, + Icon, + IconButton, + LinearProgress, + MenuItem, + Select, + Theme +} from "@mui/material"; +import { HomeMarker } from "models/HomeMarker"; +import { useGlobalState, useHomeMarkerAPI } from "providers"; +import { useMobile, useSnackbar } from "hooks"; +import { useCallback } from "react"; +import HomeIcon from "products/AgIcons/HomeIcon"; +//import { Theme } from "@material-ui/core/styles/createMuiTheme"; +import { GpsFixed, Layers } from "@mui/icons-material"; +//import Geocoder from "react-map-gl-geocoder"; +import "maps/mapboxStyleOverrides.css"; +import { pond } from "protobuf-ts/pond"; +import MarkerMove from "products/AgIcons/MarkerMove"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import Markers, { MarkerData } from "./mapMarkers/Markers"; +import GeoMapLayer from "./mapLayers/geoMapLayer"; +import Geocoder from "./mapControllers/Geocoder"; +import { GeometryMapping, shapeFromCoords } from "models/GeometryMapping"; +import { FeatureCollection, Feature } from "geojson"; +import DrawController from "./mapControllers/drawController"; +//import { Geometry } from "geojson"; +import Map, { MapRef, Marker, MarkerDragEvent } from "react-map-gl/mapbox-legacy"; +import { getDistanceUnit } from "utils"; +import { MapMouseEvent } from "mapbox-gl"; +import { makeStyles } from "@mui/styles"; +import { Result } from "@mapbox/mapbox-gl-geocoder"; + +//const MAPBOX_TOKEN = process.env.REACT_APP_MAPBOX_ACCESS_TOKEN; +const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; + +const mapStyleOptions = [ + { + label: "Dark", + value: "mapbox://styles/mapbox/dark-v10" + }, + { + label: "Satellite with streets", + value: "mapbox://styles/mapbox/satellite-streets-v11" + } +]; + +const useStyles = makeStyles((theme: Theme) => ({ + iconButtons: { + background: theme.palette.background.default, + margin: 5, + boxShadow: "4px 4px 10px black" + }, + pin: { + borderRadius: "50rem", + display: "inline-block", + borderBottomRightRadius: "0", + transform: "rotate(45deg)", + cursor: "pointer", + boxShadow: "4px 4px 10px black" + }, + geoDot: { + height: "25px", + width: "25px", + backgroundColor: "yellow", + borderRadius: "50%", + border: "5px solid black" + }, + geoIconColor: { + color: "black" + }, + geoIconSearch: { + animation: "$rotation 3s infinite" + }, + "@keyframes rotation": { + from: { + transform: "rotate(0deg)" + }, + to: { + transform: "rotate(360deg)" + } + } +})); + +export interface ViewData { + longitude: number; + latitude: number; + zoom: number; + transitionDuration?: number; + xOffset?: number; + yOffset?: number; +} + +export interface MeasurementData { + key: string; + geoShape: string; + coordinates: number[][]; + segmentDistanceKm: number[]; + totalDistanceKm: number; +} + +interface Props { + mapTools: JSX.Element; + mapClick(mapClickEvent: MapMouseEvent): void; + currentView: ViewData; + ignoreHomeLoad?: boolean; + defaultMapStyle?: string; + layerOptions?: JSX.Element; + markerData?: MarkerData[]; + displayMarkerTitles?: boolean; + displayMarkerDetails?: boolean; + customCursor?: JSX.Element; + objectGeoData?: Map; + measurementData?: Map; + objectTitles?: boolean; + editorMode?: "drawPolygon" | "drawLine" | "drawPoint" | "edit" | "none" | "delete"; + placingMarker?: boolean; + linePointLimit?: number; + customSearchEntries?: Result[]; + drawMeasurement?: boolean; + geocoderResultFunction?: (result: Result) => void; + geocoderTransitionFunction?: (result: Result) => void; + cutHoleInPolygon?: (object: string, coordinates: any[], geoType: string) => void; + editGeoCallback?: (object: string, borders: pond.Shape[], holes?: pond.Shape[]) => void; + addNewShape?: (coordinates: any[], geoType: string) => void; + addNewMeasurement?: (coordinates: number[][], geoType: string) => void; +} + +export default function MapBase(props: Props) { + const [startingView, setStartingView] = useState(); + //const [viewport, setViewport] = useState(props.currentView); + const transDuration = 3000; //in milliseconds + const homeMarkerAPI = useHomeMarkerAPI(); + const classes = useStyles(); + const isMobile = useMobile(); + const [mapCursor, setMapCursor] = useState("auto"); + const [hcHovered, setHCHovered] = useState(false); + const [gcHovered, setGCHovered] = useState(false); + const [dcHovered, setDCHovered] = useState(false); + const mapRef = useRef(null); + const [haveHome, setHaveHome] = useState(false); + const [homeKey, setHomeKey] = useState(""); + const [homePin, setHomePin] = useState<{ longitude: number; latitude: number }>(); + const [{ user, as }] = useGlobalState(); + const { openSnack } = useSnackbar(); + const [stateWatch, setStateWatch] = useState(0); + const [watching, setWatching] = useState(false); + const [locationFound, setLocationFound] = useState(true); + const [geoPosition, setGeoPosition] = useState({ longitude: 0, latitude: 0 }); + const [dragAllowed, setDragAllowed] = useState(false); + const [mapStyle, setMapStyle] = useState( + props.defaultMapStyle ?? "mapbox://styles/mapbox/satellite-streets-v11" + ); + const [styles, setStyles] = useState(false); + const [msHovered, setMSHovered] = useState(false); + const [layerIDs, setLayerIDs] = useState([]); + const [geoCollection, setGeoCollection] = useState(); + const [measurementCollection, setMeasurementCollection] = useState(); + const [groupSelect, setGroupSelect] = useState(false); + const [currentZoom, setCurrentZoom] = useState(props.currentView.zoom); + + const movePos = () => { + navigator.geolocation.getCurrentPosition( + pos => { + setGeoPosition({ longitude: pos.coords.longitude, latitude: pos.coords.latitude }); + setLocationFound(true); + if (mapRef.current) { + if (!mapRef.current.isMoving()) { + mapRef.current.flyTo({ + center: [pos.coords.longitude, pos.coords.latitude], + duration: transDuration, + zoom: 18 + }); + } + } + }, + () => { + //error callback function + }, + { enableHighAccuracy: true } + ); + }; + + const geolocateToggle = () => { + if (!watching) { + setWatching(true); + setStateWatch(navigator.geolocation.watchPosition(e => movePos())); + setLocationFound(false); + } else { + navigator.geolocation.clearWatch(stateWatch); + setWatching(false); + } + }; + + //used to change the viewport from the parent + useEffect(() => { + //setViewport(props.currentView); + if (mapRef.current) { + let xOffset = props.currentView.xOffset ?? 0; + let yOffset = props.currentView.yOffset ?? 0; + mapRef.current.flyTo({ + offset: [xOffset, yOffset], //this is in pixels + center: [props.currentView.longitude, props.currentView.latitude], //this is in degrees + duration: props.currentView.transitionDuration, + zoom: props.currentView.zoom + }); + } + }, [props.currentView]); + + const homeControl = () => { + if (!haveHome) { + setHaveHome(true); + if (mapRef.current) { + setHomePin({ + longitude: mapRef.current.getCenter().lng, + latitude: mapRef.current.getCenter().lat + }); + } + + // save the pin to the database + saveHomeMarker(); + } else { + if (mapRef.current && homePin) { + mapRef.current.flyTo({ + center: [homePin.longitude, homePin.latitude], + duration: transDuration + }); + } + } + }; + + const loadHomeMarkers = useCallback(() => { + homeMarkerAPI + .listHomeMarkers(1, 0, undefined, undefined, undefined, undefined, as) + .then(resp => { + if (resp.data.homeMarker.length < 1) { + setHomePin(undefined); + setHaveHome(false); + setStartingView(props.currentView); + return; + } + let hm = HomeMarker.any(resp.data.homeMarker[0]); + setHomeKey(hm.key()); + setHomePin({ + longitude: hm.long(), + latitude: hm.lat() + }); + setHaveHome(true); + if (props.ignoreHomeLoad) { + setStartingView(props.currentView); + } else if (!startingView) { + setStartingView({ + longitude: hm.long(), + latitude: hm.lat(), + zoom: user.settings.mapZoom + }); + } else if (mapRef.current) { + mapRef.current.flyTo({ + center: [hm.long(), hm.lat()], + zoom: user.settings.mapZoom, + duration: 0 + }); + } + }) + .catch(err => { + openSnack("Could not load Home Pin"); + }); + }, [as, homeMarkerAPI, openSnack, props.ignoreHomeLoad, user]); // eslint-disable-line react-hooks/exhaustive-deps + + const saveHomeMarker = () => { + if (mapRef.current) { + let hm = HomeMarker.create(); + hm.settings.userId = as ? as : user.id(); + hm.settings.longitude = mapRef.current.getCenter().lng; + hm.settings.latitude = mapRef.current.getCenter().lat; + homeMarkerAPI + .addHomeMarker(hm.settings) + .then(resp => { + setHomeKey(resp.data.homeMarker); + openSnack("New Home Marker Created"); + }) + .catch(err => openSnack("Failed to save new Home Marker")); + } + }; + + const updateHomeMarker = (longitude: number, latitude: number) => { + let hm = HomeMarker.create(); + if (homePin) { + hm.settings.latitude = latitude; + hm.settings.longitude = longitude; + hm.settings.userId = as ? as : user.id(); + homeMarkerAPI + .updateHomeMarker(homeKey, hm.settings) + .then(resp => { + openSnack("Home Marker Moved"); + }) + .catch(err => openSnack("Failed to Update Home Marker")); + } + }; + + useEffect(() => { + loadHomeMarkers(); + }, [loadHomeMarkers, as]); + + const homeDragEnd = (event: MarkerDragEvent) => { + let longitude = event.lngLat.lng; + let latitude = event.lngLat.lat; + setHomePin({ longitude, latitude }); + // update the pin in the database + updateHomeMarker(longitude, latitude); + }; + + const tools = () => { + return ( + + + {props.mapTools} + + + + {dragAllowed && ( + setGroupSelect(!groupSelect)} + onMouseOver={() => setDCHovered(true)} + onMouseOut={() => setDCHovered(false)}> + + + )} + + + + { + setGroupSelect(false); + setDragAllowed(!dragAllowed); + }} + onMouseOver={() => setDCHovered(true)} + onMouseOut={() => setDCHovered(false)}> + + + + + setStyles(true)} + onMouseOver={() => setMSHovered(true)} + onMouseOut={() => setMSHovered(false)}> + + + + + setHCHovered(true)} + onMouseOut={() => setHCHovered(false)}> + + + + + setGCHovered(true)} + onMouseOut={() => setGCHovered(false)}> + + + + + + + + + ); + }; + + const mapClick = (event: MapMouseEvent) => { + if (!watching) { + props.mapClick(event); + } + }; + + const mapStyleDialog = () => { + return ( + setStyles(false)}> + Select Map Style + + + {/* layer options from the object controller */} + {props.layerOptions} + + + + + + ); + }; + + const onMouseEnter = useCallback(() => setMapCursor("pointer"), []); + const onMouseLeave = useCallback(() => setMapCursor("auto"), []); + + useEffect(() => { + if (props.editorMode === "drawPolygon" || props.placingMarker) { + setMapCursor("crosshair"); + } else { + setMapCursor("auto"); + } + }, [props.editorMode, props.placingMarker]); + + useEffect(() => { + if (props.objectGeoData) { + let feats: Feature[] = []; + props.objectGeoData.forEach(data => { + let newFeature = GeometryMapping.geoJSON(data.geoShape, data.shapes, data.holes) as Feature; + newFeature.id = data.objectKey; + newFeature.properties = { + title: data.title, + objectKey: data.objectKey, + fill: data.colour, + lineWidth: data.geoShape === "LineString" ? 5 : 2, + origin: data.origin + }; + feats.push(newFeature); + }); + let collection: FeatureCollection = { + type: "FeatureCollection", + features: feats + }; + setGeoCollection(collection); + } + }, [props.objectGeoData]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + //add the measurementdata into the feature collection to be passed to our geo map layer + if (props.measurementData) { + let feats: Feature[] = []; + props.measurementData.forEach(data => { + let newFeature = GeometryMapping.geoJSON(data.geoShape, [ + shapeFromCoords(data.coordinates) + ]) as Feature; + newFeature.id = data.key; + newFeature.properties = { + lineWidth: 5, + origin: pond.DataOrigin.DATA_ORIGIN_ADAPTIVE, + totalDist: + getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS + ? (data.totalDistanceKm / 1000).toFixed(2) + : (data.totalDistanceKm * 3280.8398950131).toFixed(2) + }; + feats.push(newFeature); + }); + let collection: FeatureCollection = { + type: "FeatureCollection", + features: feats + }; + setMeasurementCollection(collection); + } + }, [props.measurementData]); + + const onUpdate = (event: any) => { + let borders: pond.Shape[] = []; + let holes: pond.Shape[] = []; + let objectKey: string = ""; + if (event.action === "change_coordinates" && event.features) { + let feature = event.features[0]; + if (feature) { + let coords: any[] = feature.geometry.coordinates; + objectKey = feature.id; + if (feature.geometry.type === "Polygon") { + coords.forEach((poly: any[], i) => { + //the first shape is the border + if (i === 0) { + borders.push(shapeFromCoords(poly)); + } else { + holes.push(shapeFromCoords(poly)); + } + }); + } else if (feature.geometry.type === "LineString") { + borders.push(shapeFromCoords(coords)); + } + } + } + if (props.editGeoCallback) { + props.editGeoCallback(objectKey, borders, holes.length > 0 ? holes : undefined); + } + }; + + const onCreate = (event: any, objectToCut?: string, measurement?: boolean) => { + let feature = event.features[0]; + console.log(feature) + //if the string exist we are adding a hole to an existing object + if (objectToCut) { + if (props.cutHoleInPolygon) { + props.cutHoleInPolygon(objectToCut, feature.geometry.coordinates, feature.geometry.type); + } + } else if (measurement) { + if (props.addNewMeasurement) { + props.addNewMeasurement(feature.geometry.coordinates, feature.geometry.type); + } + } else { + if (props.addNewShape) { + console.log("add new shape") + props.addNewShape(feature.geometry.coordinates, feature.geometry.type); + } + } + }; + + const getEditableFeatures = () => { + let editableFeatures: FeatureCollection = { + type: "FeatureCollection", + features: [] + }; + if (geoCollection) { + geoCollection.features.forEach(feature => { + //if the origin of the data comes from us (ie. a field/construction site with the adaptive origin) + if ( + feature.properties && + feature.properties.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE + ) { + editableFeatures.features.push(feature); + } + }); + } + return editableFeatures; + }; + + return startingView ? ( + + {tools()} + {mapStyleDialog()} + { + setCurrentZoom(e.viewState.zoom); + }} + onMouseEnter={() => { + if ( + !props.editorMode || + !props.placingMarker || + props.editorMode === "none" || + !props.placingMarker + ) + onMouseEnter(); + }} + onMouseLeave={() => { + if ( + !props.editorMode || + !props.placingMarker || + props.editorMode === "none" || + !props.placingMarker + ) + onMouseLeave(); + }} + interactiveLayerIds={layerIDs} + // onMove={e => { + // setViewport({ + // longitude: e.viewState.longitude, + // latitude: e.viewState.latitude, + // zoom: e.viewState.zoom + // }); + // }} + mapboxAccessToken={MAPBOX_TOKEN} + cursor={mapCursor} + onClick={e => { + mapClick(e); + }} + doubleClickZoom={false}> + {/* map controllers */} + {MAPBOX_TOKEN && ( + + )} + + {/* markers */} + {props.markerData && ( + + )} + + {watching && locationFound && ( + + + + )} + {haveHome && homePin && ( + + + + + + + + )} + {/* map layers */} + {geoCollection && ( + { + setLayerIDs(e); + }} + /> + )} + + + ) : ( + + + + ); +} \ No newline at end of file diff --git a/src/maps/MapMarkerSettings.tsx b/src/maps/MapMarkerSettings.tsx new file mode 100644 index 0000000..dd80faa --- /dev/null +++ b/src/maps/MapMarkerSettings.tsx @@ -0,0 +1,99 @@ +import { + Box, + Button, + DialogActions, + DialogContent, + DialogTitle, + Slider, + TextField +} from "@mui/material"; +import ColourPicker from "common/ColourPicker"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import { pond } from "protobuf-ts/pond"; +import { useEffect, useState } from "react"; + +interface Props { + open: boolean; + close: () => void; + theme: pond.ObjectTheme; + sizeControl?: boolean; + currentSize?: number; + colourControl?: boolean; + updateObject: (newTheme: pond.ObjectTheme) => void; +} + +export default function MapMarkerSettings(props: Props) { + const { theme, sizeControl, colourControl, updateObject, currentSize } = props; + //const [objectSettings, setObjectSettings] = useState(props.objectSettings); + const [newColour, setNewColour] = useState(""); + const [newSize, setNewSize] = useState(50); + const markerMin = 40; + const markerMax = 125; + + useEffect(() => { + if (currentSize) { + setNewSize(currentSize); + } else { + setNewSize(50); + } + }, [currentSize]); + + const reSize = (val: number) => { + let size = val; + if (val > markerMax) { + size = markerMax; + } + if (val < markerMin) { + size = markerMin; + } + setNewSize(size); + }; + + const update = () => { + let newTheme = theme; + newTheme.color = newColour; + newTheme.height = newSize; + newTheme.width = newSize; + updateObject(newTheme); + }; + + return ( + + Change Marker Settings + + {sizeControl && ( + + Marker Size + reSize(val as number)} + valueLabelDisplay="auto" + /> + reSize(+e.target.value)} /> + + )} + {colourControl && ( + + Marker Colour + setNewColour(color)} /> + + )} + + + + + + + ); +} \ No newline at end of file diff --git a/src/maps/mapControllers/Geocoder.tsx b/src/maps/mapControllers/Geocoder.tsx new file mode 100644 index 0000000..5bfb009 --- /dev/null +++ b/src/maps/mapControllers/Geocoder.tsx @@ -0,0 +1,146 @@ +import { ControlPosition, useMap } from "react-map-gl/mapbox-legacy"; +import MapboxGeocoder, { Result } from "@mapbox/mapbox-gl-geocoder"; +import { useEffect } from "react"; + +// export interface GeocoderObject { +// id: string; +// place_name: string; +// center: number[]; +// place_type: string[]; +// } + +// interface GeocoderResult { +// result: GeocoderObject; +// } +interface Props { + mapboxAccessToken: string; + position: ControlPosition; + customEntries?: Result[]; + customTransition?: (objectResult: Result) => void; + resultFunction?: (objectResult: Result) => void; +} + +//const MAPBOX_TOKEN = process.env.REACT_APP_MAPBOX_ACCESS_TOKEN; +// const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; + +// const geoCoder = new MapboxGeocoder({ +// accessToken: MAPBOX_TOKEN ?? "", +// flyTo: true, +// marker: false, +// zoom: 18 +// }); + +export default function Geocoder(props: Props) { + const { mapboxAccessToken, position, customEntries, resultFunction, customTransition } = props; + const { current: map } = useMap(); + + const localSearch = (query: string) => { + const matchesCoords = query.match( + /^[ ]*(?:Lat: )?(-?\d+\.?\d*)[, ]+(?:Lng: )?(-?\d+\.?\d*)[ ]*$/i + ); + let matchingResults: Result[] = []; + if (matchesCoords) { + const coord1 = Number(matchesCoords[1]); + const coord2 = Number(matchesCoords[2]); + const matchingCoords: Result[] = []; + + if (coord1 < -90 || coord1 > 90) { + // coord1 is longitude + matchingCoords.push({ + id: "Lat: " + coord2 + " Lng: " + coord1, + place_name: "Lat: " + coord2 + " Lng: " + coord1, + center: [coord1, coord2], + place_type: ["coordinate"] + } as Result); + } + + if (coord2 < -90 || coord2 > 90) { + // coord2 is longitude + matchingCoords.push({ + id: "Lat: " + coord1 + " Lng: " + coord2, + place_name: "Lat: " + coord1 + " Lng: " + coord2, + center: [coord2, coord1], + place_type: ["coordinate"] + } as Result); + } + + if (matchingCoords.length === 0) { + // else could be either lng, lat or lat, lng + matchingCoords.push({ + id: "Lat: " + coord2 + " Lng: " + coord1, + place_name: "Lat: " + coord2 + " Lng: " + coord1, + center: [coord1, coord2], + place_type: ["coordinate"] + } as Result); + matchingCoords.push({ + id: "Lat: " + coord1 + " Lng: " + coord2, + place_name: "Lat: " + coord1 + " Lng: " + coord2, + center: [coord2, coord1], + place_type: ["coordinate"] + } as Result); + } + + matchingResults = matchingResults.concat(matchingCoords); + } + + customEntries?.forEach(obj => { + if (obj.place_name.toLowerCase().includes(query.toLowerCase())) { + matchingResults.push(obj); + } + }); + return matchingResults; + }; + + // //set the geocoder options + // useEffect(() => { + // //connects the localSearch function to the geocoder if there are custom entries passed in + // if (customEntries) { + // //need to determine a new way to handle custom search with updated geocoder + // //geoCoder.options.localGeocoder = localSearch; + // } + // //disables the transition that the geocoder would fire if a custom transition was passed in + // if (customTransition) { + // geoCoder.setFlyTo(false); + // } + // }, [customEntries, customTransition]); // eslint-disable-line react-hooks/exhaustive-deps + + //attach custom functions to event handlers + useEffect(() => { + const geoCoder = new MapboxGeocoder({ + accessToken: mapboxAccessToken, + localGeocoder: localSearch, + flyTo: customTransition ? false : true, + marker: false, + zoom: 18, + }); + //events that fire as the search bar is typed into, note the function will run AFTER the event fires + + //event is fired when the search bar is cleared + geoCoder.on("clear", () => {}); + + //event is fired when starting to load the results + geoCoder.on("loading", () => {}); + + //event is fired when the results come back from a search + geoCoder.on("results", () => {}); + + //event is fired when there is an error + geoCoder.on("error", () => {}); + + //event is fired when an option is selected from the results + geoCoder.on("result", (res: Result) => { + if (resultFunction) { + resultFunction(res); + } + if (customTransition) { + customTransition(res); + } + }); + //once the map ref and access token are set up add the control to the map + if (map && mapboxAccessToken) { + map.addControl(geoCoder, position); + } + }, [map, mapboxAccessToken, position]); // eslint-disable-line react-hooks/exhaustive-deps + + return null; +} diff --git a/src/maps/mapControllers/drawController.tsx b/src/maps/mapControllers/drawController.tsx new file mode 100644 index 0000000..030eba1 --- /dev/null +++ b/src/maps/mapControllers/drawController.tsx @@ -0,0 +1,112 @@ +import { useEffect, useRef } from "react"; +import { useMap } from "react-map-gl/mapbox-legacy"; +import { FeatureCollection } from "geojson"; +import MapboxDraw from "@mapbox/mapbox-gl-draw"; + +interface Props { + featureCollection: FeatureCollection; + editMode?: "drawPolygon" | "drawLine" | "drawPoint" | "edit" | "none" | "delete"; + linePointLimit?: number; + measurement?: boolean; + onCreate: (event: any, objectToCut?: string, measurement?: boolean) => void; + onUpdate: (event: any) => void; +} +const draw = new MapboxDraw({ + displayControlsDefault: false +}); + +export default function DrawController(props: Props) { + const { featureCollection, editMode, onCreate, onUpdate, linePointLimit, measurement } = props; + //const [shapeToCut, setShapeToCut] = useState(); + const cutRef = useRef(); //the object id for what shape to cut a hole + const mRef = useRef(); + const lineLimitRef = useRef(); + const pointsRef = useRef([]); + const { current: map } = useMap(); + + //use effect is just for adding and removing the controller from the map + useEffect(() => { + if (map) { + if (editMode && editMode !== "none" && !map.hasControl(draw)) { + map.addControl(draw); + } else if ((!editMode || editMode === "none") && map.hasControl(draw)) { + map.removeControl(draw); + } + } + }, [map, editMode]); + + useEffect(() => { + lineLimitRef.current = linePointLimit; + mRef.current = measurement; + }, [linePointLimit, measurement]); + + //useEffect for adding event handlers to the map + useEffect(() => { + if (map) { + map.on("draw.create", e => { + //console.log("draw create"); + //using refs rather than state variables you can effectively get the state in an event listener + onCreate(e, cutRef.current, mRef.current); + }); + map.on("draw.update", e => { + onUpdate(e); + }); + map.on("click", ["shapefill", "shapeborder"], e => { + //set the ref to be undefined + cutRef.current = undefined; + if (map.hasControl(draw) && e.features) { + let mode = draw.getMode(); + let id = e.features[0].properties?.objectKey; + if (mode === "simple_select" && id) { + if (draw.get(id)) { + draw.changeMode("direct_select", { + // The id of the feature that will be directly selected (required) + featureId: id + }); + } + } else if (mode === "draw_polygon") { + //if a feature was clicked on set ref to have its id + cutRef.current = id; + //setShapeToCut(id); + } + } + }); + map.on("click", e => { + if (map.hasControl(draw) && draw.getMode() === "draw_line_string") { + if (lineLimitRef.current) { + pointsRef.current.push([e.lngLat.lng, e.lngLat.lat]); + if (pointsRef.current.length === lineLimitRef.current) { + draw.changeMode("simple_select"); //changing the mode causes the create event to fire + pointsRef.current = []; + draw.changeMode("draw_line_string"); + } + } + } + }); + } + }, [map]); // eslint-disable-line react-hooks/exhaustive-deps + + //useEffect for changing draw mode for the controller based on the editor mode from the object controller + useEffect(() => { + if (editMode === "drawPolygon") { + draw.deleteAll(); + draw.changeMode("draw_polygon"); + } else if (editMode === "drawLine") { + draw.deleteAll(); + draw.changeMode("draw_line_string"); + } else if (editMode === "drawPoint") { + draw.deleteAll(); + draw.changeMode("draw_point"); + } else if (editMode === "delete") { + draw.deleteAll(); + draw.changeMode("simple_select"); + } else if (editMode === "edit") { + draw.set(featureCollection); + if (draw.getMode() !== "simple_select" && draw.getMode() !== "direct_select") { + draw.changeMode("simple_select"); + } + } + }, [editMode, featureCollection]); + + return null; +} diff --git a/src/maps/mapDrawers/BinDrawer.tsx b/src/maps/mapDrawers/BinDrawer.tsx new file mode 100644 index 0000000..cdf1b3f --- /dev/null +++ b/src/maps/mapDrawers/BinDrawer.tsx @@ -0,0 +1,148 @@ +import { Box } from "@mui/material"; +import DisplayDrawer from "common/DisplayDrawer"; +import MapMarkerSettings from "maps/MapMarkerSettings"; +import { Bin as IBin } from "models"; +import Bin from "pages/Bin"; +import { pond } from "protobuf-ts/pond"; +import { useBinAPI, useSnackbar } from "providers"; +import React, { useEffect, useState } from "react"; + +interface Props { + open: boolean; + onClose: () => void; + selectedBin: string; + bins: Map; + removeMarker: (key: string) => void; + updateMarker: (key: string, newSettings: pond.BinSettings) => void; + moveMap: (long: number, lat: number) => void; +} + +export default function BinDrawer(props: Props) { + const { open, onClose, selectedBin, bins, removeMarker, updateMarker, moveMap } = props; + const [bin, setBin] = useState(IBin.create()); + const [openMarkerSettings, setOpenMarkerSettings] = useState(false); + const binAPI = useBinAPI(); + const { openSnack } = useSnackbar(); + + useEffect(() => { + let b = bins.get(selectedBin); + if (b) { + setBin(b); + } + }, [selectedBin, bins]); + + const closeDrawer = () => { + onClose(); + }; + + const displayNext = () => { + let binArr = Array.from(bins.values()); + let index = binArr.indexOf(bin); + let found = false; + do { + if (index === binArr.length - 1) { + index = 0; + } else { + index++; + } + let nextBin = binArr[index]; + let location = nextBin.location(); + if ( + location !== null && + location !== undefined && + location.longitude !== 0 && + location.longitude !== 0 + ) { + setBin(nextBin); + moveMap(location.longitude, location.latitude); + found = true; + } + } while (!found); + }; + + const displayPrev = () => { + let binArr = Array.from(bins.values()); + let index = binArr.indexOf(bin); + let found = false; + do { + if (index === 0) { + index = binArr.length - 1; + } else { + index--; + } + let nextBin = binArr[index]; + let location = nextBin.location(); + if ( + location !== null && + location !== undefined && + location.longitude !== 0 && + location.longitude !== 0 + ) { + setBin(nextBin); + moveMap(location.longitude, location.latitude); + found = true; + } + } while (!found); + }; + + const drawerBody = () => { + return {bin.key() !== "" && }; + }; + + const remove = () => { + let settings = bin.settings; + settings.location = null; + + binAPI + .updateBin(bin.key(), settings) + .then(resp => { + openSnack("Removed bin marker"); + removeMarker(bin.key()); + }) + .catch(err => { + openSnack("Failed to remove bin marker"); + }); + }; + + const update = () => { + setOpenMarkerSettings(true); + }; + + return ( + + + { + setOpenMarkerSettings(false); + }} + open={openMarkerSettings} + theme={bin.settings.theme ?? pond.ObjectTheme.create()} + sizeControl + currentSize={bin.settings.theme?.height} + updateObject={newTheme => { + let settings = bin.settings; + settings.theme = newTheme; + binAPI + .updateBin(bin.key(), settings) + .then(resp => { + openSnack("marker settings updated"); + updateMarker(bin.key(), settings); + }) + .catch(() => { + openSnack("failed to update marker settings"); + }); + }} + /> + + ); +} \ No newline at end of file diff --git a/src/maps/mapDrawers/BinYardDrawer.tsx b/src/maps/mapDrawers/BinYardDrawer.tsx new file mode 100644 index 0000000..242c756 --- /dev/null +++ b/src/maps/mapDrawers/BinYardDrawer.tsx @@ -0,0 +1,153 @@ +import BinyardDisplay from "bin/BinyardDisplay"; +import DisplayDrawer from "common/DisplayDrawer"; +import MapMarkerSettings from "maps/MapMarkerSettings"; +//import Bins from "pages/Bins"; +import { pond } from "protobuf-ts/pond"; +import { useBinYardAPI, useSnackbar } from "providers"; +import React, { useEffect, useState } from "react"; + +interface Props { + open: boolean; + onClose: () => void; + selectedYard: string; + yards: Map; + removeMarker: (key: string) => void; + updateMarker: (key: string, objectSettings: pond.BinYardSettings) => void; + moveMap: (long: number, lat: number) => void; +} + +export default function BinYardDrawer(props: Props) { + const { open, onClose, selectedYard, yards, removeMarker, updateMarker, moveMap } = props; + const [yard, setYard] = useState(pond.BinYardSettings.create()); + const [openMarkerSettings, setOpenMarkerSettings] = useState(false); + const { openSnack } = useSnackbar(); + const binyardAPI = useBinYardAPI(); + + useEffect(() => { + let yard = yards.get(selectedYard); + if (yard) { + setYard(yard); + } + }, [selectedYard, yards]); + + const closeDrawer = () => { + onClose(); + }; + + const displayNext = () => { + let yardArr = Array.from(yards.values()); + let index = yardArr.indexOf(yard); + let found = false; + do { + if (index === yardArr.length - 1) { + index = 0; + } else { + index++; + } + let nextYard = yardArr[index]; + if ( + nextYard.latitude !== 0 && + nextYard.longitude !== 0 && + nextYard.latitude !== undefined && + nextYard.longitude !== undefined + ) { + setYard(nextYard); + moveMap(nextYard.longitude, nextYard.latitude); + found = true; + } + } while (!found); + }; + + const displayPrev = () => { + let yardArr = Array.from(yards.values()); + let index = yardArr.indexOf(yard); + let found = false; + do { + if (index === 0) { + index = yardArr.length - 1; + } else { + index--; + } + let nextYard = yardArr[index]; + if ( + nextYard.latitude !== 0 && + nextYard.longitude !== 0 && + nextYard.latitude !== undefined && + nextYard.longitude !== undefined + ) { + setYard(nextYard); + moveMap(nextYard.longitude, nextYard.latitude); + found = true; + } + } while (!found); + }; + + const drawerBody = () => { + //return ; + return ; + }; + + /** + * function to remove the marker and coordinates from the object + */ + const remove = () => { + //set the long/lat of the yard to 0 and call an update + let settings = yard; + settings.longitude = 0; + settings.latitude = 0; + binyardAPI + .updateBinYard(yard.key, settings) + .then(resp => { + openSnack("Marker Removed"); + //then use the removeMarker prop function to update the markers in the parent map + removeMarker(yard.key); + }) + .catch(resp => { + openSnack("there was a problem removing the marker"); + }); + }; + + /** + * function to update the settings of the marker + */ + const update = () => { + setOpenMarkerSettings(true); + }; + + return ( + + + { + setOpenMarkerSettings(false); + }} + open={openMarkerSettings} + theme={yard.theme ?? pond.ObjectTheme.create()} + colourControl + updateObject={newTheme => { + let settings = yard; + settings.theme = newTheme; + binyardAPI + .updateBinYard(yard.key, settings) + .then(resp => { + openSnack("marker settings updated"); + updateMarker(yard.key, settings); + }) + .catch(() => { + openSnack("failed to update marker settings"); + }); + }} + /> + + ); +} \ No newline at end of file diff --git a/src/maps/mapDrawers/DeviceDrawer.tsx b/src/maps/mapDrawers/DeviceDrawer.tsx new file mode 100644 index 0000000..b02749e --- /dev/null +++ b/src/maps/mapDrawers/DeviceDrawer.tsx @@ -0,0 +1,153 @@ +import { Box } from "@mui/material"; +import DisplayDrawer from "common/DisplayDrawer"; +import DeviceViewer from "device/DeviceViewer"; +import MapMarkerSettings from "maps/MapMarkerSettings"; +import { Device } from "models"; +import { pond } from "protobuf-ts/pond"; +import { useDeviceAPI, useSnackbar } from "providers"; +import React, { useEffect, useState } from "react"; + +interface Props { + open: boolean; + onClose: () => void; + selectedDevice: string; + devices: Map; + removeMarker: (key: string) => void; + updateMarker: (key: string, newSettings: pond.DeviceSettings) => void; + moveMap: (long: number, lat: number) => void; +} + +export default function DeviceDrawer(props: Props) { + const { open, onClose, selectedDevice, devices, removeMarker, updateMarker, moveMap } = props; + const [device, setDevice] = useState(Device.create()); + const [openMarkerSettings, setOpenMarkerSettings] = useState(false); + const deviceAPI = useDeviceAPI(); + const { openSnack } = useSnackbar(); + + useEffect(() => { + let d = devices.get(selectedDevice); + if (d) { + setDevice(d); + } + }, [selectedDevice, devices]); + + const closeDrawer = () => { + onClose(); + }; + + const displayNext = () => { + let devArr = Array.from(devices.values()); + let index = devArr.indexOf(device); + let found = false; + do { + if (index === devArr.length - 1) { + index = 0; + } else { + index++; + } + let nextDev = devArr[index]; + if ( + nextDev.location().longitude !== 0 && + nextDev.location().longitude !== 0 && + nextDev.location().latitude !== undefined && + nextDev.location().longitude !== undefined + ) { + setDevice(nextDev); + moveMap(nextDev.location().longitude, nextDev.location().latitude); + found = true; + } + } while (!found); + }; + + const displayPrev = () => { + let devArr = Array.from(devices.values()); + let index = devArr.indexOf(device); + let found = false; + do { + if (index === 0) { + index = devArr.length - 1; + } else { + index--; + } + let nextDev = devArr[index]; + if ( + nextDev.location().longitude !== 0 && + nextDev.location().longitude !== 0 && + nextDev.location().latitude !== undefined && + nextDev.location().longitude !== undefined + ) { + setDevice(nextDev); + moveMap(nextDev.location().longitude, nextDev.location().latitude); + found = true; + } + } while (!found); + }; + + const drawerBody = () => { + return device.id() !== 0 ? : ; + }; + + /** + * function to remove the marker and coordinates from the object + */ + const remove = () => { + //set the long/lat of the yard to 0 and call an update + let settings = device.settings; + settings.longitude = 0; + settings.latitude = 0; + deviceAPI + .update(device.id(), settings) + .then(resp => { + openSnack("Marker Removed"); + //then use the removeMarker prop function to update the markers in the parent map + removeMarker(device.id().toString()); + }) + .catch(err => { + openSnack("Failed to remove marker"); + }); + }; + + /** + * function to update the settings of the marker + */ + const update = () => { + setOpenMarkerSettings(true); + }; + + return ( + + + { + setOpenMarkerSettings(false); + }} + open={openMarkerSettings} + theme={device.settings.theme ?? pond.ObjectTheme.create()} + colourControl + updateObject={newTheme => { + let settings = device.settings; + settings.theme = newTheme; + deviceAPI + .update(device.id(), settings) + .then(resp => { + openSnack("marker settings updated"); + updateMarker(device.id().toString(), settings); + }) + .catch(() => { + openSnack("failed to update marker settings"); + }); + }} + /> + + ); +} diff --git a/src/maps/mapDrawers/FieldDrawer.tsx b/src/maps/mapDrawers/FieldDrawer.tsx new file mode 100644 index 0000000..e859e02 --- /dev/null +++ b/src/maps/mapDrawers/FieldDrawer.tsx @@ -0,0 +1,326 @@ +import { + Box, + Divider, + Drawer, + Grid2 as Grid, + IconButton, + Tab, + Tabs, + Theme, + Typography +} from "@mui/material"; +import { Notes } from "@mui/icons-material"; +import DisplayDrawer from "common/DisplayDrawer"; +//import HarvestPlanDisplay from "harvestPlan/HarvestPlanDisplay"; +import { Field, fieldScope, /*HarvestPlan,*/ teamScope } from "models"; +import React, { useEffect, useState } from "react"; +//import TaskViewer from "tasks/TaskViewer"; +import Weather from "weather/weather"; +import { getThemeType } from "theme"; +import GrainDescriber from "grain/GrainDescriber"; +import Chat from "chat/Chat"; +import { pond } from "protobuf-ts/pond"; +import { useMobile } from "hooks"; +import { useGlobalState, /*useHarvestPlanAPI,*/ useUserAPI } from "providers"; +import { makeStyles } from "@mui/styles"; +//import FieldActions from "field/FieldActions"; + +interface TabPanelProps { + children?: React.ReactNode; + index: any; + value: any; +} + +function TabPanelMine(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} +interface Props { + open: boolean; + closeDrawer: () => void; + selectedFieldKey: string; + fields: Map; + openSettings: (fieldKey: string) => void; + moveMap: (long: number, lat: number) => void; +} + +const useStyles = makeStyles((theme: Theme) => ({ + avatar: { + color: getThemeType() === "light" ? theme.palette.common.black : theme.palette.common.white, + backgroundColor: "transparent", + width: theme.spacing(5), + height: theme.spacing(5), + border: "1px solid", + borderColor: theme.palette.divider + }, + dark: { + backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)", + padding: 5 + }, + light: { + backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)", + padding: 5 + } + +})); + +export default function FieldDrawer(props: Props) { + const { open, closeDrawer, selectedFieldKey, fields, moveMap } = props; + const [field, setField] = useState(Field.create()); + //const [hPlan, setHPlan] = useState(HarvestPlan.create()); + const [value, setValue] = useState(0); + const classes = useStyles(); + const [openNote, setOpenNote] = useState(false); + const isMobile = useMobile(); + const [{ as, user }] = useGlobalState(); + //const hPlanAPI = useHarvestPlanAPI(); + const [planLoading, setPlanLoading] = useState(false); + const userAPI = useUserAPI(); + const [permissions, setPermissions] = useState([]); + + useEffect(() => { + let field = fields.get(selectedFieldKey); + if (field) { + setField(field); + } + }, [selectedFieldKey, fields]); + + useEffect(() => { + let scope; + if (as) { + //if they are viewing as a team regardless of whether the field is adaptive or external (from JD or CNH) get the users permission to the team + scope = teamScope(as); + } else if ( + field.settings.fieldGeoData && + field.settings.fieldGeoData.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE + ) { + //if they are viewing as a user and the field is an adaptive field get the users permission to the field + scope = fieldScope(field.key()); + } + + //if the scope was set get the permissions the user has in that scope + if (scope) { + userAPI.getUser(user.id(), scope).then(resp => { + field.permissions = resp.permissions; + setPermissions(resp.permissions); + }); + } else { + //if scope wasn't set that means that they are viewing an external field as a user and so will have full permissions + //because they would have full permissions to the organization that the field is linked to since users cannot share organizations + //and they are only linked to team/user combinations during creation and cannot be shared afterward + let perms = [ + pond.Permission.PERMISSION_READ, + pond.Permission.PERMISSION_WRITE, + pond.Permission.PERMISSION_SHARE, + pond.Permission.PERMISSION_USERS + ]; + field.permissions = perms; + setPermissions(perms); + } + }, [as, user, userAPI, field]); //eslint-disable-line react-hooks/exhaustive-deps + + // useEffect(() => { + // if (field.key() !== "") { + // hPlanAPI + // .listHarvestPlans(1, 0, "desc", "createDate", field.key(), as) + // .then(resp => { + // if (resp.data.harvestPlan.length > 0) { + // let plan = resp.data.harvestPlan[0]; + // setHPlan(HarvestPlan.any(plan)); + // } else { + // setHPlan(HarvestPlan.create()); + // } + // setPlanLoading(false); + // }) + // .catch(err => { + // //openSnack("Failed to load plan"); + // }); + // } + // }, [field, as, hPlanAPI]); + + const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { + setValue(newValue); + }; + + const displayNext = () => { + let arr = Array.from(fields.values()); + let index = arr.indexOf(field); + let found = false; + do { + if (index === arr.length - 1) { + index = 0; + } else { + index++; + } + let nextField = arr[index]; + let location = nextField.center(); + if ( + location !== null && + location !== undefined && + location.longitude !== 0 && + location.longitude !== 0 + ) { + setField(nextField); + moveMap(location.longitude, location.latitude); + found = true; + } + } while (!found); + }; + + const displayPrev = () => { + let arr = Array.from(fields.values()); + let index = arr.indexOf(field); + let found = false; + do { + if (index === 0) { + index = arr.length - 1; + } else { + index--; + } + let nextField = arr[index]; + let location = nextField.center(); + if ( + location !== null && + location !== undefined && + location.longitude !== 0 && + location.longitude !== 0 + ) { + setField(nextField); + moveMap(location.longitude, location.latitude); + found = true; + } + } while (!found); + }; + + const drawerBody = () => { + let taskLoadKeys: string[] = []; + if (!planLoading) { + field.key() !== "" && taskLoadKeys.push(field.key()); + //hPlan.key() !== "" && taskLoadKeys.push(hPlan.key()); + } + return ( + + + + + + + + + setOpenNote(true)}> + + + + + {field.fieldName()} Details + + + + + Approximate Area: + {field.acres()}ac + + + Grain: + + {field.grain() === pond.Grain.GRAIN_CUSTOM + ? field.customType() + : GrainDescriber(field.crop()).name} + + + + Grain Variant: + {field.settings.grainSubtype} + + + Land Location: + {field.landLoc()} + + + + + + {/* { + if (updatedPlan) { + setHPlan(updatedPlan); + } + }} + /> */} + + + + + + + {/* */} + + + ); + }; + + const noteDrawer = () => { + return ( + setOpenNote(false)}> + + Notes + + + + ); + }; + + return ( + + { + props.openSettings(selectedFieldKey); + } + : undefined + } + // objectActions={ + // field.settings.fieldGeoData && + // field.settings.fieldGeoData.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE ? ( + // {}} /> + // ) : ( + // undefined + // ) + // } + /> + {noteDrawer()} + + ); +} diff --git a/src/maps/mapDrawers/GrainBagDrawer.tsx b/src/maps/mapDrawers/GrainBagDrawer.tsx new file mode 100644 index 0000000..5b1b0e6 --- /dev/null +++ b/src/maps/mapDrawers/GrainBagDrawer.tsx @@ -0,0 +1,148 @@ +import { Box } from "@mui/material"; +import DisplayDrawer from "common/DisplayDrawer"; +import MapMarkerSettings from "maps/MapMarkerSettings"; +import { GrainBag as BagModel } from "models/GrainBag"; +//import GrainBag from "pages/grainBag"; +import { pond } from "protobuf-ts/pond"; +import { useGrainBagAPI, useSnackbar } from "providers"; +import React, { useEffect, useState } from "react"; + +interface Props { + open: boolean; + onClose: () => void; + selectedBag: string; + grainBags: Map; + removeMarker: (key: string) => void; + updateMarker: (key: string, newSettings: pond.GrainBagSettings) => void; + moveMap: (long: number, lat: number) => void; +} + +export default function GrainBagDrawer(props: Props) { + const { open, onClose, selectedBag, grainBags, removeMarker, updateMarker, moveMap } = props; + const [bag, setBag] = useState(BagModel.create()); + const [openMarkerSettings, setOpenMarkerSettings] = useState(false); + const bagAPI = useGrainBagAPI(); + const { openSnack } = useSnackbar(); + + useEffect(() => { + let b = grainBags.get(selectedBag); + if (b) { + setBag(b); + } + }, [selectedBag, grainBags]); + + const closeDrawer = () => { + onClose(); + }; + + const displayNext = () => { + let bagArr = Array.from(grainBags.values()); + let index = bagArr.indexOf(bag); + let found = false; + do { + if (index === bagArr.length - 1) { + index = 0; + } else { + index++; + } + let nextBag = bagArr[index]; + let location = nextBag.centerLocation(); + if ( + location !== null && + location !== undefined && + location.longitude !== 0 && + location.longitude !== 0 + ) { + setBag(nextBag); + moveMap(location.longitude, location.latitude); + found = true; + } + } while (!found); + }; + + const displayPrev = () => { + let bagArr = Array.from(grainBags.values()); + let index = bagArr.indexOf(bag); + let found = false; + do { + if (index === 0) { + index = bagArr.length - 1; + } else { + index--; + } + let nextBag = bagArr[index]; + let location = nextBag.centerLocation(); + if ( + location !== null && + location !== undefined && + location.longitude !== 0 && + location.longitude !== 0 + ) { + setBag(nextBag); + moveMap(location.longitude, location.latitude); + found = true; + } + } while (!found); + }; + + const drawerBody = () => { + //return {bag.key() !== "" && }; + return + }; + + const update = () => { + setOpenMarkerSettings(true); + }; + + const remove = () => { + let settings = bag.settings; + settings.startLocation = undefined; + settings.endLocation = undefined; + bagAPI + .updateGrainBag(bag.key(), bag.name(), settings) + .then(resp => { + openSnack("Grain bag location removed"); + removeMarker(bag.key()); + }) + .catch(err => { + openSnack("Failed to remove grain bag location"); + }); + }; + + return ( + + + { + setOpenMarkerSettings(false); + }} + open={openMarkerSettings} + colourControl + theme={bag.settings.theme ?? pond.ObjectTheme.create()} + updateObject={newTheme => { + let settings = bag.settings; + settings.theme = newTheme; + bagAPI + .updateGrainBag(bag.key(), bag.name(), settings) + .then(resp => { + openSnack("marker settings updated"); + updateMarker(bag.key(), settings); + }) + .catch(() => { + openSnack("failed to update marker settings"); + }); + }} + /> + + ); +} diff --git a/src/maps/mapLayers/geoMapLayer.tsx b/src/maps/mapLayers/geoMapLayer.tsx new file mode 100644 index 0000000..15157b2 --- /dev/null +++ b/src/maps/mapLayers/geoMapLayer.tsx @@ -0,0 +1,95 @@ +import React, { useEffect } from "react"; +import { Layer, Source } from "react-map-gl/mapbox-legacy"; +import { FeatureCollection } from "geojson"; + +interface Props { + objectCollection: FeatureCollection; + measurementCollection?: FeatureCollection; + showTitle?: boolean; + setInteractiveLayers: (layers: string[]) => void; +} + +export default function GeoMapLayer(props: Props) { + const { objectCollection, measurementCollection, showTitle, setInteractiveLayers } = props; + + // useEffect(() => { + // console.log(objectCollection); + // console.log(measurementCollection); + // }, [measurementCollection, objectCollection]); + + useEffect(() => { + setInteractiveLayers(["shapefill", "shapeborder", "measurementBorder"]); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + + + + + {showTitle && ( + + )} + + + + + + + ); +} diff --git a/src/maps/mapMarkers/Markers.tsx b/src/maps/mapMarkers/Markers.tsx new file mode 100644 index 0000000..9c1024e --- /dev/null +++ b/src/maps/mapMarkers/Markers.tsx @@ -0,0 +1,247 @@ +import { Box,Grid2 as Grid, Theme, Tooltip, Typography } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import MiniPie from "charts/MiniPie"; +import { useMobile } from "hooks"; +import { clone } from "lodash"; +import { pond } from "protobuf-ts/pond"; +import React, { useCallback, useEffect, useState } from "react"; +import { Marker } from "react-map-gl/mapbox-legacy"; + +//interface for markers +export interface MarkerData { + title: string; //displayed in the tooltip + longitude: number; + latitude: number; + colour: string; + centered?: boolean; + details?: string[]; + markerIcon?: JSX.Element; + subtype?: number; + graphPercent?: number; //if defined show graph behind markericon + customImage?: JSX.Element; //if defined use image rather than basic marker with icon + customSize?: number; //if the size is different from the normal or mini markers + mini?: boolean; //whether the marker is the small scouting marker or the normal size object marker + visibleLevels?: { min?: number; max?: number }; //zoom levels that determine when the marker is visible + clickFunc?: (event: React.PointerEvent, index: number, isMobile: boolean) => void; //function the marker will run when clicked + updateFunc?: (location: pond.Location) => void; //function the marker will run when something changes +} + +interface Props { + markerData: MarkerData[]; + mapZoomLevel: number; + dragOn?: boolean; + groupSelect?: boolean; + showMarkerTitle?: boolean; + displayDetails?: boolean; +} + +const useStyles = makeStyles((theme: Theme) => ({ + pin: { + borderRadius: "50rem", + display: "inline-block", + borderBottomRightRadius: "0", + transform: "rotate(45deg)", + cursor: "pointer", + boxShadow: "4px 4px 10px black" + }, + details: { + transform: "rotate(45deg)", + background: theme.palette.background.default, + opacity: 0.75, + padding: 5, + borderRadius: 10, + paddingLeft: 40, + clipPath: "polygon(25% 0, 100% 0%, 100% 100%, 25% 100%, 0% 50%)" + }, + titleDisplay: { + textAlign: "center", + width: 100, + marginLeft: -25, + marginTop: 10, + backgroundColor: theme.palette.background.default, + borderRadius: 30, + boxShadow: "4px 4px 10px black" + } +})); + +export default function Markers(props: Props) { + const { markerData, dragOn, mapZoomLevel, displayDetails, showMarkerTitle, groupSelect } = props; + const classes = useStyles(); + const isMobile = useMobile(); + const [groupedMarkers, setGroupedMarkers] = useState>(new Map()); + + const moveGroup = (oldLong: number, oldLat: number, newLong: number, newLat: number) => { + //find the diff of the oldLong (the starting longitude of the marker that was actually dragged) and the newLong + const diffLong = newLong - oldLong; + //find the diff of the oldLat and new Lat + const diffLat = newLat - oldLat; + + let gm = clone(groupedMarkers); + + gm.forEach(marker => { + if (marker.updateFunc) { + marker.longitude = marker.longitude + diffLong; + marker.latitude = marker.latitude + diffLat; + marker.updateFunc( + pond.Location.create({ longitude: marker.longitude, latitude: marker.latitude }) + ); + } + }); + setGroupedMarkers(gm); + }; + + //when group select is turned off, clear the grouped markers + useEffect(() => { + if (!groupSelect) { + setGroupedMarkers(new Map()); + } + }, [groupSelect]); + + const renderMarkers = useCallback(() => { + let markerList: JSX.Element[] = []; + markerData.forEach((marker, index) => { + let markerSize = marker.mini ? 20 : 50; + if (marker.customSize) { + markerSize = marker.customSize; + } + + let yOff = 0; + if (!marker.centered) { + yOff = -markerSize * 0.7; + } + + let vis = "block"; + if (marker.visibleLevels) { + let max = marker.visibleLevels.max; + let min = marker.visibleLevels.min; + if ((max && mapZoomLevel > max) || (min && mapZoomLevel < min)) { + vis = "none"; + } + } + + let gm = groupedMarkers.get(index); + if (gm) { + marker.longitude = gm.longitude; + marker.latitude = gm.latitude; + } + + let border = marker.customImage ? "" : "1px solid white"; + if (gm) { + border = "2px solid black"; + } + + let blah: JSX.Element = ( + { + let newLong = e.lngLat.lng; + let newLat = e.lngLat.lat; + if (groupSelect && groupedMarkers.size > 0) { + moveGroup(marker.longitude, marker.latitude, newLong, newLat); + } else { + if (marker.updateFunc) { + marker.longitude = newLong; + marker.latitude = newLat; + marker.updateFunc( + pond.Location.create({ latitude: e.lngLat.lat, longitude: e.lngLat.lng }) + ); + } + } + }}> + + + { + if (!dragOn && marker.clickFunc) { + marker.clickFunc(e, index, isMobile); + } else if (groupSelect) { + let group = clone(groupedMarkers); + group.set(index, marker); + setGroupedMarkers(group); + } + }}> + {marker.graphPercent !== undefined && ( + + + + )} + {marker.customImage ? ( + marker.customImage + ) : ( + + {marker.markerIcon} + + )} + {marker.details && displayDetails && ( + + + {marker.details.map((detail, i) => ( + + + {detail} + + + ))} + + + )} + + + {showMarkerTitle && {marker.title}} + + ); + markerList.push({blah}); + }); + return markerList; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + markerData, + classes, + displayDetails, + dragOn, + groupSelect, + groupedMarkers, + isMobile, + showMarkerTitle, + mapZoomLevel + ]); + + return {renderMarkers()}; +} diff --git a/src/maps/mapMarkers/fieldMarkers/FieldMarkerSettings.tsx b/src/maps/mapMarkers/fieldMarkers/FieldMarkerSettings.tsx new file mode 100644 index 0000000..37eb394 --- /dev/null +++ b/src/maps/mapMarkers/fieldMarkers/FieldMarkerSettings.tsx @@ -0,0 +1,213 @@ +import { + Box, + Button, + DialogActions, + DialogContent, + DialogTitle, + Grid2 as Grid, + MenuItem, + Slider, + TextField + } from "@mui/material"; + import ResponsiveDialog from "common/ResponsiveDialog"; + import { FieldMarker } from "models"; + import { pond } from "protobuf-ts/pond"; + import { useFieldMarkerAPI, useSnackbar } from "providers"; + import { useState, useEffect } from "react"; + + interface Props { + open: boolean; + close: (refresh?: boolean) => void; + longitude?: number; + latitude?: number; + fieldMarker?: FieldMarker; + } + + const fieldMarkerTypes = [ + { + value: pond.FieldMarkerType.FIELD_MARKER_TYPE_OTHER, + label: "Other" + }, + { + value: pond.FieldMarkerType.FIELD_MARKER_TYPE_WEEDS, + label: "Weeds" + }, + { + value: pond.FieldMarkerType.FIELD_MARKER_TYPE_DISEASE, + label: "Disease" + }, + { + value: pond.FieldMarkerType.FIELD_MARKER_TYPE_PESTS, + label: "Pests" + }, + { + value: pond.FieldMarkerType.FIELD_MARKER_TYPE_ROCKS, + label: "Rocks" + } + ]; + + export default function FieldMarkerSettings(props: Props) { + const { open, close, fieldMarker, longitude, latitude } = props; + const fieldMarkerAPI = useFieldMarkerAPI(); + const { openSnack } = useSnackbar(); + const [title, setTitle] = useState("Field Marker"); + const [type, setType] = useState( + pond.FieldMarkerType.FIELD_MARKER_TYPE_OTHER + ); + const [severity, setSeverity] = useState(0); + const [description, setDescription] = useState(""); + + useEffect(() => { + if (fieldMarker) { + setTitle(fieldMarker.name()); + setType(fieldMarker.type()); + setSeverity(fieldMarker.severity()); + setDescription(fieldMarker.description()); + } + }, [fieldMarker]); + + const saveNewFieldMarker = () => { + let newFM: FieldMarker = FieldMarker.create(); + newFM.settings.name = title; + newFM.settings.type = type; + newFM.settings.description = description; + newFM.settings.latitude = latitude ?? 0; + newFM.settings.longitude = longitude ?? 0; + newFM.settings.severity = severity; + //newFM.settings.affectedArea = fmAffectedArea; + fieldMarkerAPI + .addFieldMarker(newFM.settings) + .then(resp => { + openSnack("New marker has been saved"); + close(true); + }) + .catch(err => openSnack("There was a problem saving your marker")); + }; + + const updateFieldMarker = (fieldMarker: FieldMarker) => { + let fm = fieldMarker; + fm.settings.description = description; + fm.settings.severity = severity; + //fm.settings.affectedArea = fmAffectedArea; + fieldMarkerAPI + .updateFieldMarker(fm.key(), fm.settings) + .then(resp => { + openSnack("Updated Field Marker"); + close(true); + }) + .catch(resp => { + openSnack("Failed to update Field Marker"); + }); + }; + + const removeFieldMarker = (fieldMarker: FieldMarker) => { + fieldMarkerAPI + .removeFieldMarker(fieldMarker.key()) + .then(resp => { + openSnack("Field Marker Removed"); + close(true); + }) + .catch(() => { + openSnack("Failed to remove Field Marker"); + }); + }; + + return ( + close()} fullScreen={false}> + {fieldMarker ? "Update Marker" : "Place Marker"} + + + setTitle(e.target.value)} + /> + setType(+e.target.value)} + select + disabled={fieldMarker !== undefined}> + {fieldMarkerTypes.map(option => ( + + {option.label} + + ))} + + Severity + setSeverity(val as number)} + /> + {/* Not doing affected area until i figure out how to resize with zoom */} + {/* Affected Area + setFMAffectedArea(val as number)} + /> */} + setDescription(e.target.value)} + /> + + + + + + {fieldMarker !== undefined && ( + + )} + + + + {fieldMarker !== undefined ? ( + + ) : ( + + )} + + + + + ); + } + \ No newline at end of file diff --git a/src/maps/mapObjectControllers/AgMapController.tsx b/src/maps/mapObjectControllers/AgMapController.tsx new file mode 100644 index 0000000..2153084 --- /dev/null +++ b/src/maps/mapObjectControllers/AgMapController.tsx @@ -0,0 +1,1904 @@ +import { + Box, + Button, + DialogActions, + DialogContent, + Grid2 as Grid, + TextField, + Theme, + Typography, + DialogTitle, + IconButton, + Menu, + FormControlLabel, + Checkbox, + Autocomplete + } from "@mui/material"; + import React, { useCallback, useEffect, useRef, useState } from "react"; + import MapBase, { MeasurementData, ViewData } from "../MapBase"; + import ResponsiveDialog from "common/ResponsiveDialog"; + import { useMobile, useSnackbar } from "hooks"; + import { + useGlobalState, + useFieldAPI, + useBinYardAPI, + useBinAPI, + useFieldMarkerAPI, + useDeviceAPI, + useGrainBagAPI + } from "providers"; + import { pond } from "protobuf-ts/pond"; + import { Bin as IBin, Field, FieldMarker, Device as DeviceModel } from "models"; + import { Edit } from "@mui/icons-material"; + //import { Autocomplete } from "@material-ui/lab"; + import BinSettings from "bin/BinSettings"; + import FieldMarkerSettings from "../mapMarkers/fieldMarkers/FieldMarkerSettings"; + import { GrainBag as BagModel } from "models/GrainBag"; + import { MarkerData } from "../mapMarkers/Markers"; + import BinYardDrawer from "../mapDrawers/BinYardDrawer"; + import { clone } from "lodash"; + import DeviceDrawer from "../mapDrawers/DeviceDrawer"; + import WeedsIcon from "products/AgIcons/WeedsIcon"; + import PestsIcon from "products/AgIcons/PestsIcon"; + import RocksIcon from "products/AgIcons/RocksIcon"; + import DiseaseIcon from "products/AgIcons/DiseaseIcon"; + import BinsIcon from "products/Bindapt/BinsIcon"; + import BindaptIcon from "products/Bindapt/BindaptIcon"; + import binTop from "assets/products/Ag/binTop.png"; + import BinDrawer from "../mapDrawers/BinDrawer"; + import GrainBagDrawer from "../mapDrawers/GrainBagDrawer"; + import AgMapTools from "../mapObjectTools/agMapTools"; + import FieldDrawer from "../mapDrawers/FieldDrawer"; + import { coordDistance, shapeFromCoords } from "models/GeometryMapping"; + //import FieldSettings from "field/FieldSettings"; + import GrainBagSettings from "grainBag/grainBagSettings"; +import { makeStyles } from "@mui/styles"; +import { Result } from "@mapbox/mapbox-gl-geocoder"; + // import { useJohnDeereProxyAPI } from "providers/pond/johnDeereProxyAPI"; + // import { useCNHiProxyAPI } from "providers/pond/cnhiProxyAPI"; + + const useStyles = makeStyles((theme: Theme) => ({ + map: { + height: "100%" + }, + cycleButton: { + height: 25, + width: 25, + marginLeft: 10, + marginRight: 10, + //position: "absolute", + backgroundColor: theme.palette.background.default, + opacity: 0.8 + } + })); + + interface Props { + startingView?: ViewData; + } + + export default function AgMapController(props: Props) { + const transDuration = 3000; + const [{ user, as }] = useGlobalState(); + const [currentView, setCurrentView] = useState( + props.startingView + ? props.startingView + : { + latitude: 52.1579, + longitude: -106.6702, + zoom: user.settings.mapZoom + } + ); + + const classes = useStyles(); + const [fieldDrawer, setFieldDrawer] = useState(false); + const [update, setUpdate] = useState(false); + const isMobile = useMobile(); + const userId = user.id(); + const fieldAPI = useFieldAPI(); + //const johnDeereAPI = useJohnDeereProxyAPI(); + //const cnhiAPI = useCNHiProxyAPI(); + const { openSnack } = useSnackbar(); + const grainBagAPI = useGrainBagAPI(); + const binYardAPI = useBinYardAPI(); + const [yardOptions, setYardOptions] = useState([]); + const [markerDialogOpen, setMarkerDialogOpen] = useState(false); + const [newMarkerLong, setNewMarkerLong] = useState(0); + const [newMarkerLat, setNewMarkerLat] = useState(0); + const [markerType, setMarkerType] = useState(0); + const [selectKey, setSelectKey] = useState(""); + const binAPI = useBinAPI(); + const [binOptions, setBinOptions] = useState([]); + const [endTime, setEndTime] = useState(0); + const fieldMarkerAPI = useFieldMarkerAPI(); + const [openFMSettings, setOpenFMSettings] = useState(false); + const deviceAPI = useDeviceAPI(); + const [deviceOptions, setDeviceOptions] = useState([]); + const [binSettingsOpen, setBinSettingsOpen] = useState(false); + const [loading, setLoading] = useState(false); + const [whoAmI, setWhoAmI] = useState(as ?? userId); + const [fields, setFields] = useState>(new Map()); + const [jdFields, setJDFields] = useState>(new Map()); + const [cnhiFields, setCNHIFields] = useState>(new Map()); + const fieldsRef = useRef>(new Map()); + const [devices, setDevices] = useState>(new Map()); + const [bins, setBins] = useState>(new Map()); + const [grainBags, setGrainBags] = useState>(new Map()); + const [bagOptions, setBagOptions] = useState([]); + const [openBagSelect, setOpenBagSelect] = useState(false); + const [openBagSettings, setOpenBagSettings] = useState(false); + const bagCoordinates = useRef<{ start: pond.Coordinates; end: pond.Coordinates } | undefined>(); + const grainBagsRef = useRef>(new Map()); + const [binYards, setBinYards] = useState>( + new Map() + ); + + const [openFieldSettings, setOpenFieldSettings] = useState(false); + const [fieldBorders, setFieldBorders] = useState([]); + + //Data for the stuff to be displayed on the map + const [anchorfieldMarker, setAnchorFieldMarker] = React.useState(null); // to be used for the menu to display when a field mareker is click + const [markerData, setMarkerData] = useState([]); + const [objectKey, setObjectKey] = useState(""); //using this key to replace the marker index variable + const [isNewObject, setIsNewObject] = useState(false); + const [yardMarkerData, setYardMarkerData] = useState>(new Map()); + const [yardDrawer, setYardDrawer] = useState(false); + const [deviceMarkerData, setDeviceMarkerData] = useState>(new Map()); + const [deviceDrawer, setDeviceDrawer] = useState(false); + const [fieldMarkers, setFieldMarkers] = useState>(new Map()); + const [fieldMarkerData, setFieldMarkerData] = useState>(new Map()); + const [fmIndexKey, setFMIndexKey] = useState(0); + const [binMarkerData, setBinMarkerData] = useState>(new Map()); + const [showBinDetails, setShowBinDetails] = useState(false); + const [binDrawer, setBinDrawer] = useState(false); + const [bagMarkerData, setBagMarkerData] = useState>(new Map()); + const bagMarkerRef = useRef(bagMarkerData); + const [bagDrawer, setBagDrawer] = useState(false); + const [geoData, setGeoData] = useState>(new Map()); + const geoRef = useRef(geoData); + const [showFieldTitles, setShowFieldTitles] = useState(false); + const [ignoreFeatures, setIgnoreFeatures] = useState(false); + const [editorMode, setEditorMode] = useState< + "edit" | "drawPolygon" | "drawLine" | "drawPoint" | "none" | "delete" | undefined + >(); + const [objectTypeToDraw, setObjectTypeToDraw] = useState( + pond.ObjectType.OBJECT_TYPE_UNKNOWN + ); + const zoomLevels = { + far: 14, + close: 18 + }; + const [customSearchEntries, setCustomSearchEntries] = useState([]); + const [fieldSearchEntries, setFieldSearchEntries] = useState([]); + const [binSearchEntries, setBinSearchEntries] = useState([]); + const [bagSearchEntries, setBagSearchEntries] = useState([]); + const [deviceSearchEntries, setDeviceSearchEntries] = useState([]); + const [yardSearchEntries, setYardSearchEntries] = useState([]); + const [fieldMarkerSearchEntries, setFieldMarkerSearchEntries] = useState([]); + const [jdFieldSearchEntries, setJDFieldSearchEntries] = useState([]); + const [cnhiFieldSearchEntries, setCNHIFieldSearchEntries] = useState([]); + const [enableFields, setEnableFields] = useState(true); + const [showAdaptiveFields, setShowAdaptiveFields] = useState(true); + const [showJDFields, setShowJDFields] = useState(true); + const [showCNHFields, setShowCNHFields] = useState(true); + const [measurement, setMeasurement] = useState(false); + const [measurementData, setMeasurementData] = useState>(new Map()); + const measurementRef = useRef(measurementData); + + //watches for changes to the viewing as and sets the load boolean to trigger a load + useEffect(() => { + if (as !== whoAmI) { + setWhoAmI(as); + setLoading(false); + } + }, [as, whoAmI]); + + const closeDrawers = () => { + setFieldDrawer(false); + setYardDrawer(false); + setDeviceDrawer(false); + setBinDrawer(false); + setBagDrawer(false); + }; + + const markerClick = (key: string, long: number, lat: number, isMobile: boolean) => { + closeDrawers(); + clickDelay(); + setIgnoreFeatures(true); + setObjectKey(key); + moveMap(lat, long, zoomLevels.close, isMobile); + }; + + /** + * used to load the fields managed by us + */ + const loadFields = useCallback(() => { + fieldAPI + .listFields(500, 0, "asc", "fieldName", undefined, as) + .then(resp => { + let fields: Map = new Map(); + let fieldEntries: Result[] = []; + resp.data.fields.forEach(f => { + let field = Field.any(f); + fields.set(field.key(), field); + fieldEntries.push({ + id: field.key(), + place_name: field.name(), + place_type: ["field"], + center: [field.center().longitude, field.center().latitude], + address: "", + type: "Feature", + text: field.name(), + bbox: [0,0,0,0], + geometry: {type: "Point", coordinates: [field.center().longitude, field.center().latitude]}, + context: [], + properties: {}, + relevance: 0 + }); + }); + setFields(fields); + setFieldSearchEntries(fieldEntries); + }) + .catch(err => { + openSnack("Failed to load Field Mapping"); + }); + }, [fieldAPI, as, openSnack]); + + /** + * used to load the fields managed by John Deere + */ + // const loadJDFields = useCallback(() => { + // johnDeereAPI + // .listFields(100, 0, as) + // .then(resp => { + // let fields: Map = new Map(); + // let fieldEntries: GeocoderObject[] = []; + // if (resp.data.fields) { + // resp.data.fields.forEach(f => { + // let field = Field.any(f); + // fields.set(field.key(), field); + // fieldEntries.push({ + // id: field.key(), + // place_name: field.name(), + // place_type: ["field"], + // center: [field.center().longitude, field.center().latitude] + // }); + // }); + // } + // setJDFields(fields); + // setJDFieldSearchEntries(fieldEntries); + // }) + // .catch(err => { + // openSnack("Failed to load JD Field Mappings"); + // }); + // }, [johnDeereAPI, as, openSnack]); + + /** + * used to load the fields managed by John Deere + */ + // const loadCNHIFields = useCallback(() => { + // cnhiAPI + // .listFields(100, 0, as) + // .then(resp => { + // let fields: Map = new Map(); + // let fieldEntries: GeocoderObject[] = []; + // if (resp.data.fields) { + // resp.data.fields.forEach(f => { + // let field = Field.any(f); + // fields.set(field.key(), field); + // fieldEntries.push({ + // id: field.key(), + // place_name: field.name(), + // place_type: ["field"], + // center: [field.center().longitude, field.center().latitude] + // }); + // }); + // } + + // setCNHIFields(fields); + // setCNHIFieldSearchEntries(fieldEntries); + // }) + // .catch(err => { + // openSnack("Failed to load CNHI Field Mappings"); + // }); + // }, [cnhiAPI, as, openSnack]); + + const loadGrainBags = useCallback(() => { + grainBagAPI + .listGrainBags(100, 0) + .then(resp => { + let bags: Map = new Map(); + let bagOp: BagModel[] = []; + let bagMarkerData: Map = new Map(); + let bagEntries: Result[] = []; + resp.data.grainBags.forEach(bag => { + let b = BagModel.create(bag); + bags.set(b.key(), b); + if (bag.settings) { + if (!b.settings.startLocation?.longitude && !b.settings.endLocation?.longitude) { + bagOp.push(b); + } else { + //build the data for the marker + //TODO-CS: think about making the creation of marker data a function in the object model + let mData: MarkerData = { + title: b.name(), + longitude: b.centerLocation().longitude, + latitude: b.centerLocation().latitude, + visibleLevels: { max: 13 }, + colour: b.settings.theme?.color ?? "white", + clickFunc: (e, i, isMobile) => { + clickDelay(); + closeDrawers(); + setBagDrawer(true); + setIgnoreFeatures(true); + setObjectKey(b.key); + moveMap( + b.centerLocation().latitude, + b.centerLocation().longitude, + zoomLevels.far, + isMobile + ); + } + }; + bagMarkerData.set(b.key(), mData); + bagEntries.push({ + center: [b.centerLocation().longitude, b.centerLocation().latitude], + id: b.key(), + place_name: b.name(), + place_type: ["grainBag"], + address: "", + bbox: [0,0,0,0], + context: [], + geometry: {type: "Point", coordinates: [b.centerLocation().longitude, b.centerLocation().latitude]}, + properties: {}, + relevance: 0, + text: "", + type: "Feature" + }); + } + } + }); + setBagOptions(bagOp); + setGrainBags(bags); + grainBagsRef.current = bags; + setBagMarkerData(bagMarkerData); + setBagSearchEntries(bagEntries); + bagMarkerRef.current = bagMarkerData; + }) + .catch(err => { + openSnack("Failed to load Grain Bags"); + }); + }, [grainBagAPI, openSnack]); // eslint-disable-line react-hooks/exhaustive-deps + + const loadYards = useCallback(() => { + let yardMap: Map = new Map(); + let yardOps: pond.BinYardSettings[] = []; + let newYardMarkers: Map = new Map(); + let yardEntries: Result[] = []; + + binYardAPI + .listBinYards(400, 0, "asc") + .then(resp => { + resp.data.yard.forEach(yard => { + if (yard.settings) { + yardMap.set(yard.settings.key, yard.settings); + if ( + (!yard.settings.latitude && !yard.settings.longitude) || + (yard.settings.latitude === 0 && yard.settings.longitude === 0) + ) { + yardOps.push(yard.settings); + } else { + let mData: MarkerData = { + title: yard.settings.name, + longitude: yard.settings.longitude, + latitude: yard.settings.latitude, + colour: yard.settings.theme?.color ?? "green", + markerIcon: , + visibleLevels: { max: 17 }, + clickFunc: (e, i, isMobile) => { + if (yard.settings) { + clickDelay(); + closeDrawers(); + setYardDrawer(true); + setIgnoreFeatures(true); + setObjectKey(yard.settings.key); + moveMap( + yard.settings.latitude, + yard.settings.longitude, + zoomLevels.close, + isMobile + ); + } + }, + updateFunc: location => { + if (yard.settings) { + updateYardMarkers(yard.settings, location.longitude, location.latitude); + } + } + }; + newYardMarkers.set(yard.settings.key, mData); + yardEntries.push({ + center: [yard.settings.longitude, yard.settings.latitude], + id: yard.settings.key, + place_name: yard.settings.name, + place_type: ["binYard"] + } as Result); + } + } + }); + setYardMarkerData(newYardMarkers); + setYardSearchEntries(yardEntries); + setBinYards(yardMap); + setYardOptions(yardOps); + }) + .catch(() => { + openSnack("Failed to load yard data"); + }); + }, [binYardAPI, openSnack]); // eslint-disable-line react-hooks/exhaustive-deps + + const loadBins = useCallback(() => { + binAPI + .listBins(2000, 0, "asc", "name", undefined, as) + .then(resp => { + let binMap: Map = new Map(); + let markerData: Map = new Map(); + let searchEntries: Result[] = []; + let binOp: IBin[] = []; + resp.data.bins.forEach(bin => { + let b = IBin.create(bin); + binMap.set(b.key(), b); + if (b.settings) { + if (!b.settings.location?.longitude && !b.settings.location?.latitude) { + binOp.push(b); + } else { + let marker: MarkerData = b.getMarkerData( + (e, i, isMobile) => { + if (b.settings.location?.longitude && b.settings.location?.latitude) { + markerClick( + b.key(), + b.settings.location.longitude, + b.settings.location.latitude, + isMobile + ); + setBinDrawer(true); + } + }, + location => { + updateBinMarkers(b, location.longitude, location.latitude); + } + ); + marker.customImage = ( + binTopImage + ); + markerData.set(b.key(), marker); + searchEntries.push({ + id: b.key(), + center: [b.settings.location.longitude, b.settings.location.latitude], + place_name: b.name(), + place_type: ["bin"] + } as Result); + } + } + }); + setBinMarkerData(markerData); + setBinSearchEntries(searchEntries); + setBins(binMap); + setBinOptions(binOp); + }) + .catch(err => { + openSnack("Failed to load Bin data"); + }); + }, [binAPI, as, openSnack]); // eslint-disable-line react-hooks/exhaustive-deps + + const loadDevices = useCallback(() => { + deviceAPI + .list(500, 0, "asc", "name") + .then(resp => { + let map = new Map(); + let newDevMarkers: Map = new Map(); + let searchEntries: Result[] = []; + + let options: DeviceModel[] = []; + resp.data.devices.forEach((device, i) => { + //add device to the main map + let d = DeviceModel.any(device); + map.set(d.id().toString(), d); + //check to make sure the settings are there + if (d.settings) { + let long = d.settings.longitude; + let lat = d.settings.latitude; + //if location data is undefined or 0/0 make it an option to be mapped + if ((!long && !lat) || (long === 0 && lat === 0)) { + let dOption: DeviceModel = d; + options.push(dOption); + //otherwise make a new marker for the device + } else { + let marker: MarkerData = d.getMarkerData( + (e, i, isMobile) => { + markerClick( + d.id().toString(), + d.location().longitude, + d.location().latitude, + isMobile + ); + setDeviceDrawer(true); + }, + location => { + updateDeviceMarkers(d, location.longitude, location.latitude); + } + ); + marker.markerIcon = ; + newDevMarkers.set(d.id().toString(), marker); + searchEntries.push({ + center: [d.location().longitude, d.location().latitude], + id: d.id().toString(), + place_name: d.name(), + place_type: ["device"] + } as Result); + } + } + }); + setDevices(map); + setDeviceOptions(options); + setDeviceMarkerData(newDevMarkers); + setDeviceSearchEntries(searchEntries); + }) + .catch(() => { + openSnack("Failed to load Devices"); + }); + }, [deviceAPI, openSnack]); // eslint-disable-line react-hooks/exhaustive-deps + + const fmIcon = (type: pond.FieldMarkerType, size: number) => { + switch (type) { + case pond.FieldMarkerType.FIELD_MARKER_TYPE_PESTS: + return ; + case pond.FieldMarkerType.FIELD_MARKER_TYPE_ROCKS: + return ; + case pond.FieldMarkerType.FIELD_MARKER_TYPE_DISEASE: + return ; + case pond.FieldMarkerType.FIELD_MARKER_TYPE_WEEDS: + return ; + } + }; + + const nextFM = (fm: FieldMarker) => { + let arr = Array.from(fieldMarkers.values()); + let i = arr.indexOf(fm); + let domKey = "marker-" + fmIndexKey; + let newID = fmIndexKey; + let newKey = ""; + if (i === arr.length - 1) { + newID = fmIndexKey - (arr.length - 1); + //domKey = "marker-" + newID; + newKey = arr[0].key(); + } else { + newID = fmIndexKey + 1; + //domKey = "marker-" + newID; + newKey = arr[i + 1].key(); + } + let newFM = fieldMarkers.get(newKey); + if (newFM) { + setCurrentView({ + latitude: newFM.lat(), + longitude: newFM.long(), + zoom: 12, + transitionDuration: 1000 + }); + } + setObjectKey(newKey); + setFMIndexKey(newID); + //setting the anchor to the marker that is in the center (marker you are moving from) so that the menu stays in the correct position + setAnchorFieldMarker(document.getElementById(domKey)); + }; + + const prevFM = (fm: FieldMarker) => { + let arr = Array.from(fieldMarkers.values()); + let i = arr.indexOf(fm); + let domKey = "marker-" + fmIndexKey; + let newID = fmIndexKey; + let newKey = ""; + if (i === 0) { + newID = fmIndexKey + (arr.length - 1); + //domKey = "marker-" + newID; + newKey = arr[arr.length - 1].key(); + } else { + newID = fmIndexKey - 1; + //domKey = "marker-" + newID; + newKey = arr[i - 1].key(); + } + let newFM = fieldMarkers.get(newKey); + if (newFM) { + setCurrentView({ + latitude: newFM.lat(), + longitude: newFM.long(), + zoom: 12, + transitionDuration: 1000 + }); + } + setObjectKey(newKey); + setFMIndexKey(newID); + //setting the anchor to the marker that is in the center (marker you are moving from) so that the menu stays in the correct position (center of the map) + setAnchorFieldMarker(document.getElementById(domKey)); + }; + + //uses the anchor to show a menu at the active marker, not really good for dynamically moving through the markers so I do want to think of a way to use + //react-map-gl Popup components + const fieldMarkerMenu = () => { + let fm = fieldMarkers.get(objectKey); + return ( + { + if (new Date().valueOf() > endTime) { + setAnchorFieldMarker(null); + setObjectKey(""); + } + }} + anchorOrigin={{ vertical: "top", horizontal: "center" }} + transformOrigin={{ vertical: "top", horizontal: "center" }} + //style={{marginTop: -5}} + PaperProps={{ + style: { background: "none" } + }}> + + { + if (fm) { + prevFM(fm); + } + }}> + {"<"} + + { + if (fm) { + nextFM(fm); + } + }}> + {">"} + + + + + + + {fm && fmIcon(fm.type(), 30)} + + {fm && fm.typeString()} + + + + + + + + {fm && fm.name()} + + + + {fm && fm.description()} + + + + + + + + + + ); + }; + + const loadFieldMarkers = useCallback(() => { + fieldMarkerAPI + .listFieldMarkers(500, 0, "asc", "key", undefined, undefined, as) + .then(resp => { + let fmMap: Map = new Map(); + let markerData: Map = new Map(); + let searchEntries: Result[] = []; + resp.data.fieldMarker.forEach(fieldMarker => { + let fm = FieldMarker.create(fieldMarker); + fmMap.set(fm.key(), fm); + markerData.set(fm.key(), { + title: fm.name(), + longitude: fm.long(), + latitude: fm.lat(), + colour: fm.defaultColor(), + markerIcon: fmIcon(fm.type(), 20 * 0.6), + subtype: fm.type(), + mini: true, + clickFunc: (event, index) => { + clickDelay(); + closeDrawers(); + setIgnoreFeatures(true); + setCurrentView({ + latitude: fm.lat(), + longitude: fm.long(), + zoom: 12, + transitionDuration: 0 + //transitionInterpolator: new FlyToInterpolator() + }); + setAnchorFieldMarker(event.currentTarget); + setFMIndexKey(index); + setObjectKey(fm.key()); + } + }); + searchEntries.push({ + center: [fm.long(), fm.lat()], + id: fm.key(), + place_name: fm.name(), + place_type: ["fieldMarker"] + } as Result); + }); + setFieldMarkers(fmMap); + setFieldMarkerData(markerData); + setFieldMarkerSearchEntries(searchEntries); + }) + .catch(() => { + openSnack("Failed to load Scouting Markers"); + }); + }, [fieldMarkerAPI, openSnack, as]); + + useEffect(() => { + if (!loading) { + setLoading(true); + loadFields(); + //loadJDFields(); + //loadCNHIFields(); + loadBins(); + loadYards(); + loadDevices(); + loadFieldMarkers(); + loadGrainBags(); + } + }, [ + as, + loadBins, + loadYards, + loadDevices, + loadFieldMarkers, + loadFields, + //loadJDFields, + //loadCNHIFields, + loadGrainBags, + loading + ]); + + //combine the different object markerdata into a single array to pass into the map + useEffect(() => { + let yArray = Array.from(yardMarkerData.values()); + let dArray = Array.from(deviceMarkerData.values()); + let fmArray = Array.from(fieldMarkerData.values()); + let bArray = Array.from(binMarkerData.values()); + let bagArray = Array.from(bagMarkerData.values()); + let md = yArray.concat(dArray, fmArray, bArray, bagArray); + setMarkerData(md); + }, [ + yardMarkerData, + deviceMarkerData, + fieldMarkerData, + binMarkerData, + bagMarkerData + //fieldSearchEntries + ]); + + //combine the seperate objects search entries into a single array to pass into the map + useEffect(() => { + setCustomSearchEntries( + fieldSearchEntries.concat( + binSearchEntries, + bagSearchEntries, + yardSearchEntries, + deviceSearchEntries, + fieldMarkerSearchEntries, + jdFieldSearchEntries, + cnhiFieldSearchEntries + ) + ); + }, [ + fieldSearchEntries, + binSearchEntries, + bagSearchEntries, + yardSearchEntries, + deviceSearchEntries, + fieldMarkerSearchEntries, + jdFieldSearchEntries, + cnhiFieldSearchEntries + ]); + + useEffect(() => { + let gd: Map = new Map(); + let currentFields: Map = new Map(); + if (enableFields) { + if (showAdaptiveFields) { + fields.forEach(field => { + if (field.settings.fieldGeoData) { + let geo = field.settings.fieldGeoData; + geo.objectKey = field.settings.key; + geo.title = field.settings.fieldName; + if (!geo.origin) { + geo.origin = pond.DataOrigin.DATA_ORIGIN_ADAPTIVE; + } + gd.set(field.key(), geo); + currentFields.set(field.key(), field); + } + }); + } + if (showJDFields) { + jdFields.forEach(field => { + if (field.settings.fieldGeoData) { + let geo = field.settings.fieldGeoData; + geo.colour = "green"; + geo.objectKey = field.settings.key; + geo.title = field.settings.fieldName; + geo.origin = pond.DataOrigin.DATA_ORIGIN_JOHN_DEERE; + gd.set(field.key(), geo); + currentFields.set(field.key(), field); + } + }); + } + if (showCNHFields) { + cnhiFields.forEach(field => { + if (field.settings.fieldGeoData) { + let geo = field.settings.fieldGeoData; + geo.colour = "green"; + geo.objectKey = field.settings.key; + geo.title = field.settings.fieldName; + geo.origin = pond.DataOrigin.DATA_ORIGIN_CASE_NEW_HOLLAND; + gd.set(field.key(), geo); + currentFields.set(field.key(), field); + } + }); + } + } + + grainBags.forEach(grainbag => { + let bagGeoData: pond.GeoData = pond.GeoData.create({ + colour: "white", + geoShape: "LineString", + objectKey: grainbag.key(), + shapes: [ + pond.Shape.create({ + points: [grainbag.startLocation(), grainbag.endLocation()] + }) + ] + }); + gd.set(grainbag.key(), bagGeoData); + }); + + setGeoData(gd); + //fieldsRef.current = new Map([...fields, ...jdFields, ...cnhiFields]); + fieldsRef.current = currentFields; + geoRef.current = gd; + }, [ + fields, + grainBags, + jdFields, + cnhiFields, + enableFields, + showAdaptiveFields, + showJDFields, + showCNHFields + ]); + + const closeNewMarkerDialog = () => { + if (new Date().valueOf() > endTime) { + setMarkerDialogOpen(false); + setSelectKey(""); + } + }; + + const newMarker = (long: number, lat: number) => { + clickDelay(); + setNewMarkerLong(long); + setNewMarkerLat(lat); + setUpdate(false); + if (markerType === pond.ObjectType.OBJECT_TYPE_FIELDMARKER) { + setOpenFMSettings(true); + } else if (markerType !== pond.ObjectType.OBJECT_TYPE_UNKNOWN) { + if (isNewObject) { + createNewObject(); + } else { + setMarkerDialogOpen(true); + } + } + }; + + const updateYardMarkers = ( + yard: pond.BinYardSettings, + long: number, + lat: number, + newMarker?: boolean + ) => { + yard.longitude = long; + yard.latitude = lat; + binYardAPI + .updateBinYard(yard.key, yard) + .then(resp => { + openSnack("BinYard Location Updated"); + if (yardOptions.includes(yard)) { + yardOptions.splice(yardOptions.indexOf(yard), 1); + } + if (newMarker) { + //make new marker data + let mData: MarkerData = { + title: yard.name, + longitude: yard.longitude, + latitude: yard.latitude, + colour: yard.theme?.color ?? "green", + markerIcon: , + visibleLevels: { max: 17 }, + clickFunc: (e, i, isMobile) => { + if (yard) { + clickDelay(); + closeDrawers(); + setYardDrawer(true); + setIgnoreFeatures(true); + setObjectKey(yard.key); + moveMap(yard.latitude, yard.longitude, zoomLevels.close, isMobile); + } + }, + updateFunc: location => { + if (yard) { + updateYardMarkers(yard, location.longitude, location.latitude); + } + } + }; + //add it into the state and clone then set to force and update + let data = clone(yardMarkerData); + data.set(yard.key, mData); + setYardMarkerData(data); + } + }) + .catch(err => { + openSnack("Could not update BinYard location"); + }); + }; + + const createNewObject = () => { + clickDelay(); + if (markerType === pond.ObjectType.OBJECT_TYPE_BIN) { + setBinSettingsOpen(true); + } + }; + + const updateBinMarkers = (bin: IBin, long: number, lat: number, newMarker?: boolean) => { + bin.setLocation(long, lat); + binAPI + .updateBin(bin.key(), bin.settings) + .then(resp => { + openSnack("Bin Location Updated"); + //remove from options + if (binOptions.includes(bin)) { + binOptions.splice(binOptions.indexOf(bin), 1); + } + if (newMarker) { + //create marker + let marker: MarkerData = bin.getMarkerData( + (e, i, isMobile) => { + if (bin.settings.location?.longitude && bin.settings.location?.latitude) { + markerClick( + bin.key(), + bin.settings.location.longitude, + bin.settings.location.latitude, + isMobile + ); + setBinDrawer(true); + } + }, + location => { + updateBinMarkers(bin, location.longitude, location.latitude); + } + ); + marker.customImage = ( + binTopImage + ); + let data = clone(binMarkerData); + data.set(bin.key(), marker); + setBinMarkerData(data); + } + }) + .catch(err => { + openSnack("Could not update Bin Location"); + }); + }; + + const updateDeviceMarkers = ( + device: DeviceModel, + long: number, + lat: number, + newMarker?: boolean + ) => { + device.settings.longitude = long; + device.settings.latitude = lat; + deviceAPI + .update(device.id(), device.settings) + .then(resp => { + openSnack("Device location Updated"); + if (deviceOptions.includes(device)) { + deviceOptions.splice(deviceOptions.indexOf(device), 1); + } + if (newMarker) { + let marker: MarkerData = device.getMarkerData( + (e, i, isMobile) => { + markerClick( + device.id().toString(), + device.location().longitude, + device.location().latitude, + isMobile + ); + setDeviceDrawer(true); + }, + location => { + updateDeviceMarkers(device, location.longitude, location.latitude); + } + ); + marker.markerIcon = ; + let data = clone(deviceMarkerData); + data.set(device.id().toString(), marker); + setDeviceMarkerData(data); + } + }) + .catch(err => { + openSnack("Could not update Device Location"); + }); + }; + + const validSelection = () => { + switch (markerType) { + case pond.ObjectType.OBJECT_TYPE_BIN: + return bins.get(selectKey) === undefined; + case pond.ObjectType.OBJECT_TYPE_BINYARD: + return binYards.get(selectKey) === undefined; + case pond.ObjectType.OBJECT_TYPE_DEVICE: + return devices.get(selectKey) === undefined; + default: + return false; + } + }; + + const setMarker = () => { + if (markerType === pond.ObjectType.OBJECT_TYPE_BIN) { + let bin = bins.get(selectKey); + bin && updateBinMarkers(bin, newMarkerLong, newMarkerLat, true); + } + if (markerType === pond.ObjectType.OBJECT_TYPE_BINYARD) { + let yard = binYards.get(selectKey); + yard && updateYardMarkers(yard, newMarkerLong, newMarkerLat, true); + } + if (markerType === pond.ObjectType.OBJECT_TYPE_DEVICE) { + let device = devices.get(selectKey); + device && updateDeviceMarkers(device, newMarkerLong, newMarkerLat, true); + } + closeNewMarkerDialog(); + }; + + const moveMap = ( + lat: number, + long: number, + zoom: number, + mobileOffset?: boolean, + center?: boolean + ) => { + let xOff = !mobileOffset ? -250 : undefined; + let yOff = mobileOffset ? -150 : undefined; + + setCurrentView({ + latitude: lat, + longitude: long, + zoom: zoom, + transitionDuration: transDuration, + xOffset: center ? 0 : xOff, + yOffset: center ? 0 : yOff + }); + }; + + const markerDialog = () => { + return ( + + {update ? "Update Marker" : "Place Marker"} + + + {markerType === pond.ObjectType.OBJECT_TYPE_BINYARD && ( + option.name} + fullWidth + onChange={(e, val) => { + val && setSelectKey(val.key); + }} + renderInput={params => } + /> + )} + {markerType === pond.ObjectType.OBJECT_TYPE_BIN && ( + + b.settings.yardKey.localeCompare(a.settings.yardKey) + )} + groupBy={options => { + let yard = binYards.get(options.settings.yardKey); + if (yard) { + return yard.name; + } + return "No Yard"; + }} + getOptionLabel={(option: IBin) => option.name()} + fullWidth + onChange={(e, val) => { + val && setSelectKey(val.key()); + }} + renderInput={params => } + /> + )} + {markerType === pond.ObjectType.OBJECT_TYPE_DEVICE && ( + option.name()} + fullWidth + onChange={(e, val) => { + val && setSelectKey(val.id().toString()); + }} + renderInput={params => } + /> + )} + + + + + + + + + + ); + }; + + //this click delay is implemented to fix an issue with touch screens that caused the drawer to close immediately after opening + const clickDelay = () => { + let endTime = new Date().valueOf() + 1000; + setEndTime(endTime); + }; + + const updateFieldGeo = (key: string, geoData: pond.GeoData) => { + let field = fieldsRef.current.get(key); + if (field) { + let newSettings = field.settings; + newSettings.fieldGeoData = geoData; + fieldAPI + .updateField(key, newSettings) + .then(resp => { + let cloneGeo = clone(geoRef.current); + geoData.objectKey = key; + cloneGeo.set(key, geoData); + geoRef.current = cloneGeo; + setGeoData(cloneGeo); + openSnack("updated fields geographical data"); + }) + .catch(err => { + openSnack("failed to update fields geographical data"); + }); + } + }; + + const removeFieldGeo = (key: string) => { + let field = fieldsRef.current.get(key); + if (field) { + fieldAPI + .removeField(key) + .then(resp => { + let cloneGeo = clone(geoRef.current); + cloneGeo.delete(key); + geoRef.current = cloneGeo; + setGeoData(cloneGeo); + openSnack("field deleted"); + }) + .catch(err => { + openSnack("there was a problem deleting the field"); + }); + } + }; + + const updateMapBags = (newBag: BagModel) => { + //update the grainbags ref + grainBagsRef.current.set(newBag.key(), newBag); + + //update geo data + let newGeo = pond.GeoData.create(); + let start = newBag.startLocation(); + let end = newBag.endLocation(); + let shape = shapeFromCoords([ + [start.longitude, start.latitude], + [end.longitude, end.latitude] + ]); + newGeo.geoShape = "LineString"; + newGeo.colour = "white"; + newGeo.objectKey = newBag.key(); + newGeo.shapes.push(shape); + let data = clone(geoRef.current); + data.set(newBag.key(), newGeo); + setGeoData(data); + geoRef.current = data; + + // update markerdata + let mData: MarkerData = { + title: newBag.name(), + longitude: newBag.centerLocation().longitude, + latitude: newBag.centerLocation().latitude, + colour: newBag.settings.theme?.color ?? "white", + visibleLevels: { max: 14 }, + clickFunc: (e, i, isMobile) => { + closeDrawers(); + setBagDrawer(true); + setIgnoreFeatures(true); + setObjectKey(newBag.key); + moveMap( + newBag.centerLocation().latitude, + newBag.centerLocation().longitude, + zoomLevels.far, + isMobile + ); + } + // updateFunc: key => { + // console.log("updated marker: " + key); + // } + }; + let mClone = clone(bagMarkerRef.current); + mClone.set(newBag.key(), mData); + bagMarkerRef.current = mClone; + setBagMarkerData(mClone); + }; + + const updateBag = (bag: BagModel) => { + //update the bag + grainBagAPI + .updateGrainBag(bag.key(), bag.name(), bag.settings) + .then(resp => { + openSnack("Grain Bag location set"); + //if the update succeeded than update the geo state and ref + if (bag) updateMapBags(bag); + }) + .catch(err => { + openSnack("there was a problem setting the bags location"); + }); + }; + + const setBag = () => { + if (selectKey === "") { + setOpenBagSettings(true); + } else { + let bag = grainBagsRef.current.get(selectKey); + if (bag) { + bag.settings.startLocation = bagCoordinates.current?.start; + bag.settings.endLocation = bagCoordinates.current?.end; + updateBag(bag); + } + setSelectKey(""); + } + setOpenBagSelect(false); + }; + + const bagSelectDialog = () => { + return ( + { + setOpenBagSelect(false); + }}> + Map Grain Bag + + option.name()} + fullWidth + onChange={(e, val) => { + val && setSelectKey(val.key); + }} + renderInput={params => ( + + )} + /> + + + + + + + + + ); + }; + + const geocoderResult = (result: Result) => { + clickDelay() + console.log("result function") + console.log(result) + console.log("immediate id: " + result.id) + setTimeout(() => { + console.log("result.id after waiting 2 seconds: " + result.id) + }, 2000); + if(result.id !== undefined){ + setObjectKey(result.id.toString()); + if (result.place_type.includes("bin")) { + setBinDrawer(true); + } else if (result.place_type.includes("field")) { + setFieldDrawer(true); + } else if (result.place_type.includes("grainBag")) { + setBagDrawer(true); + } else if (result.place_type.includes("device")) { + setDeviceDrawer(true); + } else if (result.place_type.includes("binYard")) { + setYardDrawer(true); + } + } + }; + + const geocoderTransition = (result: Result) => { + let long = result.center ? result.center[0] : 0; + let lat = result.center ? result.center[1] : 0; + if (long && lat) { + let centered = true; + let zoom = zoomLevels.far; + if (result.place_type.includes("field") || result.place_type.includes("grainBag")) { + centered = false; + } else if ( + result.place_type.includes("bin") || + result.place_type.includes("device") || + result.place_type.includes("binYard") + ) { + zoom = zoomLevels.close; + centered = false; + } + moveMap(lat, long, zoom, isMobile, centered); + } + }; + + return ( + + {/* dialogs */} + {markerDialog()} + {fieldMarkerMenu()} + {bagSelectDialog()} + + {/* main map component */} + { + //because this function is technically run in an event handler you need to use refs for state variables + if (type === "Polygon") { + coords.forEach(set => { + let newHole = shapeFromCoords(set); + let fieldsGeo = geoRef.current.get(object); + if (fieldsGeo && fieldsGeo.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE) { + fieldsGeo.holes.push(newHole); + updateFieldGeo(object, fieldsGeo); + } else { + openSnack("Field Boundaries maintained externally"); + } + }); + } + }} + objectTitles={showFieldTitles} + currentView={currentView} + drawMeasurement={measurement} + mapTools={ + { + setShowBinDetails(show); + }} + toggleObjectEditType={setObjectTypeToDraw} + toggleEditorMode={setEditorMode} + toggleFieldTitles={setShowFieldTitles} + toggleIsNewObject={setIsNewObject} + toggleMarkerType={setMarkerType} + toggleMeasurement={setMeasurement} + /> + } + layerOptions={ + +
+ + { + setEnableFields(!enableFields); + }} + /> + } + /> + + + { + setShowAdaptiveFields(!showAdaptiveFields); + }} + /> + } + /> + { + setShowJDFields(!showJDFields); + }} + /> + } + /> + { + setShowCNHFields(!showCNHFields); + }} + /> + } + /> + +
+
+ } + editorMode={editorMode} + editGeoCallback={(object, borders, holes) => { + let field = fieldsRef.current.get(object); + let grainbag = grainBagsRef.current.get(object); + if (field) { + let fieldGeo = field.settings.fieldGeoData; + if (fieldGeo) { + fieldGeo.shapes = borders; + if (holes) { + fieldGeo.holes = holes; + } + updateFieldGeo(object, fieldGeo); + } + } else if (grainbag) { + if (borders.length > 0) { + let line = borders[0]; + if (line.points.length > 0) { + let start = line.points[0]; + let end = line.points[line.points.length - 1]; + grainbag.settings.startLocation = start; + grainbag.settings.endLocation = end; + updateBag(grainbag); + } + } + } + }} + addNewShape={(coords, type) => { + console.log("controller add shape") + console.log(coords) + console.log(type) + if (type === "Polygon") { + //if it is a polygon it is a field (for now until we add more objects that use polygons) + let newShapes: pond.Shape[] = []; + coords.forEach(set => { + newShapes.push(shapeFromCoords(set)); + }); + setFieldBorders(newShapes); + setOpenFieldSettings(true); + } else if (type === "LineString") { + //if it is a line string it is a grain bag + let line = shapeFromCoords(coords); //if it is a grainbag type the points in the line will be limited to 2 while drawing + if (line.points.length > 0) { + let start = line.points[0]; + let end = line.points[line.points.length - 1]; + bagCoordinates.current = { start, end }; + } + setOpenBagSelect(true); + } + }} + addNewMeasurement={(coords, type) => { + let measurements = new Map(measurementRef.current); + if (type === "LineString") { + //determine length of line from the coordinates + let totalLengthKm = 0; + let segmentsKm: number[] = []; + let prevPoint: number[] = []; + let currentPoint: number[] = []; + //loop thorugh the array determine the length of each segment + coords.forEach(pair => { + currentPoint = pair; + if (prevPoint.length === 2) { + let lat1 = prevPoint[0]; + let long1 = prevPoint[1]; + let lat2 = currentPoint[0]; + let long2 = currentPoint[1]; + + let segmentLengthKm = coordDistance(lat1, long1, lat2, long2); + segmentsKm.push(segmentLengthKm); + totalLengthKm = totalLengthKm + segmentLengthKm; + } + prevPoint = pair; + }); + //make a new measurement data + let measurement: MeasurementData = { + key: "measurement" + measurements.size, + coordinates: coords, + geoShape: type, + segmentDistanceKm: segmentsKm, + totalDistanceKm: totalLengthKm + }; + //add to the list of measurements + measurements.set(measurement.key, measurement); + measurementRef.current = measurements; + setMeasurementData(measurements); + } + }} + objectGeoData={geoData} + measurementData={measurementData} + mapClick={event => { + if (markerType !== pond.ObjectType.OBJECT_TYPE_UNKNOWN) { + newMarker(event.lngLat.lng, event.lngLat.lat); + } else if (editorMode === "delete") { + if (event.features) { + let selected = event.features[0]; + if (selected && selected.properties && selected.properties.objectKey) { + removeFieldGeo(selected.properties.objectKey); + } + } + } else if (editorMode === "none" || !editorMode) { + //if no modes are active it was just a click on the map + if (event.features && !ignoreFeatures) { + let selected = event.features[0]; + if (selected && selected.properties && selected.properties.objectKey) { + let center = pond.Location.create({ + latitude: currentView.latitude, + longitude: currentView.longitude + }); + setObjectKey(selected.properties.objectKey); + closeDrawers(); + let bag = grainBagsRef.current.get(selected.properties.objectKey); + let field = fieldsRef.current.get(selected.properties.objectKey); + if (bag) { + center = bag.centerLocation(); + setBagDrawer(true); + } else if (field) { + center = field.center(); + setFieldDrawer(true); + } + moveMap(center.latitude, center.longitude, zoomLevels.far, isMobile); + } + } else { + //if the feature was ignored that means they clicked on a marker so can set the ignore back to false during the click so that the user + //can still click on a "feature" (field) while the drawer is still open + setIgnoreFeatures(false); + } + } + }} + ignoreHomeLoad={props.startingView && true} + markerData={markerData} + displayMarkerDetails={showBinDetails} + /> + + {/* new objectDrawers - consider making an ag drawers file to contain these components to shirnk this file down further*/} + {objectKey !== "" && ( + + { + if (new Date().valueOf() > endTime) { + setYardDrawer(false); + setObjectKey(""); + } + }} + selectedYard={objectKey} + yards={binYards} + removeMarker={key => { + if (new Date().valueOf() > endTime) { + //use the key to remove it from the map of yard markers which should cause the markerData array to update + let newMarkers = clone(yardMarkerData); + newMarkers.delete(key); + setYardMarkerData(newMarkers); + //adds it back to the options + let yard = binYards.get(key); + if (yard) { + yardOptions.push(yard); + } + } + }} + updateMarker={(key, objectSettings) => { + if (new Date().valueOf() > endTime) { + let newMarkers = clone(yardMarkerData); + let toupdate = newMarkers.get(key); + if (toupdate) { + if (objectSettings.theme) { + toupdate.colour = objectSettings.theme.color; + } + } + setYardMarkerData(newMarkers); + } + }} + moveMap={(long, lat) => { + moveMap(lat, long, zoomLevels.close, isMobile); + }} + /> + { + if (new Date().valueOf() > endTime) { + setDeviceDrawer(false); + setObjectKey(""); + } + }} + selectedDevice={objectKey} + devices={devices} + removeMarker={key => { + if (new Date().valueOf() > endTime) { + //use the key to remove it from the map of yard markers which should cause the markerData array to update + let newMarkers = clone(deviceMarkerData); + newMarkers.delete(key); + setDeviceMarkerData(newMarkers); + + //adds it back to the options + let dev = devices.get(key); + if (dev) { + deviceOptions.push(dev); + } + } + }} + updateMarker={(key, objectSettings) => { + if (new Date().valueOf() > endTime) { + let newMarkers = clone(deviceMarkerData); + let toupdate = newMarkers.get(key); + if (toupdate) { + if (objectSettings.theme) { + toupdate.colour = objectSettings.theme.color; + } + } + setDeviceMarkerData(newMarkers); + } + }} + moveMap={(long, lat) => { + moveMap(lat, long, zoomLevels.far, isMobile); + }} + /> + { + if (new Date().valueOf() > endTime) { + setBinDrawer(false); + setObjectKey(""); + } + }} + selectedBin={objectKey} + bins={bins} + removeMarker={key => { + if (new Date().valueOf() > endTime) { + let newMarkers = clone(binMarkerData); + newMarkers.delete(key); + setBinMarkerData(newMarkers); + //adds it back to the options + let bin = bins.get(key); + if (bin) { + binOptions.push(bin); + } + } + }} + updateMarker={(key, objectSettings) => { + if (new Date().valueOf() > endTime) { + let newMarkers = clone(binMarkerData); + let toupdate = newMarkers.get(key); + if (toupdate) { + if (objectSettings.theme) { + toupdate.customSize = objectSettings.theme.height; + //toupdate.colour = objectSettings.theme.color; + } + } + setBinMarkerData(newMarkers); + } + }} + moveMap={(long, lat) => { + moveMap(lat, long, zoomLevels.close, isMobile); + }} + /> + { + if (new Date().valueOf() > endTime) { + setBagDrawer(false); + setObjectKey(""); + } + }} + grainBags={grainBagsRef.current} + selectedBag={objectKey} + removeMarker={key => { + if (new Date().valueOf() > endTime) { + //remove the line from the map + let geo = clone(geoRef.current); + geo.delete(key); + geoRef.current = geo; + setGeoData(geo); + + //removes the marker from the map + let newMarkers = clone(bagMarkerRef.current); + newMarkers.delete(key); + bagMarkerRef.current = newMarkers; + setBagMarkerData(newMarkers); + + //adds it back to the options + let bag = grainBags.get(key); + if (bag) { + bagOptions.push(bag); + } + } + }} + updateMarker={(key, objectSettings) => { + if (new Date().valueOf() > endTime) { + let newMarkers = clone(bagMarkerData); + let toupdate = newMarkers.get(key); + if (toupdate) { + if (objectSettings.theme) { + toupdate.colour = objectSettings.theme.color; + } + } + setBinMarkerData(newMarkers); + } + }} + moveMap={(long, lat) => { + moveMap(lat, long, zoomLevels.far, isMobile); + }} + /> + { + setFieldDrawer(false); + setObjectKey(""); + }} + openSettings={fieldKey => { + setObjectKey(fieldKey); + setOpenFieldSettings(true); + }} + fields={fieldsRef.current} + selectedFieldKey={objectKey} + moveMap={(long, lat) => { + moveMap(lat, long, zoomLevels.far, isMobile); + }} + /> + + )} + + {/* settings components for editing objects from the map */} + { + if (new Date().valueOf() > endTime) { + setBinSettingsOpen(false); + } + if (refresh) { + loadBins(); + } + }} + canEdit={true} + userID={user.id()} + coords={{ longitude: newMarkerLong, latitude: newMarkerLat }} + binYards={Array.from(binYards.values())} + /> + { + if (new Date().valueOf() > endTime) { + if (refresh) { + //TODO: possibly change this to just modify the array + loadFieldMarkers(); + } + setOpenFMSettings(false); + } + }} + fieldMarker={fieldMarkers.get(objectKey)} + longitude={newMarkerLong} + latitude={newMarkerLat} + /> + {/* { + let data = clone(geoData); + settings.key = key; + if (settings.fieldGeoData) { + data.set( + key, + pond.GeoData.create({ + colour: settings.fieldGeoData.colour, + geoShape: "Polygon", + shapes: settings.fieldGeoData.shapes, + holes: settings.fieldGeoData.holes, + objectKey: key, + title: settings.fieldName, + origin: settings.fieldGeoData.origin + }) + ); + geoRef.current = data; + setGeoData(data); + + fieldsRef.current.set(key, Field.create(pond.Field.create({ settings: settings }))); + } + }} + removeField={field => { + let data = clone(geoData); + data.delete(field.key()); + geoRef.current = data; + setGeoData(data); + fieldsRef.current.delete(field.key()); + setFieldDrawer(false); + setObjectKey(""); + }} + open={openFieldSettings} + onClose={() => { + setOpenFieldSettings(false); + }} + borders={fieldBorders} + /> */} + { + if (newBag) { + updateMapBags(newBag); + } + bagCoordinates.current = undefined; + setOpenBagSettings(false); + }} + /> +
+ ); + } \ No newline at end of file diff --git a/src/maps/mapObjectTools/agMapTools.tsx b/src/maps/mapObjectTools/agMapTools.tsx new file mode 100644 index 0000000..58ccc51 --- /dev/null +++ b/src/maps/mapObjectTools/agMapTools.tsx @@ -0,0 +1,419 @@ +import { + Grid2 as Grid, + IconButton, + Theme, + List, + ListItem, + ListItemIcon, + Typography, + Box, + FormControlLabel, + Checkbox +} from "@mui/material"; +import AddMarkerIcon from "products/AgIcons/AddMarker"; +import FieldMapIcon from "products/AgIcons/FieldMap"; +import FieldNamesIcon from "products/AgIcons/FieldNames"; +import ScoutIcon from "products/AgIcons/ScoutIcon"; +import AddBinIcon from "products/Bindapt/AddBinIcon"; +import BinsIcon from "products/Bindapt/BinsIcon"; +import { VisibilityOff } from "@mui/icons-material"; +import React, { useState } from "react"; +import AddFieldIcon from "products/AgIcons/AddField"; +import EditIcon from "products/AgIcons/Edit"; +import DeleteIcon from "products/AgIcons/Delete"; +import FieldListIcon from "products/AgIcons/FieldList"; +import DeviceIcon from "products/Bindapt/BindaptIcon"; +import { pond } from "protobuf-ts/pond"; +//import { useHistory } from "react-router"; +import { makeStyles } from "@mui/styles"; + +const useStyles = makeStyles((theme: Theme) => ({ + iconButtons: { + background: theme.palette.background.default, + margin: 5, + boxShadow: "4px 4px 10px black" + }, + list: { + background: theme.palette.background.default, + opacity: 0.75, + borderRadius: 15, + marginTop: 10, + boxShadow: "4px 4px 10px black" + }, + liFont: { + fontWeight: 700, + marginLeft: -15 + }, + listItems: { + borderRadius: "3rem" + } +})); + +interface Props { + toggleEditorMode: ( + modeName: "edit" | "drawPolygon" | "drawLine" | "drawPoint" | "none" | "delete" | undefined + ) => void; + //these modes are for the imported editor, could possibly be removed when new editor is made + binMarkerDetails: boolean; + toggleBinMarkerDetails: (detailsVisible: boolean) => void; //controls whether the details are visible on the + toggleIsNewObject: (newObject: boolean) => void; //controls whether creating a new object, in this case bin, when clicking on the map to place a marker + toggleMarkerType: (type: pond.ObjectType) => void; + toggleFieldTitles: (show: boolean) => void; + toggleObjectEditType: (type: pond.ObjectType) => void; + toggleMeasurement: (measurement: boolean) => void; + //because the draw control for the map actually changes the modes itself internally will need to have the object controller on the outside change its mode as well + editorMode: "edit" | "drawPolygon" | "drawLine" | "drawPoint" | "none" | "delete" | undefined; +} + +export default function AgMapTools(props: Props) { + const classes = useStyles(); + const { + toggleEditorMode, + toggleBinMarkerDetails, + binMarkerDetails, + toggleIsNewObject, + toggleMarkerType, + toggleFieldTitles, + toggleObjectEditType, + toggleMeasurement + } = props; + const [markerType, setMarkerType] = useState(); + const [isNew, setIsNew] = useState(false); + const [fieldControlDisplay, setFieldControlDisplay] = useState<"none" | "block">("none"); + const [binControlDisplay, setBinControlDisplay] = useState<"none" | "block">("none"); + const [scoutingControlDisplay, setScoutingControlDisplay] = useState<"none" | "block">("none"); + const [fieldControlHovered, setFieldControlHovered] = useState(false); + const [binControlHovered, setBinControlHovered] = useState(false); + const [scoutingControlHovered, setScoutingControlHovered] = useState(false); + const [showFieldTitle, setShowFieldTitles] = useState(false); + //const history = useHistory(); + + const goToMyFields = () => { + console.log("navigate to my fields page") + //history.push("/myFields"); + }; + + const toggleControls = (control: "field" | "bin" | "scout") => { + if (control === "field") { + if (fieldControlDisplay === "none") { + setFieldControlDisplay("block"); + setBinControlDisplay("none"); + setScoutingControlDisplay("none"); + } else { + setFieldControlDisplay("none"); + } + } + if (control === "bin") { + if (binControlDisplay === "none") { + setBinControlDisplay("block"); + setFieldControlDisplay("none"); + setScoutingControlDisplay("none"); + } else { + setBinControlDisplay("none"); + } + } + if (control === "scout") { + if (scoutingControlDisplay === "none") { + setScoutingControlDisplay("block"); + setFieldControlDisplay("none"); + setBinControlDisplay("none"); + } else { + setScoutingControlDisplay("none"); + } + } + toggleEditorMode("none"); + toggleIsNewObject(false); + changeMarkerType(pond.ObjectType.OBJECT_TYPE_UNKNOWN); + }; + + const changeMarkerType = (type: pond.ObjectType) => { + setMarkerType(type); + toggleMarkerType(type); + }; + + const changeNewStatus = (isNew: boolean) => { + setIsNew(isNew); + toggleIsNewObject(isNew); + }; + + return ( + + + + + + { + toggleControls("bin"); + }} + onMouseOver={() => setBinControlHovered(true)} + onMouseOut={() => setBinControlHovered(false)}> + {binControlDisplay === "block" ? : } + + + + { + toggleControls("field"); + }} + onMouseOver={() => setFieldControlHovered(true)} + onMouseOut={() => setFieldControlHovered(false)}> + {fieldControlDisplay === "block" ? : } + + + + { + toggleControls("scout"); + }} + onMouseOver={() => setScoutingControlHovered(true)} + onMouseOut={() => setScoutingControlHovered(false)}> + {scoutingControlDisplay === "block" ? : } + + + + + + + { + changeNewStatus(true); + //setPlaceLine(false); + changeMarkerType(pond.ObjectType.OBJECT_TYPE_BIN); + }} + className={classes.listItems} + style={ + isNew && markerType === pond.ObjectType.OBJECT_TYPE_BIN + ? { background: "green" } + : {} + }> + + + + Create New Bin + + { + changeNewStatus(false); + //setPlaceLine(false); + changeMarkerType(pond.ObjectType.OBJECT_TYPE_BIN); + }} + className={classes.listItems} + style={ + !isNew && markerType === pond.ObjectType.OBJECT_TYPE_BIN + ? { background: "green" } + : {} + }> + + + + Set Bin Location + + { + changeNewStatus(false); + //setPlaceLine(false); + changeMarkerType(pond.ObjectType.OBJECT_TYPE_BINYARD); + }} + className={classes.listItems} + style={ + markerType === pond.ObjectType.OBJECT_TYPE_BINYARD + ? { background: "green" } + : {} + }> + + + + Set Bin Yard Location + + + toggleBinMarkerDetails(e.target.checked)} + /> + } + /> + + + + + + + + { + toggleEditorMode("drawPolygon"); + toggleMeasurement(false); + }} + className={classes.listItems} + style={props.editorMode === "drawPolygon" ? { background: "green" } : {}}> + + + + Add Field/Hole + + { + toggleEditorMode("drawLine"); + toggleMeasurement(false); + toggleObjectEditType(pond.ObjectType.OBJECT_TYPE_GRAIN_BAG); + }} + className={classes.listItems} + style={props.editorMode === "drawLine" ? { background: "green" } : {}}> + + Add Grain Bag + + { + toggleEditorMode("edit"); + toggleMeasurement(false); + }} + className={classes.listItems} + style={props.editorMode === "edit" ? { background: "green" } : {}}> + + + + Edit Field Structures + + { + toggleEditorMode("delete"); + toggleMeasurement(false); + }} + className={classes.listItems} + style={props.editorMode === "delete" ? { background: "green" } : {}}> + + + + Delete Field + + + + + + Go to My Fields + + { + toggleFieldTitles(!showFieldTitle); + setShowFieldTitles(!showFieldTitle); + }} + className={classes.listItems}> + + {showFieldTitle ? : } + + View Field Names + + { + changeMarkerType(pond.ObjectType.OBJECT_TYPE_DEVICE); + }} + className={classes.listItems} + style={ + markerType === pond.ObjectType.OBJECT_TYPE_DEVICE + ? { background: "green" } + : {} + }> + + + + Set Device Location + + + + + + + { + changeMarkerType(pond.ObjectType.OBJECT_TYPE_FIELDMARKER); + // if (watching) { + // props.newMarker(geoPosition.longitude, geoPosition.latitude, "custom"); + // } + }} + className={classes.listItems} + style={ + markerType === pond.ObjectType.OBJECT_TYPE_FIELDMARKER + ? { background: "green" } + : {} + }> + + + + Set Custom Marker + + { + toggleEditorMode("drawLine"); + toggleObjectEditType(pond.ObjectType.OBJECT_TYPE_UNKNOWN); //TODO: make this a measurement object type in the future + toggleMeasurement(true); + }} + className={classes.listItems} + style={props.editorMode === "drawLine" ? { background: "green" } : {}}> + + Measurement Line + + + + + + + + ); +} diff --git a/src/maps/mapboxStyleOverrides.css b/src/maps/mapboxStyleOverrides.css new file mode 100644 index 0000000..75c8b77 --- /dev/null +++ b/src/maps/mapboxStyleOverrides.css @@ -0,0 +1,284 @@ +/* Basics */ +.mapboxgl-ctrl-geocoder, +.mapboxgl-ctrl-geocoder *, +.mapboxgl-ctrl-geocoder *:after, +.mapboxgl-ctrl-geocoder *:before { + box-sizing: border-box; +} + +.mapboxgl-ctrl-top-right { + pointer-events: none; + position: absolute; + z-index: 2; + padding-left: 20px; + width: 100%; +} + +/* .mapboxgl-ctrl-top-left { + left: 0; + top: 0; +} +.mapboxgl-ctrl-top-right { + right: 0; + top: 0; +} +.mapboxgl-ctrl-bottom-left { + bottom: 0; + left: 0; +} +.mapboxgl-ctrl-bottom-right { + bottom: 0; + right: 0; +} */ + +.mapboxgl-ctrl-geocoder { + font-size: 18px; + line-height: 24px; + font-family: "Open Sans", "Helvetica Neue", Arial, Helvetica, sans-serif; + position: relative; + background-color: #424242; + width: 100%; + min-width: 240px; + z-index: 1; + border-radius: 20px; + transition: width 0.25s, min-width 0.25s; + opacity: 0.95; + margin: 0px; +} + +.mapboxgl-ctrl-geocoder--input { + font: inherit; + width: 100%; + border: 0; + background-color: transparent; + margin: 0; + height: 50px; + color: #fff; + padding: 6px 45px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.mapboxgl-ctrl-geocoder--input::-ms-clear { + display: none; /* hide input clear button in IE */ +} + +.mapboxgl-ctrl-geocoder--input:focus { + color: #fff; + outline: 0; + box-shadow: none; + outline: thin dotted; +} + +.mapboxgl-ctrl-geocoder .mapboxgl-ctrl-geocoder--pin-right > * { + z-index: 2; + position: absolute; + right: 8px; + top: 7px; + display: none; +} + +.mapboxgl-ctrl-geocoder, +.mapboxgl-ctrl-geocoder .suggestions { + box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1); +} + +/* Collapsed */ +.mapboxgl-ctrl-geocoder.mapboxgl-ctrl-geocoder--collapsed { + width: 50px; + min-width: 50px; + transition: width 0.25s, min-width 0.25s; +} + +/* Suggestions */ +.mapboxgl-ctrl-geocoder .suggestions { + background-color: #424242; + opacity: 0.95; + border-radius: 4px; + left: 0; + list-style: none; + margin: 0; + padding: 0; + position: absolute; + width: 100%; + top: 110%; /* fallback */ + top: calc(100% + 6px); + z-index: 1000; + overflow: hidden; + font-size: 15px; +} + +.mapboxgl-ctrl-bottom-left .suggestions, +.mapboxgl-ctrl-bottom-right .suggestions { + top: auto; + bottom: 100%; +} + +.mapboxgl-ctrl-geocoder .suggestions > li > a { + cursor: default; + display: block; + padding: 6px 12px; + color: #fff; +} + +.mapboxgl-ctrl-geocoder .suggestions > .active > a, +.mapboxgl-ctrl-geocoder .suggestions > li > a:hover { + color: #fff; + background-color: #303030; + text-decoration: none; + cursor: pointer; +} + +.mapboxgl-ctrl-geocoder--suggestion-title { + font-weight: bold; +} + +.mapboxgl-ctrl-geocoder--suggestion-title, +.mapboxgl-ctrl-geocoder--suggestion-address { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +/* Icons */ +.mapboxgl-ctrl-geocoder--icon { + display: inline-block; + vertical-align: middle; + fill: #757575; + top: 15px; +} + +.mapboxgl-ctrl-geocoder--icon-search { + position: absolute; + top: 13px; + left: 12px; + width: 23px; + height: 23px; +} + +.mapboxgl-ctrl-geocoder--button { + padding: 0; + margin: 0; + border: none; + cursor: pointer; + background: #424242; + line-height: 1; +} + +.mapboxgl-ctrl-geocoder--icon-close { + width: 20px; + height: 20px; + margin-top: 8px; + margin-right: 3px; +} + +.mapboxgl-ctrl-geocoder--button:hover .mapboxgl-ctrl-geocoder--icon-close { + fill: #909090; +} + +.mapboxgl-ctrl-geocoder--icon-loading { + width: 26px; + height: 26px; + margin-top: 5px; + margin-right: 0px; + -moz-animation: rotate 0.8s infinite cubic-bezier(0.45, 0.05, 0.55, 0.95); + -webkit-animation: rotate 0.8s infinite cubic-bezier(0.45, 0.05, 0.55, 0.95); + animation: rotate 0.8s infinite cubic-bezier(0.45, 0.05, 0.55, 0.95); +} + +/* Animation */ +@-webkit-keyframes rotate { + from { + -webkit-transform: rotate(0); + transform: rotate(0); + } + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes rotate { + from { + -webkit-transform: rotate(0); + transform: rotate(0); + } + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +/* Media queries*/ +@media screen and (min-width: 640px) { + .mapboxgl-ctrl-geocoder.mapboxgl-ctrl-geocoder--collapsed { + width: 36px; + min-width: 36px; + } + + .mapboxgl-ctrl-geocoder { + width: 33.3333%; + font-size: 15px; + line-height: 20px; + max-width: 360px; + } + .mapboxgl-ctrl-geocoder .suggestions { + font-size: 13px; + } + + .mapboxgl-ctrl-geocoder--icon { + top: 8px; + } + + .mapboxgl-ctrl-geocoder--icon-close { + width: 16px; + height: 16px; + margin-top: 3px; + margin-right: 0; + } + + .mapboxgl-ctrl-geocoder--icon-search { + left: 7px; + width: 20px; + height: 20px; + } + + .mapboxgl-ctrl-geocoder--input { + height: 36px; + padding: 6px 35px; + } + + .mapboxgl-ctrl-geocoder--icon-loading { + width: 26px; + height: 26px; + margin-top: -2px; + margin-right: -5px; + } + + .mapbox-gl-geocoder--error { + color: #909090; + padding: 6px 12px; + font-size: 16px; + text-align: center; + } +} + +/* popup styles */ +.mapboxgl-popup-content { + background: #1f1e1d !important; + box-shadow: 5px 5px 20px 4px rgb(0, 0, 0) !important; + border-radius: 3 3 10px 10px !important; +} + +.mapboxgl-popup-tip { + border-bottom-color: #1f1e1d !important; +} + +.mapboxgl-popup-close-button { + background-color: #e8dd40 !important; + border-radius: 3px !important; + position: absolute !important; + top: -5px !important; + right: -5px !important; +} diff --git a/src/models/Bin.ts b/src/models/Bin.ts index c500bab..6ad0b03 100644 --- a/src/models/Bin.ts +++ b/src/models/Bin.ts @@ -1,6 +1,6 @@ import GrainDescriber from "grain/GrainDescriber"; import { cloneDeep } from "lodash"; -// import { MarkerData } from "Maps/mapMarkers/Markers"; +import { MarkerData } from "maps/mapMarkers/Markers"; import { pond } from "protobuf-ts/pond"; import { stringToMaterialColour } from "utils/strings"; import { or } from "utils/types"; @@ -176,25 +176,25 @@ export class Bin { return s; } - // public getMarkerData( - // clickFunc?: (event: React.PointerEvent, index: number, isMobile: boolean) => void, - // updateFunc?: (location: pond.Location) => void - // ): MarkerData { - // let m: MarkerData = { - // centered: true, - // longitude: this.location()?.longitude ?? 0, - // latitude: this.location()?.latitude ?? 0, - // title: this.name(), - // colour: this.grainColour(), - // visibleLevels: { min: 17 }, - // graphPercent: this.fillPercent(), - // customSize: this.settings.theme?.height, - // details: [this.name() + ", " + this.binFillCap()], - // clickFunc: clickFunc, - // updateFunc: updateFunc - // }; - // return m; - // } + public getMarkerData( + clickFunc?: (event: React.PointerEvent, index: number, isMobile: boolean) => void, + updateFunc?: (location: pond.Location) => void + ): MarkerData { + let m: MarkerData = { + centered: true, + longitude: this.location()?.longitude ?? 0, + latitude: this.location()?.latitude ?? 0, + title: this.name(), + colour: this.grainColour(), + visibleLevels: { min: 17 }, + graphPercent: this.fillPercent(), + customSize: this.settings.theme?.height, + details: [this.name() + ", " + this.binFillCap()], + clickFunc: clickFunc, + updateFunc: updateFunc + }; + return m; + } /** * returns the bushels per tonne set in the bins settings, if not set will return 1 diff --git a/src/models/Device.ts b/src/models/Device.ts index 638373c..da08742 100644 --- a/src/models/Device.ts +++ b/src/models/Device.ts @@ -1,7 +1,7 @@ import { pond } from "protobuf-ts/pond"; import { or } from "utils/types"; import { cloneDeep } from "lodash"; -// import { MarkerData } from "Maps/mapMarkers/Markers"; +import { MarkerData } from "maps/mapMarkers/Markers"; interface FeatureVersionByPlatform { photon: string; @@ -65,20 +65,20 @@ export class Device { return loc; } - // public getMarkerData( - // clickFunc?: (event: React.PointerEvent, index: number, isMobile: boolean) => void, - // updateFunc?: (location: pond.Location) => void - // ): MarkerData { - // let m: MarkerData = { - // longitude: this.location()?.longitude ?? 0, - // latitude: this.location()?.latitude ?? 0, - // title: this.name(), - // colour: this.settings.theme?.color ?? "blue", - // clickFunc: clickFunc, - // updateFunc: updateFunc - // }; - // return m; - // } + public getMarkerData( + clickFunc?: (event: React.PointerEvent, index: number, isMobile: boolean) => void, + updateFunc?: (location: pond.Location) => void + ): MarkerData { + let m: MarkerData = { + longitude: this.location()?.longitude ?? 0, + latitude: this.location()?.latitude ?? 0, + title: this.name(), + colour: this.settings.theme?.color ?? "blue", + clickFunc: clickFunc, + updateFunc: updateFunc + }; + return m; + } public featureSupported(feature: string): boolean { let versions = featureVersions.get(feature); diff --git a/src/models/FieldMarker.ts b/src/models/FieldMarker.ts new file mode 100644 index 0000000..3044542 --- /dev/null +++ b/src/models/FieldMarker.ts @@ -0,0 +1,127 @@ +import { cloneDeep } from "lodash"; +import { pond } from "protobuf-ts/pond"; +import { or } from "utils/types"; + +export class FieldMarker { + public settings: pond.FieldMarkerSettings = pond.FieldMarkerSettings.create(); + public status: pond.FieldMarkerStatus = pond.FieldMarkerStatus.create(); + + public static create(hm?: pond.FieldMarker): FieldMarker { + let my = new FieldMarker(); + if (hm) { + my.settings = pond.FieldMarkerSettings.fromObject(cloneDeep(or(hm.settings, {}))); + my.status = pond.FieldMarkerStatus.fromObject(cloneDeep(or(hm.status, {}))); + } + return my; + } + + public static clone(other?: FieldMarker): FieldMarker { + if (other) { + return FieldMarker.create( + pond.FieldMarker.fromObject({ + settings: cloneDeep(other.settings), + status: cloneDeep(other.status) + }) + ); + } + return FieldMarker.create(); + } + + public static any(data: any): FieldMarker { + return FieldMarker.create(pond.FieldMarker.fromObject(cloneDeep(data))); + } + + public key(): string { + return this.settings.key; + } + + public long(): number { + return this.settings.longitude; + } + + public lat(): number { + return this.settings.latitude; + } + + public description(): string { + return this.settings.description; + } + + public name(): string { + return this.settings.name; + } + + public severity(): number { + return this.settings.severity; + } + + public severityColour(): string { + switch (this.settings.severity) { + case 1: + return "#58d68d"; + case 2: + return "#FFC300"; + case 3: + return "#FF5733"; + case 4: + return "#FF0000"; + default: + return "#229954"; + } + } + + public severityString(): string { + switch (this.settings.severity) { + case 1: + return "Mild"; + case 2: + return "Moderate"; + case 3: + return "High"; + case 4: + return "Severe"; + default: + return "Low"; + } + } + + public area(): number { + return this.settings.affectedArea; + } + + public type(): pond.FieldMarkerType { + return this.settings.type; + } + + public typeString(): string { + switch (this.settings.type) { + case pond.FieldMarkerType.FIELD_MARKER_TYPE_WEEDS: + return "Weeds"; + case pond.FieldMarkerType.FIELD_MARKER_TYPE_PESTS: + return "Pests"; + case pond.FieldMarkerType.FIELD_MARKER_TYPE_DISEASE: + return "Disease"; + case pond.FieldMarkerType.FIELD_MARKER_TYPE_OTHER: + return "Other"; + case pond.FieldMarkerType.FIELD_MARKER_TYPE_ROCKS: + return "Rocks"; + default: + return "Invalid Type"; + } + } + + public defaultColor(): string { + switch (this.settings.type) { + case pond.FieldMarkerType.FIELD_MARKER_TYPE_WEEDS: + return "green"; + case pond.FieldMarkerType.FIELD_MARKER_TYPE_PESTS: + return "blue"; + case pond.FieldMarkerType.FIELD_MARKER_TYPE_DISEASE: + return "yellow"; + case pond.FieldMarkerType.FIELD_MARKER_TYPE_ROCKS: + return "grey"; + default: + return "red"; + } + } +} diff --git a/src/models/GeometryMapping.ts b/src/models/GeometryMapping.ts index 0c84081..413e18d 100644 --- a/src/models/GeometryMapping.ts +++ b/src/models/GeometryMapping.ts @@ -115,3 +115,19 @@ export function shapeFromCoords(coordinates: number[][]): pond.Shape { }); return shape; } + +export function coordDistance(lat1: number, long1: number, lat2: number, long2: number) { + var R = 6371; // Radius of the earth in km + var dLat = deg2rad(lat2 - lat1); // deg2rad below + var dLon = deg2rad(long2 - long1); + var a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + var d = R * c; // Distance in km + return d; +} + +function deg2rad(deg: number) { + return deg * (Math.PI / 180); +} diff --git a/src/models/HomeMarker.ts b/src/models/HomeMarker.ts new file mode 100644 index 0000000..8a1001d --- /dev/null +++ b/src/models/HomeMarker.ts @@ -0,0 +1,45 @@ +import { cloneDeep } from "lodash"; +import { pond } from "protobuf-ts/pond"; +import { or } from "utils/types"; + +export class HomeMarker { + public settings: pond.HomeMarkerSettings = pond.HomeMarkerSettings.create(); + public status: pond.HomeMarkerStatus = pond.HomeMarkerStatus.create(); + + public static create(hm?: pond.HomeMarker): HomeMarker { + let my = new HomeMarker(); + if (hm) { + my.settings = pond.HomeMarkerSettings.fromObject(cloneDeep(or(hm.settings, {}))); + my.status = pond.HomeMarkerStatus.fromObject(cloneDeep(or(hm.status, {}))); + } + return my; + } + + public static clone(other?: HomeMarker): HomeMarker { + if (other) { + return HomeMarker.create( + pond.HomeMarker.fromObject({ + settings: cloneDeep(other.settings), + status: cloneDeep(other.status) + }) + ); + } + return HomeMarker.create(); + } + + public static any(data: any): HomeMarker { + return HomeMarker.create(pond.HomeMarker.fromObject(cloneDeep(data))); + } + + public key(): string { + return this.settings.key; + } + + public long(): number { + return this.settings.longitude; + } + + public lat(): number { + return this.settings.latitude; + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 5f7ad8e..8d53d26 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -21,6 +21,6 @@ export * from "./team"; // export * from "./HarvestYear"; export * from "./Note"; // export * from "./FieldMapping"; -// export * from "./FieldMarker"; +export * from "./FieldMarker"; export * from "./GrainBag"; export * from "./Contract"; diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx index caf0536..026acd0 100644 --- a/src/navigation/Router.tsx +++ b/src/navigation/Router.tsx @@ -7,6 +7,7 @@ import Logout from "pages/Logout"; import { ErrorBoundary } from "react-error-boundary"; import { getWhitelabel } from "services/whiteLabel"; import Ventilation from "pages/VentEditor"; +import FieldMap from "pages/FieldMap"; const DeviceHistory = lazy(() => import("pages/DeviceHistory")); const DevicePage = lazy(() => import("pages/Device")); @@ -219,6 +220,7 @@ export default function Router(props: Props) { {/* Page routes */} {/* Hello!} /> */} } /> + } /> } /> {/* diff --git a/src/navigation/SideNavigator.tsx b/src/navigation/SideNavigator.tsx index 130a476..1976d0d 100644 --- a/src/navigation/SideNavigator.tsx +++ b/src/navigation/SideNavigator.tsx @@ -24,6 +24,7 @@ import { useLocation, useNavigate } from "react-router-dom"; import BinsIcon from "products/Bindapt/BinsIcon"; import { useGlobalState } from "providers"; import { + IsAdaptiveAgriculture, // hasTutorialPlaylist, // IsAdaptiveAgriculture, // IsAdCon, @@ -33,6 +34,7 @@ import { } from "services/whiteLabel"; import MiningIcon from "products/ventilation/MiningIcon"; import { useAuth0 } from "@auth0/auth0-react"; +import FieldsIcon from "products/AgIcons/FieldsIcon"; const drawerWidth = 230; @@ -137,8 +139,10 @@ export default function SideNavigator(props: Props) { const authenticatedSideMenu = () => { const isMiVent = IsMiVent(); + const isAg = IsAdaptiveAgriculture() return ( + {(isAg || user.hasFeature("admin")) && ( } + )} } + {(isAg || user.hasFeature("admin")) && ( + + goTo("/visualFarm")} + classes={getClasses("/visualFarm")} + > + + + + {open && } + + + )} ) } diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index 205c3c7..5f5dbd5 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -566,7 +566,7 @@ export default function Bin(props: Props) { }; const goToYard = (bin: IBin) => { - navigate("/fields", { state: { + navigate("/visualFarm", { state: { long: bin.getLocation()?.longitude, lat: bin.getLocation()?.latitude } diff --git a/src/pages/FieldMap.tsx b/src/pages/FieldMap.tsx new file mode 100644 index 0000000..2fc42f4 --- /dev/null +++ b/src/pages/FieldMap.tsx @@ -0,0 +1,26 @@ +import AgMapController from "maps/mapObjectControllers/AgMapController"; +import PageContainer from "./PageContainer"; +import { useLocation } from "react-router"; + +export default function FieldMap() { + const location = useLocation(); + return ( + + {location.state !== undefined && + location.state !== null && + location.state.long !== undefined && + location.state.lat !== undefined ? ( + + ) : ( + + )} + + ); +} \ No newline at end of file diff --git a/src/products/AgIcons/FieldsIcon.tsx b/src/products/AgIcons/FieldsIcon.tsx index e5c80fc..4e9ab84 100644 --- a/src/products/AgIcons/FieldsIcon.tsx +++ b/src/products/AgIcons/FieldsIcon.tsx @@ -2,7 +2,6 @@ import FieldsIconLight from "assets/products/Ag/FieldsIconLight.png"; import FieldsIconDark from "assets/products/Ag/FieldsIconDark.png"; import { ImgIcon } from "common/ImgIcon"; import { useThemeType } from "hooks"; -import React from "react"; interface Props { type?: "light" | "dark"; diff --git a/src/providers/index.ts b/src/providers/index.ts index 58b2e10..4b5ba0b 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -14,12 +14,12 @@ export { useDeviceAPI, // useDeviceWebsocket, useFieldAPI, - // useFieldMarkerAPI, + useFieldMarkerAPI, useFirmwareAPI, useGroupAPI, // useHarvestPlanAPI, // useHarvestYearAPI, - // useHomeMarkerAPI, + useHomeMarkerAPI, useInteractionsAPI, // useMeasurementsWebsocket, // useMetricAPI, diff --git a/src/providers/pond/fieldMarkerAPI.tsx b/src/providers/pond/fieldMarkerAPI.tsx new file mode 100644 index 0000000..ed0e4bf --- /dev/null +++ b/src/providers/pond/fieldMarkerAPI.tsx @@ -0,0 +1,113 @@ +import { AxiosResponse } from "axios"; +import { useHTTP } from "hooks"; +import { pond } from "protobuf-ts/pond"; +import { useGlobalState } from "providers"; +import { createContext, PropsWithChildren, useContext } from "react"; +import { pondURL } from "./pond"; + +export interface IFieldMarkerAPIContext { + addFieldMarker: ( + fieldMarker: pond.FieldMarkerSettings + ) => Promise>; + getFieldMarker: (fieldMarkerID: string) => Promise>; + listFieldMarkers: ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + asRoot?: boolean, + as?: string + ) => Promise>; + removeFieldMarker: ( + fieldMarkerID: string + ) => Promise>; + updateFieldMarker: ( + key: string, + fieldMarker: pond.FieldMarkerSettings, + asRoot?: true + ) => Promise>; +} + +export const FieldMarkersAPIContext = createContext( + {} as IFieldMarkerAPIContext +); + +interface Props {} + +export default function FieldMarkerProvider(props: PropsWithChildren) { + const { children } = props; + const { get, del, post, put } = useHTTP(); + const [{ as }] = useGlobalState(); + + const addFieldMarker = (fieldMarker: pond.FieldMarkerSettings) => { + if (as) + return post(pondURL("/fieldMarkers?as=" + as), fieldMarker); + return post(pondURL("/fieldMarkers"), fieldMarker); + }; + + const getFieldMarker = (fieldMarkerId: string) => { + if (as) return get(pondURL("/fieldMarkers/" + fieldMarkerId + "?as=" + as)); + return get(pondURL("/fieldMarkers/" + fieldMarkerId)); + }; + + const removeFieldMarker = (key: string) => { + if (as) + return del(pondURL("/fieldMarkers/" + key + "?as=" + as)); + return del(pondURL("/fieldMarkers/" + key)); + }; + + const listFieldMarkers = ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + asRoot?: boolean, + as?: string + ) => { + return get( + pondURL( + "/fieldMarkers" + + "?limit=" + + limit + + "&offset=" + + offset + + ("&order=" + (order ? order : "asc")) + + ("&by=" + (orderBy ? orderBy : "key")) + + (search ? "&search=" + search : "") + + (asRoot ? "&asRoot=" + asRoot.toString() : "") + + (as ? "&as=" + as : "") + ) + ); + }; + + const updateFieldMarker = (key: string, hm: pond.FieldMarkerSettings, asRoot?: boolean) => { + if (as) + return put( + pondURL( + "/fieldMarkers/" + key + "?as=" + as + (asRoot ? "&asRoot=" + asRoot.toString() : "") + ), + hm + ); + return put( + pondURL("/fieldMarkers/" + key + (asRoot ? "?asRoot=" + asRoot.toString() : "")), + hm + ); + }; + + return ( + + {children} + + ); +} + +export const useFieldMarkerAPI = () => useContext(FieldMarkersAPIContext); diff --git a/src/providers/pond/homeMarkerAPI.tsx b/src/providers/pond/homeMarkerAPI.tsx new file mode 100644 index 0000000..ac11573 --- /dev/null +++ b/src/providers/pond/homeMarkerAPI.tsx @@ -0,0 +1,107 @@ +import { AxiosResponse } from "axios"; +import { useHTTP } from "hooks"; +import { pond } from "protobuf-ts/pond"; +import { useGlobalState } from "providers"; +import { createContext, PropsWithChildren, useContext } from "react"; +import { pondURL } from "./pond"; + +export interface IHomeMarkerAPIContext { + addHomeMarker: ( + homeMarker: pond.HomeMarkerSettings + ) => Promise>; + getHomeMarker: (homeMarkerID: string) => Promise>; + listHomeMarkers: ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + asRoot?: boolean, + as?: string + ) => Promise>; + removeHomeMarker: (homeMarkerID: string) => Promise>; + updateHomeMarker: ( + key: string, + homeMarker: pond.HomeMarkerSettings, + asRoot?: true + ) => Promise>; +} + +export const HomeMarkersAPIContext = createContext( + {} as IHomeMarkerAPIContext +); + +interface Props {} + +export default function HomeMarkerProvider(props: PropsWithChildren) { + const { children } = props; + const { get, del, post, put } = useHTTP(); + const [{ as }] = useGlobalState(); + + const addHomeMarker = (homeMarker: pond.HomeMarkerSettings) => { + if (as) return post(pondURL("/homeMarkers?as=" + as), homeMarker); + return post(pondURL("/homeMarkers"), homeMarker); + }; + + const getHomeMarker = (homeMarkerId: string) => { + return get(pondURL("/homeMarkers/" + homeMarkerId)); + }; + + const removeHomeMarker = (key: string) => { + return del(pondURL("/homeMarkers/" + key)); + }; + + const listHomeMarkers = ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + asRoot?: boolean, + as?: string + ) => { + return get( + pondURL( + "/homeMarkers" + + "?limit=" + + limit + + "&offset=" + + offset + + ("&order=" + (order ? order : "asc")) + + ("&by=" + (orderBy ? orderBy : "key")) + + (search ? "&search=" + search : "") + + (asRoot ? "&asRoot=" + asRoot.toString() : "") + + (as ? "&as=" + as : "") + ) + ); + }; + + const updateHomeMarker = (key: string, hm: pond.HomeMarkerSettings, asRoot?: boolean) => { + if (as) + return put( + pondURL( + "/homeMarkers/" + key + "?as=" + as + (asRoot ? "&asRoot=" + asRoot.toString() : "") + ), + hm + ); + return put( + pondURL("/homeMarkers/" + key + (asRoot ? "?asRoot=" + asRoot.toString() : "")), + hm + ); + }; + + return ( + + {children} + + ); +} + +export const useHomeMarkerAPI = () => useContext(HomeMarkersAPIContext); diff --git a/src/providers/pond/pond.tsx b/src/providers/pond/pond.tsx index 0cef572..fe8b617 100644 --- a/src/providers/pond/pond.tsx +++ b/src/providers/pond/pond.tsx @@ -21,6 +21,8 @@ import FieldProvider, { useFieldAPI } from "./fieldAPI"; import TransactionProvider, { useTransactionAPI } from "./transactionAPI"; import FileControllerProvider, { useFileControllerAPI } from "./fileControllerAPI"; import MineProvider, { useMineAPI } from "./mineAPI"; +import FieldMarkerProvider, {useFieldMarkerAPI} from "./fieldMarkerAPI"; +import HomeMarkerProvider, {useHomeMarkerAPI} from "./homeMarkerAPI"; // import NoteProvider from "providers/noteAPI"; export const pondURL = (partial: string, demo: boolean = false): string => { @@ -58,7 +60,11 @@ export default function PondProvider(props: PropsWithChildren) { - {children} + + + {children} + + @@ -104,4 +110,6 @@ export { useTransactionAPI, useFileControllerAPI, useMineAPI, + useFieldMarkerAPI, + useHomeMarkerAPI }; diff --git a/src/weather/weather.tsx b/src/weather/weather.tsx new file mode 100644 index 0000000..9c40f78 --- /dev/null +++ b/src/weather/weather.tsx @@ -0,0 +1,314 @@ +import { + Box, + Grid2 as Grid, + LinearProgress, + Typography, +} from "@mui/material"; +import { useState } from "react"; +import OpenWeatherMap from "openweathermap-ts"; +import { CurrentResponse, ThreeHourResponse } from "openweathermap-ts/dist/types"; +import { useEffect } from "react"; +//import WeatherIcon from "./weatherIcon"; +import { getThemeType } from "theme"; +import { useMobile, useThemeType } from "hooks"; +import WindSpeedDark from "assets/components/windSpeedDark.png"; +import WindSpeedLight from "assets/components/windSpeedLight.png"; +import { makeStyles } from "@mui/styles"; +//import { WiHumidity } from "weather-icons-react"; + +const openWeather = new OpenWeatherMap({ + //apiKey: process.env.REACT_APP_OPEN_WEATHERMAP ? process.env.REACT_APP_OPEN_WEATHERMAP : "" + apiKey: import.meta.env.REACT_APP_OPEN_WEATHERMAP ? import.meta.env.REACT_APP_OPEN_WEATHERMAP : "" +}); + +interface Props { + longitude?: number; + latitude?: number; +} + +const useStyles = makeStyles(() => ({ + currentBox: { + boxShadow: "0px 0px 10px black", + height: "110px", + borderRadius: 10, + margin: 7 + }, + upperSquare: { + height: "75%", + display: "flex", + justifyContent: "center", + alignItems: "center" + }, + lowerSquare: { + height: "25%", + display: "flex", + justifyContent: "center", + alignItems: "center" + }, + dark: { + backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)", + padding: 5, + opacity: 1 + }, + light: { + backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)", + padding: 5, + opacity: 1 + } +})) + +export default function Weather(props: Props) { + const [currentConditions, setCurrentConditions] = useState(); + const [forecast, setForecast] = useState(); + const classes = useStyles(); + const isMobile = useMobile(); + const themeType = useThemeType(); + + useEffect(() => { + if (props.longitude && props.latitude) { + openWeather + .getThreeHourForecastByGeoCoordinates(props.latitude, props.longitude) + .then(forecast => { + setForecast(forecast); + }); + openWeather + .getCurrentWeatherByGeoCoordinates(props.latitude, props.longitude) + .then(weather => { + setCurrentConditions(weather); + }); + } + }, [props.latitude, props.longitude]); + + const celsiusConverter = (temp: number) => { + return (temp - 32) * (5 / 9); + }; + + /** + * gets the high and low of the first 8 readings in the 5 day forecast, is technically getting the high and low for tomorrow + * but is usually close to the same temperatures, for actual current high/low must use a paid plan for open weather + * @returns + */ + const getHighLow = () => { + if (!forecast) return; + let loopCounter = 0; + let currentHigh = -1000; + let currentLow = 1000; + while (loopCounter <= 7) { + let temp = forecast.list[loopCounter].main.temp; + if (temp < currentLow) { + currentLow = temp; + } + if (temp > currentHigh) { + currentHigh = temp; + } + loopCounter++; + } + return ( + + {Math.round(celsiusConverter(currentHigh))}° /{" "} + {Math.round(celsiusConverter(currentLow))}° + + ); + }; + + const getDirection = (current: CurrentResponse) => { + if (!current) return; + let windDegrees = current.wind.deg; + let direction = ""; + + if (windDegrees > 338 || windDegrees <= 23) { + direction = "N"; + } + if (windDegrees > 23 && windDegrees <= 67) { + direction = "NE"; + } + if (windDegrees > 67 && windDegrees <= 112) { + direction = "E"; + } + if (windDegrees > 112 && windDegrees <= 158) { + direction = "SE"; + } + if (windDegrees > 158 && windDegrees <= 203) { + direction = "S"; + } + if (windDegrees > 203 && windDegrees <= 248) { + direction = "SW"; + } + if (windDegrees > 248 && windDegrees <= 293) { + direction = "W"; + } + if (windDegrees > 293 && windDegrees <= 338) { + direction = "NW"; + } + return direction; + }; + + const dayOfWeek = (day: number) => { + switch (day) { + case 1: + return "Monday"; + case 2: + return "Tuesday"; + case 3: + return "Wednesday"; + case 4: + return "Thursday"; + case 5: + return "Friday"; + case 6: + return "Saturday"; + default: + return "Sunday"; + } + }; + + /** + * Three hour response contains a list of 40 items for the weather, each 8 is a 24 hour period + * loop through 8 items to get the forecast for that 24 hour period + * @param forecast the three hour response that was retrieved + * @param day the day we are getting the forecast for as a number + */ + const getForeCast = (day: number) => { + if (!forecast) return; + let startingPoint = day * 8; + let loopCount = 0; + let high = -1000; + let low = 1000; + let d = forecast.list[startingPoint + 4].dt; + let date = new Date(0); + date.setUTCSeconds(d); + + while (loopCount < 8) { + let temp = forecast.list[startingPoint].main.temp; + if (temp > high) { + high = temp; + } + if (temp < low) { + low = temp; + } + startingPoint++; + loopCount++; + } + + return ( + + + + {dayOfWeek(date.getDay())} + + + {Math.round(celsiusConverter(high))}° / {Math.round(celsiusConverter(low))}° + + + + ); + }; + + return ( + + {currentConditions && forecast ? ( + + + + Current Weather - {dayOfWeek(new Date().getDay())} + + + + + + + {/* */} + + {Math.round(celsiusConverter(currentConditions.main.temp))}℃ + + + + + Feels like {Math.round(celsiusConverter(currentConditions.main.feels_like))} + ℃ + + + + + + + + {/* */} + + {currentConditions.main.humidity}% + + + + + Humidity + + + + + + + + Wind Speed + + + {Math.round(currentConditions.wind.speed * 3.6)}/ + + + km/h + + + + + + Direction {getDirection(currentConditions)} + + + + + + + + {getHighLow()} + + + + Today's High/Low + + + + + + + 5 - Day Forecast + + + {getForeCast(0)} + {getForeCast(1)} + {getForeCast(2)} + {getForeCast(3)} + {getForeCast(4)} + + + ) : ( + + + + )} + + ); +} diff --git a/src/weather/weatherIcon.tsx b/src/weather/weatherIcon.tsx new file mode 100644 index 0000000..32e9385 --- /dev/null +++ b/src/weather/weatherIcon.tsx @@ -0,0 +1,114 @@ +import { AcUnit, Cloud, LightMode, Thunderstorm, WaterDrop } from "@mui/icons-material"; +// import { +// WiDaySunny, +// WiDayCloudy, +// WiDaySunnyOvercast, +// WiDayLightning, +// WiDayStormShowers, +// WiDayThunderstorm, +// WiDaySprinkle, +// WiDayRainMix, +// WiDayRain, +// WiDayShowers, +// WiDayRainWind, +// WiDaySleet, +// WiDaySnow, +// WiDayFog, +// WiDayHaze, +// WiDust, +// WiSmoke, +// WiVolcano, +// WiTornado +// } from "weather-icons-react"; + +interface Props { + openweatherIconId: number; + size?: number; +} + +//need to get Dustin/Sandaru to make some custom weather icons +export default function WeatherIcon(props: Props) { + const size = props.size ? props.size : 25; + const iconId = props.openweatherIconId; + + switch (iconId) { + case 200: + case 210: + case 221: + case 230: + //return ; + case 201: + case 211: + case 231: + //return ; + case 202: + case 212: + case 232: + return + //return ; + case 300: + case 301: + case 302: + case 500: + //return ; + case 310: + case 311: + case 312: + case 501: + //return ; + case 502: + case 503: + case 522: + //return ; + case 314: + case 321: + case 520: + case 521: + case 531: + //return ; + case 504: + //return ; + case 511: + case 611: + case 612: + case 613: + case 615: + case 616: + case 620: + case 621: + case 622: + return + //return ; + case 600: + case 601: + case 602: + return + //return ; + // case 701: + // case 741: + // return ; + // case 711: + // return ; + // case 721: + // return ; + // case 731: + // case 751: + // case 761: + // return ; + // case 762: + // return ; + // case 771: + // case 781: + // return ; + case 801: + case 802: + //return ; + case 803: + case 804: + return + //return ; + default: + return + //return ; + } +} From 52c762fa74ebaea09c6f3110cbd578468729d45d Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 26 Mar 2025 14:56:39 -0600 Subject: [PATCH 2/2] put the geocoder creation into its one useeffect so that it doesn't create duplicates on the map --- src/maps/mapControllers/Geocoder.tsx | 31 +++++++++++++++++----------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/maps/mapControllers/Geocoder.tsx b/src/maps/mapControllers/Geocoder.tsx index 5bfb009..ab0f640 100644 --- a/src/maps/mapControllers/Geocoder.tsx +++ b/src/maps/mapControllers/Geocoder.tsx @@ -1,6 +1,6 @@ import { ControlPosition, useMap } from "react-map-gl/mapbox-legacy"; import MapboxGeocoder, { Result } from "@mapbox/mapbox-gl-geocoder"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; // export interface GeocoderObject { // id: string; @@ -32,6 +32,7 @@ interface Props { export default function Geocoder(props: Props) { const { mapboxAccessToken, position, customEntries, resultFunction, customTransition } = props; + const [geoCoder, setGeoCoder] = useState() const { current: map } = useMap(); const localSearch = (query: string) => { @@ -91,6 +92,19 @@ export default function Geocoder(props: Props) { return matchingResults; }; + //create the geocoder + useEffect(()=>{ + const g = new MapboxGeocoder({ + accessToken: mapboxAccessToken, + localGeocoder: localSearch, + flyTo: customTransition ? false : true, + marker: false, + zoom: 18, + }); + + setGeoCoder(g) + },[mapboxAccessToken]) + // //set the geocoder options // useEffect(() => { // //connects the localSearch function to the geocoder if there are custom entries passed in @@ -106,15 +120,7 @@ export default function Geocoder(props: Props) { //attach custom functions to event handlers useEffect(() => { - const geoCoder = new MapboxGeocoder({ - accessToken: mapboxAccessToken, - localGeocoder: localSearch, - flyTo: customTransition ? false : true, - marker: false, - zoom: 18, - }); - //events that fire as the search bar is typed into, note the function will run AFTER the event fires - + if(geoCoder){ //event is fired when the search bar is cleared geoCoder.on("clear", () => {}); @@ -137,10 +143,11 @@ export default function Geocoder(props: Props) { } }); //once the map ref and access token are set up add the control to the map - if (map && mapboxAccessToken) { + if (map) { map.addControl(geoCoder, position); } - }, [map, mapboxAccessToken, position]); // eslint-disable-line react-hooks/exhaustive-deps + } + }, [map, geoCoder]); // eslint-disable-line react-hooks/exhaustive-deps return null; }