diff --git a/frontend/.env.sample b/frontend/.env.sample index c00219036..7b95380ee 100644 --- a/frontend/.env.sample +++ b/frontend/.env.sample @@ -30,10 +30,10 @@ VITE_MAX_TRAINING_AREA_SIZE = 5000000 # Default value: 5797 square meters. VITE_MIN_TRAINING_AREA_SIZE = 5797 -# The maximum file size allowed for training area upload, measure in bytes. -# Data type: Positive Integer (e.g., 500000). -# Default value: 5242880 bytes (5 MB). -VITE_MAX_TRAINING_AREA_UPLOAD_FILE_SIZE = 5242880 +# The maximum file size allowed for training area upload, measured in bytes. +# Data type: Positive Integer (e.g., 1048576 for 1 MB). +# Default value: 1048576 bytes (1 MB). +VITE_MAX_TRAINING_AREA_UPLOAD_FILE_SIZE = 1048576 # The current version of the application. # This is used in the OSM redirect callback when a training area is opened in OSM. @@ -209,4 +209,22 @@ VITE_FAIR_MODELS_BASE_PATH = '/mnt/efsmount/data' # The offset step for adjusting the training labels in the training area page. # Data type: Positive Integer (e.g., 0.5). # Default value: 0.5. -VITE_OFFSET_STEP = 0.5 \ No newline at end of file +VITE_OFFSET_STEP = 0.5 + +# The Firebase configuration for MapSwipe. +# This configuration is used to connect to the Firebase project for MapSwipe. +# Note: The values below are examples and should be replaced with your actual Firebase project configuration. +# - apiKey: String (e.g., "AIxxxxxx"). +# - authDomain: String (e.g., "dev-hxxxxxxxxx"). +# - databaseURL: String (e.g., "https://dev-hxxxxxxxx"). +# - projectId: String (e.g., "dev-xxxxxxxx"). +# - storageBucket: String (e.g., "dev-heigit-xxxxxxxx"). +# - messagingSenderId: String (e.g., "72xxxxxx"). +# - appId: String (e.g, "1:7240294dxxxxx) +VITE_MAPSWIPE_FIREBASE_API_KEY="", +VITE_MAPSWIPE_FIREBASE_AUTH_DOMAIN="", +VITE_MAPSWIPE_FIREBASE_DATABASE_URL="", +VITE_MAPSWIPE_FIREBASE_PROJECT_ID="", +VITE_MAPSWIPE_FIREBASE_STORAGE_BUCKET="", +VITE_MAPSWIPE_FIREBASE_MESSAGING_SENDER_ID="", +VITE_MAPSWIPE_FIREBASE_APP_ID="" \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index fe6a0c5ee..0e6535629 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "@turf/helpers": "^7.2.0", "axios": "^1.7.7", "clsx": "^2.1.1", + "firebase": "^11.10.0", "framer-motion": "^11.9.0", "geojson": "^0.5.0", "maplibre-gl": "^5.3.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 38d4d5f9a..2dc74ce47 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -54,6 +54,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + firebase: + specifier: ^11.10.0 + version: 11.10.0 framer-motion: specifier: ^11.9.0 version: 11.9.0(@emotion/is-prop-valid@0.7.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -150,7 +153,7 @@ importers: version: 8.18.2(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2) '@vitejs/plugin-react': specifier: ^4.3.2 - version: 4.3.2(vite@5.4.8) + version: 4.3.2(vite@5.4.8(@types/node@24.0.8)) autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.47) @@ -201,13 +204,13 @@ importers: version: 8.8.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2) vite: specifier: ^5.4.8 - version: 5.4.8 + version: 5.4.8(@types/node@24.0.8) vite-tsconfig-paths: specifier: ^5.0.1 - version: 5.0.1(typescript@5.6.2)(vite@5.4.8) + version: 5.0.1(typescript@5.6.2)(vite@5.4.8(@types/node@24.0.8)) vitest: specifier: ^3.0.5 - version: 3.0.5(@types/debug@4.1.12)(jsdom@26.0.0) + version: 3.0.5(@types/debug@4.1.12)(@types/node@24.0.8)(jsdom@26.0.0) packages: @@ -538,6 +541,216 @@ packages: resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@firebase/ai@1.4.1': + resolution: {integrity: sha512-bcusQfA/tHjUjBTnMx6jdoPMpDl3r8K15Z+snHz9wq0Foox0F/V+kNLXucEOHoTL2hTc9l+onZCyBJs2QoIC3g==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-types': 0.x + + '@firebase/analytics-compat@0.2.23': + resolution: {integrity: sha512-3AdO10RN18G5AzREPoFgYhW6vWXr3u+OYQv6pl3CX6Fky8QRk0AHurZlY3Q1xkXO0TDxIsdhO3y65HF7PBOJDw==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/analytics-types@0.8.3': + resolution: {integrity: sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==} + + '@firebase/analytics@0.10.17': + resolution: {integrity: sha512-n5vfBbvzduMou/2cqsnKrIes4auaBjdhg8QNA2ZQZ59QgtO2QiwBaXQZQE4O4sgB0Ds1tvLgUUkY+pwzu6/xEg==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-check-compat@0.3.26': + resolution: {integrity: sha512-PkX+XJMLDea6nmnopzFKlr+s2LMQGqdyT2DHdbx1v1dPSqOol2YzgpgymmhC67vitXVpNvS3m/AiWQWWhhRRPQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/app-check-interop-types@0.3.3': + resolution: {integrity: sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==} + + '@firebase/app-check-types@0.5.3': + resolution: {integrity: sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==} + + '@firebase/app-check@0.10.1': + resolution: {integrity: sha512-MgNdlms9Qb0oSny87pwpjKush9qUwCJhfmTJHDfrcKo4neLGiSeVE4qJkzP7EQTIUFKp84pbTxobSAXkiuQVYQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-compat@0.4.2': + resolution: {integrity: sha512-LssbyKHlwLeiV8GBATyOyjmHcMpX/tFjzRUCS1jnwGAew1VsBB4fJowyS5Ud5LdFbYpJeS+IQoC+RQxpK7eH3Q==} + engines: {node: '>=18.0.0'} + + '@firebase/app-types@0.9.3': + resolution: {integrity: sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==} + + '@firebase/app@0.13.2': + resolution: {integrity: sha512-jwtMmJa1BXXDCiDx1vC6SFN/+HfYG53UkfJa6qeN5ogvOunzbFDO3wISZy5n9xgYFUrEP6M7e8EG++riHNTv9w==} + engines: {node: '>=18.0.0'} + + '@firebase/auth-compat@0.5.28': + resolution: {integrity: sha512-HpMSo/cc6Y8IX7bkRIaPPqT//Jt83iWy5rmDWeThXQCAImstkdNo3giFLORJwrZw2ptiGkOij64EH1ztNJzc7Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/auth-interop-types@0.2.4': + resolution: {integrity: sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==} + + '@firebase/auth-types@0.13.0': + resolution: {integrity: sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/auth@1.10.8': + resolution: {integrity: sha512-GpuTz5ap8zumr/ocnPY57ZanX02COsXloY6Y/2LYPAuXYiaJRf6BAGDEdRq1BMjP93kqQnKNuKZUTMZbQ8MNYA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@react-native-async-storage/async-storage': ^1.18.1 + peerDependenciesMeta: + '@react-native-async-storage/async-storage': + optional: true + + '@firebase/component@0.6.18': + resolution: {integrity: sha512-n28kPCkE2dL2U28fSxZJjzPPVpKsQminJ6NrzcKXAI0E/lYC8YhfwpyllScqVEvAI3J2QgJZWYgrX+1qGI+SQQ==} + engines: {node: '>=18.0.0'} + + '@firebase/data-connect@0.3.10': + resolution: {integrity: sha512-VMVk7zxIkgwlVQIWHOKFahmleIjiVFwFOjmakXPd/LDgaB/5vzwsB5DWIYo+3KhGxWpidQlR8geCIn39YflJIQ==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/database-compat@2.0.11': + resolution: {integrity: sha512-itEsHARSsYS95+udF/TtIzNeQ0Uhx4uIna0sk4E0wQJBUnLc/G1X6D7oRljoOuwwCezRLGvWBRyNrugv/esOEw==} + engines: {node: '>=18.0.0'} + + '@firebase/database-types@1.0.15': + resolution: {integrity: sha512-XWHJ0VUJ0k2E9HDMlKxlgy/ZuTa9EvHCGLjaKSUvrQnwhgZuRU5N3yX6SZ+ftf2hTzZmfRkv+b3QRvGg40bKNw==} + + '@firebase/database@1.0.20': + resolution: {integrity: sha512-H9Rpj1pQ1yc9+4HQOotFGLxqAXwOzCHsRSRjcQFNOr8lhUt6LeYjf0NSRL04sc4X0dWe8DsCvYKxMYvFG/iOJw==} + engines: {node: '>=18.0.0'} + + '@firebase/firestore-compat@0.3.53': + resolution: {integrity: sha512-qI3yZL8ljwAYWrTousWYbemay2YZa+udLWugjdjju2KODWtLG94DfO4NALJgPLv8CVGcDHNFXoyQexdRA0Cz8Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/firestore-types@3.0.3': + resolution: {integrity: sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/firestore@4.8.0': + resolution: {integrity: sha512-QSRk+Q1/CaabKyqn3C32KSFiOdZpSqI9rpLK5BHPcooElumOBooPFa6YkDdiT+/KhJtel36LdAacha9BptMj2A==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/functions-compat@0.3.26': + resolution: {integrity: sha512-A798/6ff5LcG2LTWqaGazbFYnjBW8zc65YfID/en83ALmkhu2b0G8ykvQnLtakbV9ajrMYPn7Yc/XcYsZIUsjA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/functions-types@0.6.3': + resolution: {integrity: sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==} + + '@firebase/functions@0.12.9': + resolution: {integrity: sha512-FG95w6vjbUXN84Ehezc2SDjGmGq225UYbHrb/ptkRT7OTuCiQRErOQuyt1jI1tvcDekdNog+anIObihNFz79Lg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/installations-compat@0.2.18': + resolution: {integrity: sha512-aLFohRpJO5kKBL/XYL4tN+GdwEB/Q6Vo9eZOM/6Kic7asSUgmSfGPpGUZO1OAaSRGwF4Lqnvi1f/f9VZnKzChw==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/installations-types@0.5.3': + resolution: {integrity: sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==} + peerDependencies: + '@firebase/app-types': 0.x + + '@firebase/installations@0.6.18': + resolution: {integrity: sha512-NQ86uGAcvO8nBRwVltRL9QQ4Reidc/3whdAasgeWCPIcrhOKDuNpAALa6eCVryLnK14ua2DqekCOX5uC9XbU/A==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/logger@0.4.4': + resolution: {integrity: sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==} + engines: {node: '>=18.0.0'} + + '@firebase/messaging-compat@0.2.22': + resolution: {integrity: sha512-5ZHtRnj6YO6f/QPa/KU6gryjmX4Kg33Kn4gRpNU6M1K47Gm8kcQwPkX7erRUYEH1mIWptfvjvXMHWoZaWjkU7A==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/messaging-interop-types@0.2.3': + resolution: {integrity: sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==} + + '@firebase/messaging@0.12.22': + resolution: {integrity: sha512-GJcrPLc+Hu7nk+XQ70Okt3M1u1eRr2ZvpMbzbc54oTPJZySHcX9ccZGVFcsZbSZ6o1uqumm8Oc7OFkD3Rn1/og==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/performance-compat@0.2.20': + resolution: {integrity: sha512-XkFK5NmOKCBuqOKWeRgBUFZZGz9SzdTZp4OqeUg+5nyjapTiZ4XoiiUL8z7mB2q+63rPmBl7msv682J3rcDXIQ==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/performance-types@0.2.3': + resolution: {integrity: sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==} + + '@firebase/performance@0.7.7': + resolution: {integrity: sha512-JTlTQNZKAd4+Q5sodpw6CN+6NmwbY72av3Lb6wUKTsL7rb3cuBIhQSrslWbVz0SwK3x0ZNcqX24qtRbwKiv+6w==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/remote-config-compat@0.2.18': + resolution: {integrity: sha512-YiETpldhDy7zUrnS8e+3l7cNs0sL7+tVAxvVYU0lu7O+qLHbmdtAxmgY+wJqWdW2c9nDvBFec7QiF58pEUu0qQ==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/remote-config-types@0.4.0': + resolution: {integrity: sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==} + + '@firebase/remote-config@0.6.5': + resolution: {integrity: sha512-fU0c8HY0vrVHwC+zQ/fpXSqHyDMuuuglV94VF6Yonhz8Fg2J+KOowPGANM0SZkLvVOYpTeWp3ZmM+F6NjwWLnw==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/storage-compat@0.3.24': + resolution: {integrity: sha512-XHn2tLniiP7BFKJaPZ0P8YQXKiVJX+bMyE2j2YWjYfaddqiJnROJYqSomwW6L3Y+gZAga35ONXUJQju6MB6SOQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/storage-types@0.8.3': + resolution: {integrity: sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/storage@0.13.14': + resolution: {integrity: sha512-xTq5ixxORzx+bfqCpsh+o3fxOsGoDjC1nO0Mq2+KsOcny3l7beyBhP/y1u5T6mgsFQwI1j6oAkbT5cWdDBx87g==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/util@1.12.1': + resolution: {integrity: sha512-zGlBn/9Dnya5ta9bX/fgEoNC3Cp8s6h+uYPYaDieZsFOAdHP/ExzQ/eaDgxD3GOROdPkLKpvKY0iIzr9adle0w==} + engines: {node: '>=18.0.0'} + + '@firebase/webchannel-wrapper@1.0.3': + resolution: {integrity: sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==} + '@floating-ui/core@1.6.8': resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} @@ -547,6 +760,15 @@ packages: '@floating-ui/utils@0.2.8': resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} + '@grpc/grpc-js@1.9.15': + resolution: {integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==} + engines: {node: ^8.13.0 || >=10.10.0} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -652,6 +874,36 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@radix-ui/primitive@1.1.1': resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} @@ -1099,6 +1351,9 @@ packages: '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + '@types/node@24.0.8': + resolution: {integrity: sha512-WytNrFSgWO/esSH9NbpWUfTMGQwCGIKfCmNlmFDNiI5gGhgMmEA+V1AEvKLeBNvvtBnailJtkrEa2OIISwrVAA==} + '@types/pbf@3.0.5': resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} @@ -1494,6 +1749,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1858,6 +2117,10 @@ packages: fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -1877,6 +2140,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + firebase@11.10.0: + resolution: {integrity: sha512-nKBXoDzF0DrXTBQJlZa+sbC5By99ysYU1D6PkMRYknm0nCW7rJly47q492Ht7Ndz5MeYSBuboKuhS1e6mFC03w==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -1951,6 +2217,10 @@ packages: resolution: {integrity: sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ==} engines: {node: '>= 0.10'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.2.6: resolution: {integrity: sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==} engines: {node: '>= 0.4'} @@ -2059,6 +2329,9 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -2074,6 +2347,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2391,6 +2667,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.castarray@4.4.0: resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} @@ -2403,6 +2682,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -2820,6 +3102,10 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + protobufjs@7.5.3: + resolution: {integrity: sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==} + engines: {node: '>=12.0.0'} + protocol-buffers-schema@3.6.0: resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} @@ -2980,6 +3266,10 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -3020,6 +3310,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -3350,6 +3643,9 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -3491,10 +3787,21 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -3573,6 +3880,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -3581,6 +3892,14 @@ packages: engines: {node: '>= 14'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3901,6 +4220,324 @@ snapshots: dependencies: levn: 0.4.1 + '@firebase/ai@1.4.1(@firebase/app-types@0.9.3)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/app-types': 0.9.3 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/analytics-compat@0.2.23(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2)': + dependencies: + '@firebase/analytics': 0.10.17(@firebase/app@0.13.2) + '@firebase/analytics-types': 0.8.3 + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/analytics-types@0.8.3': {} + + '@firebase/analytics@0.10.17(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/installations': 0.6.18(@firebase/app@0.13.2) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/app-check-compat@0.3.26(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-check': 0.10.1(@firebase/app@0.13.2) + '@firebase/app-check-types': 0.5.3 + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/app-check-interop-types@0.3.3': {} + + '@firebase/app-check-types@0.5.3': {} + + '@firebase/app-check@0.10.1(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/app-compat@0.4.2': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/app-types@0.9.3': {} + + '@firebase/app@0.13.2': + dependencies: + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/auth-compat@0.5.28(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/auth': 1.10.8(@firebase/app@0.13.2) + '@firebase/auth-types': 0.13.0(@firebase/app-types@0.9.3)(@firebase/util@1.12.1) + '@firebase/component': 0.6.18 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + - '@react-native-async-storage/async-storage' + + '@firebase/auth-interop-types@0.2.4': {} + + '@firebase/auth-types@0.13.0(@firebase/app-types@0.9.3)(@firebase/util@1.12.1)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.1 + + '@firebase/auth@1.10.8(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/component@0.6.18': + dependencies: + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/data-connect@0.3.10(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/database-compat@2.0.11': + dependencies: + '@firebase/component': 0.6.18 + '@firebase/database': 1.0.20 + '@firebase/database-types': 1.0.15 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/database-types@1.0.15': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.1 + + '@firebase/database@1.0.20': + dependencies: + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + faye-websocket: 0.11.4 + tslib: 2.8.1 + + '@firebase/firestore-compat@0.3.53(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/firestore': 4.8.0(@firebase/app@0.13.2) + '@firebase/firestore-types': 3.0.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.1) + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/firestore-types@3.0.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.1)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.1 + + '@firebase/firestore@4.8.0(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + '@firebase/webchannel-wrapper': 1.0.3 + '@grpc/grpc-js': 1.9.15 + '@grpc/proto-loader': 0.7.15 + tslib: 2.8.1 + + '@firebase/functions-compat@0.3.26(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/functions': 0.12.9(@firebase/app@0.13.2) + '@firebase/functions-types': 0.6.3 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/functions-types@0.6.3': {} + + '@firebase/functions@0.12.9(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.6.18 + '@firebase/messaging-interop-types': 0.2.3 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/installations-compat@0.2.18(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/installations': 0.6.18(@firebase/app@0.13.2) + '@firebase/installations-types': 0.5.3(@firebase/app-types@0.9.3) + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/installations-types@0.5.3(@firebase/app-types@0.9.3)': + dependencies: + '@firebase/app-types': 0.9.3 + + '@firebase/installations@0.6.18(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/util': 1.12.1 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/logger@0.4.4': + dependencies: + tslib: 2.8.1 + + '@firebase/messaging-compat@0.2.22(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/messaging': 0.12.22(@firebase/app@0.13.2) + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/messaging-interop-types@0.2.3': {} + + '@firebase/messaging@0.12.22(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/installations': 0.6.18(@firebase/app@0.13.2) + '@firebase/messaging-interop-types': 0.2.3 + '@firebase/util': 1.12.1 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/performance-compat@0.2.20(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/performance': 0.7.7(@firebase/app@0.13.2) + '@firebase/performance-types': 0.2.3 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/performance-types@0.2.3': {} + + '@firebase/performance@0.7.7(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/installations': 0.6.18(@firebase/app@0.13.2) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + web-vitals: 4.2.4 + + '@firebase/remote-config-compat@0.2.18(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/logger': 0.4.4 + '@firebase/remote-config': 0.6.5(@firebase/app@0.13.2) + '@firebase/remote-config-types': 0.4.0 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/remote-config-types@0.4.0': {} + + '@firebase/remote-config@0.6.5(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/installations': 0.6.18(@firebase/app@0.13.2) + '@firebase/logger': 0.4.4 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/storage-compat@0.3.24(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2)': + dependencies: + '@firebase/app-compat': 0.4.2 + '@firebase/component': 0.6.18 + '@firebase/storage': 0.13.14(@firebase/app@0.13.2) + '@firebase/storage-types': 0.8.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.1) + '@firebase/util': 1.12.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/storage-types@0.8.3(@firebase/app-types@0.9.3)(@firebase/util@1.12.1)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.12.1 + + '@firebase/storage@0.13.14(@firebase/app@0.13.2)': + dependencies: + '@firebase/app': 0.13.2 + '@firebase/component': 0.6.18 + '@firebase/util': 1.12.1 + tslib: 2.8.1 + + '@firebase/util@1.12.1': + dependencies: + tslib: 2.8.1 + + '@firebase/webchannel-wrapper@1.0.3': {} + '@floating-ui/core@1.6.8': dependencies: '@floating-ui/utils': 0.2.8 @@ -3912,6 +4549,18 @@ snapshots: '@floating-ui/utils@0.2.8': {} + '@grpc/grpc-js@1.9.15': + dependencies: + '@grpc/proto-loader': 0.7.15 + '@types/node': 24.0.8 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.3 + yargs: 17.7.2 + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/retry@0.3.0': {} @@ -4015,6 +4664,29 @@ snapshots: '@pkgr/core@0.1.1': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@radix-ui/primitive@1.1.1': {} '@radix-ui/react-compose-refs@1.1.1(@types/react@18.3.10)(react@18.3.1)': @@ -4498,6 +5170,10 @@ snapshots: '@types/ms@0.7.34': {} + '@types/node@24.0.8': + dependencies: + undici-types: 7.8.0 + '@types/pbf@3.0.5': {} '@types/prop-types@15.7.13': {} @@ -4685,14 +5361,14 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-react@4.3.2(vite@5.4.8)': + '@vitejs/plugin-react@4.3.2(vite@5.4.8(@types/node@24.0.8))': dependencies: '@babel/core': 7.25.2 '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.2) '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.2) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.8 + vite: 5.4.8(@types/node@24.0.8) transitivePeerDependencies: - supports-color @@ -4703,13 +5379,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.5(vite@5.4.8)': + '@vitest/mocker@3.0.5(vite@5.4.8(@types/node@24.0.8))': dependencies: '@vitest/spy': 3.0.5 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.8 + vite: 5.4.8(@types/node@24.0.8) '@vitest/pretty-format@3.0.5': dependencies: @@ -4982,6 +5658,12 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clsx@2.1.1: {} color-convert@1.9.3: @@ -5444,6 +6126,10 @@ snapshots: dependencies: reusify: 1.0.4 + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + fflate@0.8.2: {} file-entry-cache@8.0.0: @@ -5463,6 +6149,39 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + firebase@11.10.0: + dependencies: + '@firebase/ai': 1.4.1(@firebase/app-types@0.9.3)(@firebase/app@0.13.2) + '@firebase/analytics': 0.10.17(@firebase/app@0.13.2) + '@firebase/analytics-compat': 0.2.23(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2) + '@firebase/app': 0.13.2 + '@firebase/app-check': 0.10.1(@firebase/app@0.13.2) + '@firebase/app-check-compat': 0.3.26(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2) + '@firebase/app-compat': 0.4.2 + '@firebase/app-types': 0.9.3 + '@firebase/auth': 1.10.8(@firebase/app@0.13.2) + '@firebase/auth-compat': 0.5.28(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2) + '@firebase/data-connect': 0.3.10(@firebase/app@0.13.2) + '@firebase/database': 1.0.20 + '@firebase/database-compat': 2.0.11 + '@firebase/firestore': 4.8.0(@firebase/app@0.13.2) + '@firebase/firestore-compat': 0.3.53(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2) + '@firebase/functions': 0.12.9(@firebase/app@0.13.2) + '@firebase/functions-compat': 0.3.26(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2) + '@firebase/installations': 0.6.18(@firebase/app@0.13.2) + '@firebase/installations-compat': 0.2.18(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2) + '@firebase/messaging': 0.12.22(@firebase/app@0.13.2) + '@firebase/messaging-compat': 0.2.22(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2) + '@firebase/performance': 0.7.7(@firebase/app@0.13.2) + '@firebase/performance-compat': 0.2.20(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2) + '@firebase/remote-config': 0.6.5(@firebase/app@0.13.2) + '@firebase/remote-config-compat': 0.2.18(@firebase/app-compat@0.4.2)(@firebase/app@0.13.2) + '@firebase/storage': 0.13.14(@firebase/app@0.13.2) + '@firebase/storage-compat': 0.3.24(@firebase/app-compat@0.4.2)(@firebase/app-types@0.9.3)(@firebase/app@0.13.2) + '@firebase/util': 1.12.1 + transitivePeerDependencies: + - '@react-native-async-storage/async-storage' + flat-cache@4.0.1: dependencies: flatted: 3.3.1 @@ -5526,6 +6245,8 @@ snapshots: geojson@0.5.0: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.2.6: dependencies: call-bind-apply-helpers: 1.0.1 @@ -5649,6 +6370,8 @@ snapshots: html-url-attributes@3.0.1: {} + http-parser-js@0.5.10: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 @@ -5669,6 +6392,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb@7.1.1: {} + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -6037,6 +6762,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + lodash.castarray@4.4.0: {} lodash.isplainobject@4.0.6: {} @@ -6045,6 +6772,8 @@ snapshots: lodash@4.17.21: {} + long@5.3.2: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -6681,6 +7410,21 @@ snapshots: property-information@6.5.0: {} + protobufjs@7.5.3: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 24.0.8 + long: 5.3.2 + protocol-buffers-schema@3.6.0: {} proxy-from-env@1.1.0: {} @@ -6882,6 +7626,8 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + require-directory@2.1.1: {} + resize-observer-polyfill@1.5.1: {} resolve-from@4.0.0: {} @@ -6942,6 +7688,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -7324,6 +8072,8 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + undici-types@7.8.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -7403,13 +8153,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.0.5: + vite-node@3.0.5(@types/node@24.0.8): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 5.4.8 + vite: 5.4.8(@types/node@24.0.8) transitivePeerDependencies: - '@types/node' - less @@ -7421,29 +8171,30 @@ snapshots: - supports-color - terser - vite-tsconfig-paths@5.0.1(typescript@5.6.2)(vite@5.4.8): + vite-tsconfig-paths@5.0.1(typescript@5.6.2)(vite@5.4.8(@types/node@24.0.8)): dependencies: debug: 4.3.7 globrex: 0.1.2 tsconfck: 3.1.3(typescript@5.6.2) optionalDependencies: - vite: 5.4.8 + vite: 5.4.8(@types/node@24.0.8) transitivePeerDependencies: - supports-color - typescript - vite@5.4.8: + vite@5.4.8(@types/node@24.0.8): dependencies: esbuild: 0.21.5 postcss: 8.4.47 rollup: 4.23.0 optionalDependencies: + '@types/node': 24.0.8 fsevents: 2.3.3 - vitest@3.0.5(@types/debug@4.1.12)(jsdom@26.0.0): + vitest@3.0.5(@types/debug@4.1.12)(@types/node@24.0.8)(jsdom@26.0.0): dependencies: '@vitest/expect': 3.0.5 - '@vitest/mocker': 3.0.5(vite@5.4.8) + '@vitest/mocker': 3.0.5(vite@5.4.8(@types/node@24.0.8)) '@vitest/pretty-format': 3.0.5 '@vitest/runner': 3.0.5 '@vitest/snapshot': 3.0.5 @@ -7459,11 +8210,12 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 5.4.8 - vite-node: 3.0.5 + vite: 5.4.8(@types/node@24.0.8) + vite-node: 3.0.5(@types/node@24.0.8) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 + '@types/node': 24.0.8 jsdom: 26.0.0 transitivePeerDependencies: - less @@ -7486,8 +8238,18 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + web-vitals@4.2.4: {} + webidl-conversions@7.0.0: {} + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 @@ -7579,10 +8341,24 @@ snapshots: xmlchars@2.2.0: {} + y18n@5.0.8: {} + yallist@3.1.1: {} yaml@2.5.1: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} zustand@5.0.3(@types/react@18.3.10)(react@18.3.1): diff --git a/frontend/src/app/index.tsx b/frontend/src/app/index.tsx index 4cb007d13..229bf5f48 100644 --- a/frontend/src/app/index.tsx +++ b/frontend/src/app/index.tsx @@ -10,6 +10,11 @@ import { TourProvider } from "@reactour/tour"; import { APP_TOUR_STEPS } from "@/constants/site-tour"; import { AuthProvider } from "./providers/auth-provider"; import { HelmetProvider } from "react-helmet-async"; +import { initializeApp } from "firebase/app"; +import { firebaseConfig } from "@/config"; + +// Initialize Firebase +initializeApp(firebaseConfig); export const App = () => { const queryClient = new QueryClient({ diff --git a/frontend/src/app/providers/auth-provider.tsx b/frontend/src/app/providers/auth-provider.tsx index 06f4d9712..2d9b19145 100644 --- a/frontend/src/app/providers/auth-provider.tsx +++ b/frontend/src/app/providers/auth-provider.tsx @@ -24,9 +24,9 @@ const AuthContext = createContext({ token: "", user: {} as TUser, authenticateUser: async () => Promise.resolve(), - logout: () => { }, + logout: () => {}, isAuthenticated: false, - setUser: () => { }, + setUser: () => {}, }); export const useAuth = () => { diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx index e380b7d86..2145298d5 100644 --- a/frontend/src/app/router.tsx +++ b/frontend/src/app/router.tsx @@ -363,6 +363,17 @@ const router = createBrowserRouter([ }; }, }, + { + path: APPLICATION_ROUTES.PROFILE_OFFLINE_PREDICTIONS, + lazy: async () => { + const { UserProfileOfflinePredictionsPage } = await import( + "@/app/routes/profile/offline-predictions" + ); + return { + Component: () => , + }; + }, + }, ], }, /** diff --git a/frontend/src/app/routes/datasets/dataset-detail.tsx b/frontend/src/app/routes/datasets/dataset-detail.tsx index 73f75fb59..e3dad7c2d 100644 --- a/frontend/src/app/routes/datasets/dataset-detail.tsx +++ b/frontend/src/app/routes/datasets/dataset-detail.tsx @@ -183,6 +183,7 @@ export const TrainingDatasetsDetailPage = () => { title="Models Using this Dataset" datasetId={data.id} disableStatusFilter + status={0} /> diff --git a/frontend/src/app/routes/models/models-list.tsx b/frontend/src/app/routes/models/models-list.tsx index 6476d0849..5a06a2696 100644 --- a/frontend/src/app/routes/models/models-list.tsx +++ b/frontend/src/app/routes/models/models-list.tsx @@ -21,7 +21,8 @@ import { ModelListGridLayout, ModelListTableLayout, } from "@/features/models/layouts"; -import { LayoutToggle, ModelsMap } from "@/features/models/components"; +import { LayoutToggle } from "@/components/shared/layout-toggle"; +import { ModelsMap } from "@/features/models/components"; import { CategoryFilter, DateRangeFilter, diff --git a/frontend/src/app/routes/profile/offline-predictions.tsx b/frontend/src/app/routes/profile/offline-predictions.tsx new file mode 100644 index 000000000..78bb53b47 --- /dev/null +++ b/frontend/src/app/routes/profile/offline-predictions.tsx @@ -0,0 +1,165 @@ +import { useAuth } from "@/app/providers/auth-provider"; +import { Head } from "@/components/seo"; +import { OrderingFilter, Pagination, SearchFilter } from "@/components/shared"; +import { LayoutToggle } from "@/components/shared/layout-toggle"; +import { LayoutView } from "@/enums"; +import { ProfileSectionHeader } from "@/features/user-profile/components"; +import OfflinePredictionsTable from "@/features/offline-predictions/components/offline-predictions-table"; +import { OfflinePredictionsList } from "@/features/offline-predictions/components/offline-predictions-list"; +import { useOfflinePredictionsQueryParams } from "@/features/offline-predictions/hooks/use-predictions"; +import { SEARCH_PARAMS } from "@/utils/search-params"; +import { useDialog } from "@/hooks/use-dialog"; +import { useState } from "react"; +import { TOfflinePrediction } from "@/types"; +import { PredictionResultDrawer } from "@/features/offline-predictions/components/predictions-results-drawer"; +import { TrainingLogsDialog } from "@/features/offline-predictions/components/dialogs/training-logs-dialog"; +import { CreateMapswipeProjectDialog } from "@/features/mapswipe/components/mapswipe-project-creation-dialog"; + +export const UserProfileOfflinePredictionsPage = () => { + const { user } = useAuth(); + + const { + data, + isError, + isPending, + isPlaceholderData, + query, + updateQuery, + refetch, + } = useOfflinePredictionsQueryParams(user.osm_id); + const { isOpened, openDialog, closeDialog } = useDialog(); + const { + isOpened: isPredictionResultOpened, + openDialog: openPredictionResultDialog, + closeDialog: closePredictionResultDialog, + } = useDialog(); + const { + isOpened: isMapSwipeProjectCreationDialogOpened, + openDialog: openMapSwipeProjectCreationDialog, + closeDialog: closeMapSwipeProjectCreationDialog, + } = useDialog(); + const [activeTaskId, setActiveTaskId] = useState(null); + const [activePrediction, setActivePrediction] = + useState(null); + const handleTrainingLogsModal = (taskId: string) => { + setActiveTaskId(taskId); + openDialog(); + }; + const handlePredictionResultModal = (prediction: TOfflinePrediction) => { + setActivePrediction(prediction); + openPredictionResultDialog(); + }; + + const handleCreateOrViewMapSwipeProject = ( + prediction: TOfflinePrediction, + ) => { + const mapSwipeProjectExists = prediction.mapswipe_id; + if (!mapSwipeProjectExists) { + openMapSwipeProjectCreationDialog(); + } + setActivePrediction(prediction); + }; + return ( + <> + {activePrediction && ( + + )} + {activePrediction && ( + + )} + {activeTaskId && ( + + )} + +
+ {/* Section heading */} +
+ + +
+
+
+

+ {data?.count} prediction + {data?.count && data?.count > 1 ? "s" : ""} +

+ +
+
+ +
+ +
+ +
+
+ {query[SEARCH_PARAMS.layout] === LayoutView.LIST ? ( + + ) : ( + + )} +
+ + ); +}; diff --git a/frontend/src/app/routes/start-mapping.tsx b/frontend/src/app/routes/start-mapping.tsx index a0ccfaec7..96d4c05d0 100644 --- a/frontend/src/app/routes/start-mapping.tsx +++ b/frontend/src/app/routes/start-mapping.tsx @@ -4,7 +4,12 @@ import { START_MAPPING_PAGE_CONTENT, TOAST_NOTIFICATIONS, } from "@/constants"; -import { FitToBounds, LayerControl, ZoomLevel } from "@/components/map"; +import { + DrawControl, + FitToBounds, + LayerControl, + ZoomLevel, +} from "@/components/map"; import { Head } from "@/components/seo"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useMapInstance } from "@/hooks/use-map-instance"; @@ -19,9 +24,19 @@ import { StartMappingMapComponent, StartMappingMobileDrawer, } from "@/features/start-mapping/components"; -import { constructModelCheckpointPath, geoJSONDowloader, openInJOSM, showSuccessToast } from "@/utils"; +import FileUploadDialog from "@/components/shared/modals/file-upload-dialog"; +import { + constructModelCheckpointPath, + featureIsWithinBounds, + geoJSONDowloader, + openInJOSM, + showSuccessToast, + showWarningToast, + uuid4, +} from "@/utils"; import { + MapMode, PredictedFeatureStatus, PredictionImagerySource, PredictionModel, @@ -31,7 +46,12 @@ import { ImagerySourceSelector } from "@/features/start-mapping/components/repli import { useDialog } from "@/hooks/use-dialog"; import { useModelPredictionStore } from "@/store/model-prediction-store"; import { ModelSelector } from "@/features/start-mapping/components/replicable-models/model-selector"; -import { BASE_MODELS, TileServiceType } from "@/enums"; +import { + BASE_MODELS, + DrawingModes, + TileServiceType, + ToolTipPlacement, +} from "@/enums"; import { useTileservice } from "@/hooks/use-tileservice"; import { ALL_MODEL_PREDICTIONS_FILL_LAYER_ID, @@ -39,6 +59,10 @@ import { FAIR_BASE_MODELS_PATH, OPENAERIALMAP_MOSAIC_TILES_URL, } from "@/config"; +import { OfflinePredictionRequestDialog } from "@/features/offline-predictions/components/dialogs/offline-prediction-request-dialog"; +import { GeoJSONStoreFeatures } from "terra-draw"; +import { FileUploadIcon } from "@/components/ui/icons"; +import { ToolTip } from "@/components/ui/tooltip"; export type TDownloadOptions = { name: string; @@ -56,6 +80,7 @@ export const SEARCH_PARAMS = { imagery: "imagery", predictionModelCheckpoint: "checkpoint", tileserver: "tileserver", + mode: "mode", }; export type TQueryParams = { @@ -65,7 +90,11 @@ export type TQueryParams = { export const StartMappingPage = () => { const { modelId } = useParams(); const [searchParams, setSearchParams] = useSearchParams(); - const { map, mapContainerRef } = useMapInstance(false, true); + const { map, mapContainerRef, setDrawingMode, terraDraw } = useMapInstance( + false, + true, + ); + const { isSmallViewport } = useScreenSize(); const { @@ -74,6 +103,9 @@ export const StartMappingPage = () => { updateFeatureStatus, } = useModelPredictionStore(); + const [offlinePredictionAOI, setOfflinePredictionAOI] = + useState(null); + const acceptedFeatures = useMemo( () => modelPredictions.filter( @@ -83,6 +115,7 @@ export const StartMappingPage = () => { ); const navigate = useNavigate(); + const [openMobileDrawer, setOpenMobileDrawer] = useState(isSmallViewport); @@ -94,6 +127,10 @@ export const StartMappingPage = () => { customPredictionModelCheckpointPath, setCustomPredictionModelCheckpointPath, ] = useState(""); + const currentMode = searchParams.get(SEARCH_PARAMS.mode) ?? MapMode.ONLINE; + const setCurrentMode = (newMode: MapMode) => { + updateQuery({ [SEARCH_PARAMS.mode]: newMode }); + }; const customTileServerURL = searchParams.get(SEARCH_PARAMS.tileserver) || ""; @@ -135,6 +172,17 @@ export const StartMappingPage = () => { closeDialog: closeModelSelectionDialog, } = useDialog(); + const { + openDialog: openOfflinePredictionRequestDialog, + isOpened: isOfflinePredictionRequestDialogOpened, + closeDialog: closeOfflinePredictionDialog, + } = useDialog(); + + const { + openDialog: openFileUploadDialog, + isOpened: isFileUploadDialogOpened, + closeDialog: closeFileUploadDialog, + } = useDialog(); const { isError, isPending: modelInfoRequestIspending, @@ -142,8 +190,6 @@ export const StartMappingPage = () => { error, } = useModelDetails(modelId as string, !!modelId); - - const updateQuery = useCallback( (newParams: TQueryParams) => { setQuery((prev) => ({ ...prev, ...newParams })); @@ -185,24 +231,30 @@ export const StartMappingPage = () => { */ useEffect(() => { const urlCp = searchParams.get(SEARCH_PARAMS.predictionModelCheckpoint); - if (urlCp && urlCp.length > 0 && urlCp !== "undefined" && predictionModel === PredictionModel.CUSTOM) { + if ( + urlCp && + urlCp.length > 0 && + urlCp !== "undefined" && + predictionModel === PredictionModel.CUSTOM + ) { setCustomPredictionModelCheckpointPath(urlCp); setPredictionModelCheckpoint(urlCp); } }, [predictionModel]); - - useEffect(() => { /** * Only update the checkpoint if the modelInfo is available and * the predictionModel is not set or is set to the default model. */ - if (modelInfo && (!predictionModel || predictionModel === PredictionModel.DEFAULT)) { - setPredictionModelCheckpoint(constructModelCheckpointPath(modelInfo)) + if ( + modelInfo && + (!predictionModel || predictionModel === PredictionModel.DEFAULT) + ) { + setPredictionModelCheckpoint(constructModelCheckpointPath(modelInfo)); } else if (predictionModel && predictionModel !== PredictionModel.CUSTOM) { setPredictionModelCheckpoint( - FAIR_BASE_MODELS_PATH[predictionModel as BASE_MODELS] + FAIR_BASE_MODELS_PATH[predictionModel as BASE_MODELS], ); } }, [predictionModel, modelInfo]); @@ -214,7 +266,7 @@ export const StartMappingPage = () => { if (predictionImagerySource === PredictionImagerySource.Kontour) { setTileserverURL(OPENAERIALMAP_MOSAIC_TILES_URL); } - }, [predictionImagerySource]) + }, [predictionImagerySource]); /** * When the user changes the prediction imagery source, sync it to the URL. @@ -276,6 +328,146 @@ export const StartMappingPage = () => { } }, [isError, error, navigate]); + /** + * Set the drawing mode based on the current mode. + * If the current mode is OFFLINE, set the drawing mode to POLYGON. + * If the current mode is ONLINE, set the drawing mode to STATIC. + */ + useEffect(() => { + if (currentMode === MapMode.OFFLINE) { + setDrawingMode(DrawingModes.POLYGON); + } else { + setDrawingMode(DrawingModes.STATIC); + } + }, [currentMode, setDrawingMode]); + + /** + * Check if the user has drawn an AOI in offline mode. + */ + const hasDrawnAOI = useMemo(() => { + return offlinePredictionAOI !== null; + }, [currentMode, offlinePredictionAOI]); + + /** + * Check if the offline prediction AOI is valid. + */ + const isOfflineMode = useMemo( + () => currentMode === MapMode.OFFLINE, + [currentMode], + ); + + /** + * Handle the finish event of the TerraDraw instance. + * It checks if the drawn feature is within the bounds of the OAM imagery if it exists. + * If the feature is valid, it sets the offline prediction AOI state. + * If there are multiple features drawn, it removes all but the first one. + */ + const handleDrawFinish = useCallback( + (feature?: Feature) => { + if (!terraDraw) return; + + let features: Feature[] = []; + + if (feature) { + // If a geometry is provided, wrap it as a Feature + features = [feature]; + } else { + // Otherwise, get features from terraDraw + features = terraDraw.getSnapshot(); + } + + if (!features || features.length === 0) return; + + // Check if the feature is within the bounds of the OAM imagery if it exists + if (tileJSONMetadata?.bounds) { + if (!featureIsWithinBounds(tileJSONMetadata.bounds, features[0])) { + showWarningToast( + "The drawn polygon extends beyond the imagery bounds. Please ensure the polygon AOI is within the imagery bounds.", + ); + if (!feature && terraDraw) { + terraDraw.removeFeatures( + features + .slice(0) + .map((f) => f.id) + .filter((id): id is string | number => id !== undefined), + ); + } + return; + } + } + if (features.length > 1) { + showWarningToast( + "Only one polygon can be drawn at a time. Please delete the existing polygon before drawing a new one.", + ); + // Remove the last drawn feature, keeping only the first one + if (!feature && terraDraw) { + terraDraw.removeFeatures( + features + .slice(1) + .map((f) => f.id) + .filter((id): id is string | number => id !== undefined), + ); + } + return; + } + // If a feature is provided, add it to the TerraDraw instance + if (feature) { + terraDraw.addFeatures([features[0]] as GeoJSONStoreFeatures[]); + } + setOfflinePredictionAOI(features[0]); + showSuccessToast("AOI drawn successfully."); + }, + [terraDraw, tileJSONMetadata], + ); + + /** + * Effect to handle the completion of drawing in TerraDraw. + * It listens for changes in the TerraDraw instance and updates the drawn feature state. + * If a feature is drawn, it clears previous features except the last one. + */ + useEffect(() => { + if (!terraDraw) return; + + const onFinish = () => { + handleDrawFinish(); + }; + + terraDraw.on("finish", onFinish); + + return () => { + terraDraw.off("finish", onFinish); + }; + }, [terraDraw, handleDrawFinish]); + + /** + * Effect to set the current mode to OFFLINE if an AOI has been drawn. + * This is to ensure that the user can start offline predictions after drawing an AOI. + */ + useEffect(() => { + if (hasDrawnAOI && currentMode !== MapMode.OFFLINE) { + setCurrentMode(MapMode.OFFLINE); + } + }, [hasDrawnAOI, currentMode, setCurrentMode]); + /** + * Handle the drawing state change. + * If the user starts drawing, set the current mode to OFFLINE. + * If the user stops drawing, set the current mode to ONLINE and reset the drawing mode to STATIC. + */ + const handleDrawingStateChange = useCallback( + (isDrawing: boolean) => { + if (isDrawing) { + setCurrentMode(MapMode.OFFLINE); + } else { + setCurrentMode(MapMode.ONLINE); + setDrawingMode(DrawingModes.STATIC); + } + }, + [setCurrentMode], + ); + + /** + * Check if the model predictions exist. + */ const modelPredictionsExist = useMemo(() => { if (!modelPredictions) return false; return modelPredictions.length > 0; @@ -285,16 +477,16 @@ export const StartMappingPage = () => { () => [ ...(modelPredictions.length > 0 ? [ - { - value: - START_MAPPING_PAGE_CONTENT.map.controls.legendControl - .predictionResults, - subLayers: [ - ALL_MODEL_PREDICTIONS_FILL_LAYER_ID, - ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, - ], - }, - ] + { + value: + START_MAPPING_PAGE_CONTENT.map.controls.legendControl + .predictionResults, + subLayers: [ + ALL_MODEL_PREDICTIONS_FILL_LAYER_ID, + ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, + ], + }, + ] : []), ], [modelPredictions], @@ -311,6 +503,35 @@ export const StartMappingPage = () => { showSuccessToast(TOAST_NOTIFICATIONS.startMapping.fileDownloadSuccess); }, [modelPredictions, modelInfo]); + /** + * Handle the deletion of the AOI. + * It clears the TerraDraw instance and sets the offline prediction AOI state to null. + * It also shows a success toast message. + */ + const handleAOIDelete = useCallback(() => { + if (!terraDraw) return; + terraDraw.clear(); + setOfflinePredictionAOI(null); + showSuccessToast("AOI cleared successfully."); + }, [terraDraw]); + + /** + * Reset the offline prediction mode state. + * It sets the drawing mode to STATIC, clears the TerraDraw instance, + * and sets the current mode to ONLINE. + */ + const resetOfflinePredictionModeState = useCallback(() => { + setDrawingMode(DrawingModes.STATIC); + setOfflinePredictionAOI(null); + terraDraw?.clear(); + setCurrentMode(MapMode.ONLINE); + }, [setDrawingMode, setCurrentMode, terraDraw]); + + /** + * Handle the download of accepted features. + * It creates a GeoJSON file with the accepted features and triggers a download. + * It also shows a success toast message. + */ const handleAcceptedFeaturesDownload = useCallback(async () => { geoJSONDowloader( { type: "FeatureCollection", features: acceptedFeatures }, @@ -319,6 +540,10 @@ export const StartMappingPage = () => { showSuccessToast(TOAST_NOTIFICATIONS.startMapping.fileDownloadSuccess); }, [acceptedFeatures, modelInfo]); + /** + * Handle the download of features to JOSM. + * It opens the features in JOSM with the provided dataset name and source imagery. + */ const handleFeaturesDownloadToJOSM = useCallback( (features: Feature[]) => { if (!map || !modelInfo?.dataset) return; @@ -332,10 +557,18 @@ export const StartMappingPage = () => { [map, modelInfo], ); + /** + * Handle the download of all features to JOSM. + * It calls the handleFeaturesDownloadToJOSM function with the model predictions. + */ const handleAllFeaturesDownloadToJOSM = useCallback(() => { handleFeaturesDownloadToJOSM(modelPredictions); }, [handleFeaturesDownloadToJOSM, modelPredictions]); + /** + * Handle the download of accepted features to JOSM. + * It calls the handleFeaturesDownloadToJOSM function with the accepted features. + */ const handleAcceptedFeaturesDownloadToJOSM = useCallback(() => { handleFeaturesDownloadToJOSM(acceptedFeatures); }, [handleFeaturesDownloadToJOSM, acceptedFeatures]); @@ -382,6 +615,10 @@ export const StartMappingPage = () => { }, ]; + /** + * Handle the opening of the prediction imagery dialog. + * It closes the mobile drawer to prevent focus trapping issues with vaul. + */ const handlePredictionImageryDialogOpen = useCallback(() => { /** * Close the mobile drawer when the prediction imagery dialog is opened to prevent focus trapping issues with vaul. @@ -390,11 +627,19 @@ export const StartMappingPage = () => { openDialog(); }, [openDialog, setOpenMobileDrawer]); + /** + * Handle the closing of the prediction imagery dialog. + * It reopens the mobile drawer to allow the user to interact with it again. + */ const handlePredictionImageryDialogClose = useCallback(() => { setOpenMobileDrawer(true); closeDialog(); }, [closeDialog, setOpenMobileDrawer]); + /** + * Handle the opening of the prediction model dialog. + * It closes the mobile drawer to prevent focus trapping issues with vaul. + */ const handlePredictionModelDialogOpen = useCallback(() => { /** * Close the mobile drawer when the model selection dialog is opened to prevent focus trapping issues with vaul. @@ -403,6 +648,10 @@ export const StartMappingPage = () => { openModelSelectionDialog(); }, [openModelSelectionDialog, setOpenMobileDrawer]); + /** + * Handle the closing of the prediction model dialog. + * It reopens the mobile drawer to allow the user to interact with it again. + */ const handlePredictionModelDialogClose = useCallback(() => { setOpenMobileDrawer(true); closeModelSelectionDialog(); @@ -411,6 +660,40 @@ export const StartMappingPage = () => { return ( <> + { + handleDrawFinish({ + type: "Feature", + geometry: polygon, + id: uuid4(), + properties: { + mode: DrawingModes.POLYGON, + }, + }); + await new Promise((resolve) => setTimeout(resolve, 200)); // Simulate a delay + closeFileUploadDialog(); + }} + disabled={false} + maxFiles={1} + buttonText="Add to Map" + /> +
{/* Base model dialog */} { modelPredictions={modelPredictions} setModelPredictions={setModelPredictions} isSmallViewport={isSmallViewport} + isOfflineMode={isOfflineMode} + hasDrawnAOI={hasDrawnAOI} + openOfflinePredictionRequestDialog={ + openOfflinePredictionRequestDialog + } /> )} {/* Mobile bottom sheet */} @@ -530,6 +818,11 @@ export const StartMappingPage = () => { modelPredictions={modelPredictions} setModelPredictions={setModelPredictions} isSmallViewport={isSmallViewport} + isOfflineMode={isOfflineMode} + hasDrawnAOI={hasDrawnAOI} + openOfflinePredictionRequestDialog={ + openOfflinePredictionRequestDialog + } />
@@ -541,6 +834,43 @@ export const StartMappingPage = () => {
+
+ {terraDraw && map && ( + + )} +
+ {terraDraw && map && ( +
+ + + +
+ )}
{ modelPredictions={modelPredictions} updateFeatureStatus={updateFeatureStatus} tileServerURL={tileserverURL as string} + handleDrawingStateChange={handleDrawingStateChange} + setDrawingMode={setDrawingMode} + terraDraw={terraDraw} + isOfflineMode={isOfflineMode} + hasDrawnAOI={hasDrawnAOI} + handleAOIDelete={handleAOIDelete} + openFileUploadDialog={openFileUploadDialog} />
diff --git a/frontend/src/assets/fonts/Archivo/Archivo-Black.ttf b/frontend/src/assets/fonts/Archivo/Archivo-Black.ttf deleted file mode 100644 index 3793794d9..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-Black.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-BlackItalic.ttf b/frontend/src/assets/fonts/Archivo/Archivo-BlackItalic.ttf deleted file mode 100644 index 67923b28e..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-BlackItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-Bold.ttf b/frontend/src/assets/fonts/Archivo/Archivo-Bold.ttf deleted file mode 100644 index fa4f3f075..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-Bold.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-BoldItalic.ttf b/frontend/src/assets/fonts/Archivo/Archivo-BoldItalic.ttf deleted file mode 100644 index edebbd1c7..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-BoldItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-ExtraBold.ttf b/frontend/src/assets/fonts/Archivo/Archivo-ExtraBold.ttf deleted file mode 100644 index 3badac36b..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-ExtraBold.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-ExtraBoldItalic.ttf b/frontend/src/assets/fonts/Archivo/Archivo-ExtraBoldItalic.ttf deleted file mode 100644 index 2cd4789ba..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-ExtraBoldItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-ExtraLight.ttf b/frontend/src/assets/fonts/Archivo/Archivo-ExtraLight.ttf deleted file mode 100644 index 879a6872e..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-ExtraLight.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-ExtraLightItalic.ttf b/frontend/src/assets/fonts/Archivo/Archivo-ExtraLightItalic.ttf deleted file mode 100644 index f81c1d1dc..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-ExtraLightItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-Italic.ttf b/frontend/src/assets/fonts/Archivo/Archivo-Italic.ttf deleted file mode 100644 index 33967697d..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-Italic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-Light.ttf b/frontend/src/assets/fonts/Archivo/Archivo-Light.ttf deleted file mode 100644 index 6962b4c83..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-Light.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-LightItalic.ttf b/frontend/src/assets/fonts/Archivo/Archivo-LightItalic.ttf deleted file mode 100644 index d70151706..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-LightItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-Medium.ttf b/frontend/src/assets/fonts/Archivo/Archivo-Medium.ttf deleted file mode 100644 index 08c15c63d..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-Medium.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-MediumItalic.ttf b/frontend/src/assets/fonts/Archivo/Archivo-MediumItalic.ttf deleted file mode 100644 index a79495378..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-MediumItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-Regular.ttf b/frontend/src/assets/fonts/Archivo/Archivo-Regular.ttf deleted file mode 100644 index 936d78644..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-Regular.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-SemiBold.ttf b/frontend/src/assets/fonts/Archivo/Archivo-SemiBold.ttf deleted file mode 100644 index ac49d880d..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-SemiBold.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-SemiBoldItalic.ttf b/frontend/src/assets/fonts/Archivo/Archivo-SemiBoldItalic.ttf deleted file mode 100644 index a39a9a4d8..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-SemiBoldItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-Thin.ttf b/frontend/src/assets/fonts/Archivo/Archivo-Thin.ttf deleted file mode 100644 index 654fa7725..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-Thin.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Archivo/Archivo-ThinItalic.ttf b/frontend/src/assets/fonts/Archivo/Archivo-ThinItalic.ttf deleted file mode 100644 index 0d783b7a3..000000000 Binary files a/frontend/src/assets/fonts/Archivo/Archivo-ThinItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Black.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Black.ttf deleted file mode 100644 index f5329d16f..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Black.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-BlackItalic.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-BlackItalic.ttf deleted file mode 100644 index 8452a3a78..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-BlackItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Bold.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Bold.ttf deleted file mode 100644 index 256f92475..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Bold.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-BoldItalic.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-BoldItalic.ttf deleted file mode 100644 index f3931738f..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-BoldItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ExtraBold.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ExtraBold.ttf deleted file mode 100644 index 3ee08949c..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ExtraBold.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ExtraBoldItalic.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ExtraBoldItalic.ttf deleted file mode 100644 index 9a31493fa..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ExtraBoldItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ExtraLight.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ExtraLight.ttf deleted file mode 100644 index 35ec98d6f..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ExtraLight.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ExtraLightItalic.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ExtraLightItalic.ttf deleted file mode 100644 index 507a387ae..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ExtraLightItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Italic.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Italic.ttf deleted file mode 100644 index 6df5e6b3e..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Italic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Light.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Light.ttf deleted file mode 100644 index 1776e63f4..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Light.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-LightItalic.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-LightItalic.ttf deleted file mode 100644 index 3cc79a7bb..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-LightItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Medium.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Medium.ttf deleted file mode 100644 index 82e45ac06..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Medium.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-MediumItalic.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-MediumItalic.ttf deleted file mode 100644 index 8b0335713..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-MediumItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Regular.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Regular.ttf deleted file mode 100644 index 9f3aab8cc..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Regular.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-SemiBold.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-SemiBold.ttf deleted file mode 100644 index 86c6801da..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-SemiBold.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-SemiBoldItalic.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-SemiBoldItalic.ttf deleted file mode 100644 index 194653445..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-SemiBoldItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Thin.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Thin.ttf deleted file mode 100644 index 8cd93e512..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-Thin.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ThinItalic.ttf b/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ThinItalic.ttf deleted file mode 100644 index 2364ec5ba..000000000 Binary files a/frontend/src/assets/fonts/Barlow Condensed/BarlowCondensed-ThinItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-Black.ttf b/frontend/src/assets/fonts/Barlow/Barlow-Black.ttf deleted file mode 100644 index 9675b8ae4..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-Black.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-BlackItalic.ttf b/frontend/src/assets/fonts/Barlow/Barlow-BlackItalic.ttf deleted file mode 100644 index 231459530..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-BlackItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-BoldItalic.ttf b/frontend/src/assets/fonts/Barlow/Barlow-BoldItalic.ttf deleted file mode 100644 index 9dc16aded..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-BoldItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-ExtraBold.ttf b/frontend/src/assets/fonts/Barlow/Barlow-ExtraBold.ttf deleted file mode 100644 index 22901110f..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-ExtraBold.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-ExtraBoldItalic.ttf b/frontend/src/assets/fonts/Barlow/Barlow-ExtraBoldItalic.ttf deleted file mode 100644 index 9e30cef69..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-ExtraBoldItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-ExtraLight.ttf b/frontend/src/assets/fonts/Barlow/Barlow-ExtraLight.ttf deleted file mode 100644 index 34f025145..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-ExtraLight.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-ExtraLightItalic.ttf b/frontend/src/assets/fonts/Barlow/Barlow-ExtraLightItalic.ttf deleted file mode 100644 index 14f2370f8..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-ExtraLightItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-Italic.ttf b/frontend/src/assets/fonts/Barlow/Barlow-Italic.ttf deleted file mode 100644 index dc46deb27..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-Italic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-Light.ttf b/frontend/src/assets/fonts/Barlow/Barlow-Light.ttf deleted file mode 100644 index f3c5b701e..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-Light.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-LightItalic.ttf b/frontend/src/assets/fonts/Barlow/Barlow-LightItalic.ttf deleted file mode 100644 index bf7828689..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-LightItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-Medium.ttf b/frontend/src/assets/fonts/Barlow/Barlow-Medium.ttf deleted file mode 100644 index 11d4ab205..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-Medium.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-MediumItalic.ttf b/frontend/src/assets/fonts/Barlow/Barlow-MediumItalic.ttf deleted file mode 100644 index 62a4fb215..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-MediumItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-Regular.ttf b/frontend/src/assets/fonts/Barlow/Barlow-Regular.ttf deleted file mode 100644 index d39c293ef..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-Regular.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-SemiBold.ttf b/frontend/src/assets/fonts/Barlow/Barlow-SemiBold.ttf deleted file mode 100644 index 58a643058..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-SemiBold.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-SemiBoldItalic.ttf b/frontend/src/assets/fonts/Barlow/Barlow-SemiBoldItalic.ttf deleted file mode 100644 index 8cbb7bff3..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-SemiBoldItalic.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-Thin.ttf b/frontend/src/assets/fonts/Barlow/Barlow-Thin.ttf deleted file mode 100644 index a9d7cb968..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-Thin.ttf and /dev/null differ diff --git a/frontend/src/assets/fonts/Barlow/Barlow-ThinItalic.ttf b/frontend/src/assets/fonts/Barlow/Barlow-ThinItalic.ttf deleted file mode 100644 index 8678b9971..000000000 Binary files a/frontend/src/assets/fonts/Barlow/Barlow-ThinItalic.ttf and /dev/null differ diff --git a/frontend/src/components/layouts/navbar/nav-logo.tsx b/frontend/src/components/layouts/navbar/nav-logo.tsx index 5a89b7c66..a9e894682 100644 --- a/frontend/src/components/layouts/navbar/nav-logo.tsx +++ b/frontend/src/components/layouts/navbar/nav-logo.tsx @@ -15,22 +15,22 @@ export const NavLogo = ({ onClick ? onClick() : navigate(APPLICATION_ROUTES.HOMEPAGE); }; - const width = smallerSize ? "40px" : "60px"; - const height = smallerSize ? "40px" : "22px"; - return ( ); }; diff --git a/frontend/src/components/layouts/navbar/user-profile.tsx b/frontend/src/components/layouts/navbar/user-profile.tsx index 481542f0e..bbbed6c0d 100644 --- a/frontend/src/components/layouts/navbar/user-profile.tsx +++ b/frontend/src/components/layouts/navbar/user-profile.tsx @@ -44,6 +44,12 @@ export const UserProfile = ({ navigate(APPLICATION_ROUTES.PROFILE_MODELS); }, }, + { + value: SHARED_CONTENT.navbar.userProfile.offlinePredictions, + onClick: () => { + navigate(APPLICATION_ROUTES.PROFILE_OFFLINE_PREDICTIONS); + }, + }, { value: SHARED_CONTENT.navbar.userProfile.settings, onClick: () => { diff --git a/frontend/src/components/layouts/root-layout.tsx b/frontend/src/components/layouts/root-layout.tsx index d9f22d058..021b5d77e 100644 --- a/frontend/src/components/layouts/root-layout.tsx +++ b/frontend/src/components/layouts/root-layout.tsx @@ -8,9 +8,7 @@ import { useEffect, useState } from "react"; import { useScrollToTop } from "@/hooks/use-scroll-to-element"; import { useAuth } from "@/app/providers/auth-provider"; import { AuthenticationModal } from "@/components/auth"; -import { - BANNER_TIMEOUT_DURATION, -} from "@/config"; +import { BANNER_TIMEOUT_DURATION } from "@/config"; export const RootLayout = () => { const { pathname, state } = useLocation(); @@ -45,8 +43,6 @@ export const RootLayout = () => { */ const { modelId } = useParams(); - - return ( <> diff --git a/frontend/src/components/map/controls/draw-control.tsx b/frontend/src/components/map/controls/draw-control.tsx index 352ab824e..f039bbe1e 100644 --- a/frontend/src/components/map/controls/draw-control.tsx +++ b/frontend/src/components/map/controls/draw-control.tsx @@ -1,66 +1,75 @@ +import { useCallback } from "react"; import { DrawingModes, ToolTipPlacement } from "@/enums"; -import { PenIcon } from "@/components/ui/icons"; +import { DeleteIcon } from "@/components/ui/icons"; import { TerraDraw } from "terra-draw"; import { ToolTip } from "@/components/ui/tooltip"; -import { useCallback } from "react"; +import { DrawIcon } from "@/components/ui/icons/draw-icon"; -export const DrawControl = ({ - drawingMode, - terraDraw, - setDrawingMode, -}: { +type DrawControlProps = { drawingMode: DrawingModes; terraDraw?: TerraDraw; setDrawingMode: (newMode: DrawingModes) => void; -}) => { - const changeMode = useCallback( - (newMode: DrawingModes) => { - terraDraw?.setMode(newMode); - setDrawingMode(newMode); - }, - [terraDraw], - ); + showDeleteButton?: boolean; + onDelete?: () => void; + onDrawingStateChange?: (isDrawing: boolean) => void; + drawingIsActive?: boolean; +}; - const renderButton = ( - currentMode: DrawingModes, - activeMode: DrawingModes, - label: string, - isActive: boolean, - ) => ( - - - - ); +export const DrawControl = ({ + drawingMode, + terraDraw, + setDrawingMode, + showDeleteButton = false, + onDelete, + onDrawingStateChange, + drawingIsActive = false, +}: DrawControlProps) => { + const handleClick = useCallback(() => { + if (drawingIsActive) { + setDrawingMode(DrawingModes.STATIC); + terraDraw?.setMode(DrawingModes.STATIC); + onDrawingStateChange?.(false); + } else { + setDrawingMode(drawingMode); + terraDraw?.setMode(drawingMode); + onDrawingStateChange?.(true); + } + }, [drawingIsActive, drawingMode, setDrawingMode, terraDraw]); return ( - <> - {renderButton( - drawingMode, - DrawingModes.RECTANGLE, - drawingMode === DrawingModes.STATIC ? "Draw AOI" : "Cancel", - drawingMode === DrawingModes.RECTANGLE, +
+
+ + + +
+ + {showDeleteButton && ( + + + )} - +
); }; diff --git a/frontend/src/components/map/map.tsx b/frontend/src/components/map/map.tsx index 25ad789b1..9baeed631 100644 --- a/frontend/src/components/map/map.tsx +++ b/frontend/src/components/map/map.tsx @@ -9,7 +9,6 @@ import "maplibre-gl/dist/maplibre-gl.css"; import { GeolocationControl, FitToBounds, - DrawControl, ZoomLevel, LayerControl, ZoomControls, @@ -19,7 +18,7 @@ import { TileServiceLayer } from "@/hooks/tile-service-layer"; type MapComponentProps = { geolocationControl?: boolean; controlsPosition?: ControlsPosition; - drawControl?: boolean; + showCurrentZoom?: boolean; layerControl?: boolean; layerControlLayers?: { @@ -45,7 +44,7 @@ type MapComponentProps = { export const MapComponent: React.FC = ({ geolocationControl = false, controlsPosition = ControlsPosition.TOP_RIGHT, - drawControl = false, + showCurrentZoom = false, layerControl = false, layerControlLayers = [], @@ -56,10 +55,7 @@ export const MapComponent: React.FC = ({ bounds, mapContainerRef, map, - terraDraw, - drawingMode, zoomControls = true, - setDrawingMode, tileServiceURL, hasTileServiceLayer = false, }) => { @@ -76,13 +72,6 @@ export const MapComponent: React.FC = ({ > {zoomControls ? : null} {geolocationControl && } - {drawControl && terraDraw && drawingMode && setDrawingMode && ( - - )} {fitToBounds && (
diff --git a/frontend/src/components/map/setups/setup-terra-draw.ts b/frontend/src/components/map/setups/setup-terra-draw.ts index b4d8d60c7..5d39414a9 100644 --- a/frontend/src/components/map/setups/setup-terra-draw.ts +++ b/frontend/src/components/map/setups/setup-terra-draw.ts @@ -4,6 +4,8 @@ import { ValidateNotSelfIntersecting, TerraDrawRectangleMode, TerraDrawExtend, + TerraDrawSelectMode, + TerraDrawPolygonMode, } from "terra-draw"; import { TerraDrawMapLibreGLAdapter } from "terra-draw-maplibre-gl-adapter"; import { @@ -18,7 +20,7 @@ export const setupTerraDraw = (map: maplibregl.Map) => { tracked: true, adapter: new TerraDrawMapLibreGLAdapter({ map, - coordinatePrecision: 16, + coordinatePrecision: 20, }), // idStrategy: { // isValidId: () => true, @@ -30,21 +32,39 @@ export const setupTerraDraw = (map: maplibregl.Map) => { // })(), // }, modes: [ - // new TerraDrawSelectMode({ - // flags: { - // arbitary: { - // feature: {}, - // }, - // rectangle: { - // feature: { - // draggable: true, - // coordinates: { - // resizable: "opposite", - // }, - // }, - // }, - // }, - // }), + new TerraDrawSelectMode({ + flags: { + arbitary: { + feature: {}, + }, + rectangle: { + feature: { + draggable: true, + coordinates: { + resizable: "opposite", + }, + }, + }, + polygon: { + feature: { + draggable: true, + coordinates: { + midpoints: true, + draggable: true, + deletable: true, + }, + }, + }, + }, + }), + new TerraDrawPolygonMode({ + validation: (feature, { updateType }) => { + if (updateType === "finish" || updateType === "commit") { + return ValidateNotSelfIntersecting(feature); + } + return { valid: true }; + }, + }), new TerraDrawRectangleMode({ validation: (feature, { updateType }) => { if (updateType === "finish" || updateType === "commit") { diff --git a/frontend/src/components/shared/hot-tracking.tsx b/frontend/src/components/shared/hot-tracking.tsx index 0ebfc22e3..46a50012d 100644 --- a/frontend/src/components/shared/hot-tracking.tsx +++ b/frontend/src/components/shared/hot-tracking.tsx @@ -28,4 +28,4 @@ export const HotTracking = ({ homepagePath = APPLICATION_ROUTES.HOMEPAGE }) => { }, [pathname, homepagePath]); return null; -}; \ No newline at end of file +}; diff --git a/frontend/src/features/models/components/layout-toggle.tsx b/frontend/src/components/shared/layout-toggle.tsx similarity index 83% rename from frontend/src/features/models/components/layout-toggle.tsx rename to frontend/src/components/shared/layout-toggle.tsx index 56eecf206..b0ffc375c 100644 --- a/frontend/src/features/models/components/layout-toggle.tsx +++ b/frontend/src/components/shared/layout-toggle.tsx @@ -5,22 +5,25 @@ import { TQueryParams } from "@/types"; import { useScrollToTop } from "@/hooks/use-scroll-to-element"; import { ToolTip } from "@/components/ui/tooltip"; -const LayoutToggle = ({ +export const LayoutToggle = ({ query, updateQuery, isMobile, disabled = false, + iconSize = "icon-lg", }: { updateQuery: (params: TQueryParams) => void; query: TQueryParams; isMobile?: boolean; disabled?: boolean; + iconSize?: string; }) => { const activeLayout = query[SEARCH_PARAMS.layout]; const { scrollToTop } = useScrollToTop(); + return ( ); }; - -export default LayoutToggle; diff --git a/frontend/src/features/model-creation/components/dialogs/file-upload-dialog.tsx b/frontend/src/components/shared/modals/file-upload-dialog.tsx similarity index 90% rename from frontend/src/features/model-creation/components/dialogs/file-upload-dialog.tsx rename to frontend/src/components/shared/modals/file-upload-dialog.tsx index a57e0747e..c82ead344 100644 --- a/frontend/src/features/model-creation/components/dialogs/file-upload-dialog.tsx +++ b/frontend/src/components/shared/modals/file-upload-dialog.tsx @@ -9,7 +9,6 @@ import { SlFormatBytes } from "@shoelace-style/shoelace/dist/react"; import { Spinner } from "@/components/ui/spinner"; import { useCallback, useState } from "react"; import { - MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE, MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS, MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS, MAX_TRAINING_AREA_SIZE, @@ -33,6 +32,9 @@ type FileUploadDialogProps = DialogProps & { disableFileSizeValidation?: boolean; isAOILabelsUpload?: boolean; rawFileUploadHandler?: (formData: FormData) => Promise; + maxFiles?: number; + buttonText?: string; + additionalInstruction?: string; }; const isPolygonGeometry = ( @@ -57,6 +59,9 @@ const FileUploadDialog: React.FC = ({ // AOI labels are uploaded as raw GeoJSON file isAOILabelsUpload = false, rawFileUploadHandler, + maxFiles = MAX_TRAINING_AREA_UPLOAD_FILE_SIZE, + buttonText = MODELS_CONTENT.modelCreation.trainingArea.form.upload, + additionalInstruction, }) => { const [acceptedFiles, setAcceptedFiles] = useState([]); const [uploadInProgress, setUploadInProgress] = useState(false); @@ -74,7 +79,7 @@ const FileUploadDialog: React.FC = ({ if (file.size > MAX_TRAINING_AREA_UPLOAD_FILE_SIZE) { showErrorToast( undefined, - `File ${file.name} is too large (max ${formatAreaInAppropriateUnit(MAX_TRAINING_AREA_UPLOAD_FILE_SIZE)})`, + `File ${file.name} is too large (max ${MAX_TRAINING_AREA_UPLOAD_FILE_SIZE / 1024 / 1024} MB)`, ); return false; } @@ -91,13 +96,10 @@ const FileUploadDialog: React.FC = ({ if (geojson.type === "FeatureCollection") { if (!isAOILabelsUpload) { // Validate the number of features in the collection. - if ( - geojson.features.length > - MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE - ) { + if (geojson.features.length > maxFiles) { showErrorToast( undefined, - `File ${file.name} exceeds limit of ${MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE} polygon features.`, + `File ${file.name} exceeds limit of ${maxFiles} polygon features.`, ); continue; } @@ -136,6 +138,7 @@ const FileUploadDialog: React.FC = ({ accept: { "application/json": [".geojson", ".json"], }, + maxFiles: isAOILabelsUpload ? MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS : MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS, @@ -179,7 +182,7 @@ const FileUploadDialog: React.FC = ({ ) { showErrorToast( undefined, - `File area for ${file.name} exceeds area limit.`, + `File area for ${file.name} does not satisfy size limit.`, ); continue; } @@ -307,11 +310,25 @@ const FileUploadDialog: React.FC = ({ .fleSizeInstruction } + + {!disableFileSizeValidation && ( + + {`Max file size: ${formatAreaInAppropriateUnit( + MAX_TRAINING_AREA_UPLOAD_FILE_SIZE, + )}.`} + + )} + {!disableFileSizeValidation && ( {`Area should be > ${formatAreaInAppropriateUnit(MIN_TRAINING_AREA_SIZE)} and < ${formatAreaInAppropriateUnit(MAX_TRAINING_AREA_SIZE)}.`} )} + {additionalInstruction && ( + + {additionalInstruction} + + )} )}
@@ -333,7 +350,7 @@ const FileUploadDialog: React.FC = ({ ) : ( - MODELS_CONTENT.modelCreation.trainingArea.form.upload + buttonText )} diff --git a/frontend/src/components/shared/model-explorer.tsx b/frontend/src/components/shared/model-explorer.tsx index 33d275f53..7ace75037 100644 --- a/frontend/src/components/shared/model-explorer.tsx +++ b/frontend/src/components/shared/model-explorer.tsx @@ -1,5 +1,5 @@ import ModelNotFound from "@/features/models/components/model-not-found"; -import { LayoutToggle } from "@/features/models/components"; +import { LayoutToggle } from "@/components/shared/layout-toggle"; import { LayoutView } from "@/enums"; import { MobileModelFiltersDialog } from "@/features/models/components/dialogs"; import { MODELS_CONTENT } from "@/constants"; @@ -33,7 +33,9 @@ export const ModelExplorer = ({ createRoute, createButtonAlt, userId, - datasetId, disableStatusFilter + datasetId, + disableStatusFilter, + status, }: { disableCreateNewButton?: boolean; title?: string; @@ -42,6 +44,7 @@ export const ModelExplorer = ({ userId?: number; datasetId?: number; disableStatusFilter?: boolean; + status?: number; }) => { const { isOpened, openDialog, closeDialog } = useDialog(); @@ -53,7 +56,7 @@ export const ModelExplorer = ({ isPlaceholderData, query, updateQuery, - } = useModelsListFilters(undefined, userId, datasetId); + } = useModelsListFilters(status, userId, datasetId); const navigate = useNavigate(); const handleClick = () => { @@ -122,14 +125,13 @@ export const ModelExplorer = ({ } /> - { - disableStatusFilter ? null : - - } + {disableStatusFilter ? null : ( + + )} {/* Mobile filters */}
diff --git a/frontend/src/components/shared/training-logs.tsx b/frontend/src/components/shared/training-logs.tsx new file mode 100644 index 000000000..e531dcf70 --- /dev/null +++ b/frontend/src/components/shared/training-logs.tsx @@ -0,0 +1,40 @@ +import { useTrainingStatus } from "@/features/models/hooks/use-training"; +import { useState } from "react"; +import { ChevronDownIcon } from "@/components/ui/icons"; +import { CodeBlock } from "@/components/ui/codeblock"; +import { MODELS_CONTENT } from "@/constants"; + +export const TrainingLogs = ({ + taskId, + expandByDefault = false, + disableExpandButton = false, +}: { + taskId: string; + expandByDefault?: boolean; + disableExpandButton?: boolean; +}) => { + const { data, isPending } = useTrainingStatus(taskId); + const [showLogs, setShowLogs] = useState(expandByDefault); + + if (isPending) { + return ( +
+ ); + } + return ( +
+ {!disableExpandButton && ( + + )} + {showLogs && } +
+ ); +}; diff --git a/frontend/src/components/shared/training-status-badge.tsx b/frontend/src/components/shared/training-status-badge.tsx new file mode 100644 index 000000000..24500bd23 --- /dev/null +++ b/frontend/src/components/shared/training-status-badge.tsx @@ -0,0 +1,19 @@ +import { TBadgeVariants } from "@/types"; +import { Badge } from "@/components/ui/badge"; + +export const TrainingStatusBadge = ({ status }: { status: string }) => { + const statusToVariant: Record = { + finished: "green", + failed: "red", + submitted: "blue", + running: "yellow", + }; + + return ( + + {status.toLocaleLowerCase() as string} + + ); +}; diff --git a/frontend/src/components/ui/form/radio-group/radio-group.tsx b/frontend/src/components/ui/form/radio-group/radio-group.tsx index 54fb936cc..8e7ad6e55 100644 --- a/frontend/src/components/ui/form/radio-group/radio-group.tsx +++ b/frontend/src/components/ui/form/radio-group/radio-group.tsx @@ -16,6 +16,7 @@ type RadioGroupProps = { onChange: (value: string) => void; withTooltip?: boolean; labelClassName?: string; + className?: string; }; export const RadioGroup = ({ @@ -25,6 +26,7 @@ export const RadioGroup = ({ onChange, withTooltip = false, labelClassName = "text-body-4", + className = "", }: RadioGroupProps) => { return ( onChange(e.target.value)} > -
+
{options.map((option) => ( {option.label} diff --git a/frontend/src/components/ui/icons/draw-icon.tsx b/frontend/src/components/ui/icons/draw-icon.tsx new file mode 100644 index 000000000..f7c3274d3 --- /dev/null +++ b/frontend/src/components/ui/icons/draw-icon.tsx @@ -0,0 +1,16 @@ +import { IconProps } from "@/types"; +import React from "react"; + +export const DrawIcon: React.FC = (props) => ( + + + +); diff --git a/frontend/src/components/ui/icons/file-upload-icon.tsx b/frontend/src/components/ui/icons/file-upload-icon.tsx new file mode 100644 index 000000000..fe56a27e6 --- /dev/null +++ b/frontend/src/components/ui/icons/file-upload-icon.tsx @@ -0,0 +1,16 @@ +import { IconProps } from "@/types"; +import React from "react"; + +export const FileUploadIcon: React.FC = (props) => ( + + + +); diff --git a/frontend/src/components/ui/icons/index.ts b/frontend/src/components/ui/icons/index.ts index c6108152b..3066a248e 100644 --- a/frontend/src/components/ui/icons/index.ts +++ b/frontend/src/components/ui/icons/index.ts @@ -62,3 +62,4 @@ export { PeopleIcon } from "./people-icon"; export { ResetIcon } from "./reset-icon"; export { DirectionIcon } from "./direction-icon"; export { CloseIcon } from "./close-icon"; +export { FileUploadIcon } from "./file-upload-icon"; diff --git a/frontend/src/config/env.ts b/frontend/src/config/env.ts index 7c2656fa2..26d67a8d7 100644 --- a/frontend/src/config/env.ts +++ b/frontend/src/config/env.ts @@ -88,4 +88,16 @@ export const ENVS = { .VITE_MAXIMUM_PREDICTION_TOLERANCE, FAIR_MODELS_BASE_PATH: import.meta.env.VITE_FAIR_MODELS_BASE_PATH, OFFSET_STEP: import.meta.env.VITE_OFFSET_STEP, + MAPSWIPE_FIREBASE_API_KEY: import.meta.env.VITE_MAPSWIPE_FIREBASE_API_KEY, + MAPSWIPE_FIREBASE_AUTH_DOMAIN: import.meta.env + .VITE_MAPSWIPE_FIREBASE_AUTH_DOMAIN, + MAPSWIPE_FIREBASE_DATABASE_URL: import.meta.env + .VITE_MAPSWIPE_FIREBASE_DATABASE_URL, + MAPSWIPE_FIREBASE_PROJECT_ID: import.meta.env + .VITE_MAPSWIPE_FIREBASE_PROJECT_ID, + MAPSWIPE_FIREBASE_STORAGE_BUCKET: import.meta.env + .VITE_MAPSWIPE_FIREBASE_STORAGE_BUCKET, + MAPSWIPE_FIREBASE_MESSAGING_SENDER_ID: import.meta.env + .VITE_MAPSWIPE_FIREBASE_MESSAGING_SENDER_ID, + MAPSWIPE_FIREBASE_APP_ID: import.meta.env.VITE_MAPSWIPE_FIREBASE_APP_ID, }; diff --git a/frontend/src/config/index.ts b/frontend/src/config/index.ts index 625ffed92..ba66ddbdd 100644 --- a/frontend/src/config/index.ts +++ b/frontend/src/config/index.ts @@ -162,11 +162,11 @@ export const MIN_TRAINING_AREA_SIZE: number = parseIntEnv( /** * The maximum file size (in bytes) allowed for training area upload. - * The default is set to 5 MB. + * The default is set to 1 MB. */ export const MAX_TRAINING_AREA_UPLOAD_FILE_SIZE: number = parseIntEnv( ENVS.MAX_TRAINING_AREA_UPLOAD_FILE_SIZE, - 5 * 1024 * 1024, + 1 * 1024 * 1024, ); /** @@ -233,8 +233,7 @@ export const MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS: number = parseIntEnv( * OSM Basemap style. */ export const MAP_STYLES: Record = { - OSM: - { + OSM: { version: 8, // "glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf", //https://fonts.openmaptiles.org/{fontstack}/{range}.pbf @@ -375,7 +374,6 @@ export const MATOMO_TRACKING_URL: string = parseStringEnv( */ export const HOT_TRACKING_HTML_TAG_NAME: string = "hot-tracking"; - export const BANNER_TIMEOUT_DURATION: number = parseIntEnv( ENVS.BANNER_TIMEOUT_DURATION, 3000, @@ -445,7 +443,7 @@ const REFRESH_BUFFER_MS: number = 1000; */ export const KPI_STATS_CACHE_TIME_MS: number = parseIntEnv(ENVS.KPI_STATS_CACHE_TIME, DEFAULT_KPI_STATS_CACHE_TIME_SECONDS) * - 1000 + + 1000 + REFRESH_BUFFER_MS; /** @@ -478,7 +476,8 @@ export const FAIR_BASE_MODELS_PATH: Record = { [BASE_MODELS.YOLOV8_V2]: `${FAIR_MODELS_BASE_PATH}/basemodels/yolo/yolov8s_v2-seg.onnx`, }; -export const OPENAERIALMAP_MOSAIC_TILES_URL = "https://apps.kontur.io/raster-tiler/oam/mosaic/{z}/{x}/{y}.png" +export const OPENAERIALMAP_MOSAIC_TILES_URL = + "https://apps.kontur.io/raster-tiler/oam/mosaic/{z}/{x}/{y}.png"; /** * The default offset step for the training labels offset controller. */ @@ -491,3 +490,17 @@ export const OFFSET_STEP: number = parseIntEnv(ENVS.OFFSET_STEP, 0.5); * Distance of the elements from the navbar in px for dropdowns and popups on the start mapping page. */ export const ELEMENT_DISTANCE_FROM_NAVBAR: number = 10; + +/** + * The configuration for Firebase. + * This is used to initialize Firebase in the application. + */ +export const firebaseConfig = { + apiKey: ENVS.MAPSWIPE_FIREBASE_API_KEY, + authDomain: ENVS.MAPSWIPE_FIREBASE_AUTH_DOMAIN, + databaseURL: ENVS.MAPSWIPE_FIREBASE_DATABASE_URL, + projectId: ENVS.MAPSWIPE_FIREBASE_PROJECT_ID, + storageBucket: ENVS.MAPSWIPE_FIREBASE_STORAGE_BUCKET, + messagingSenderId: ENVS.MAPSWIPE_FIREBASE_MESSAGING_SENDER_ID, + appId: ENVS.MAPSWIPE_FIREBASE_APP_ID, +}; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index 915b02ca4..5a8777627 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -59,6 +59,7 @@ export const APPLICATION_ROUTES = { PROFILE_SETTINGS: "/profile/settings", PROFILE_MODELS: "/profile/models", PROFILE_DATASETS: "/profile/datasets", + PROFILE_OFFLINE_PREDICTIONS: "/profile/offline-predictions", }; export const HOT_PRIVACY_POLICY_URL: string = "https://www.hotosm.org/privacy"; @@ -81,6 +82,11 @@ export const PROFILE_NAVIGATION_TABS: TProfileNavigationTabs = [ href: APPLICATION_ROUTES.PROFILE_DATASETS, active: true, }, + { + title: "Offline Predictions", + href: APPLICATION_ROUTES.PROFILE_OFFLINE_PREDICTIONS, + active: true, + }, { title: "Settings", href: APPLICATION_ROUTES.PROFILE_SETTINGS, diff --git a/frontend/src/constants/ui-contents/models-content.ts b/frontend/src/constants/ui-contents/models-content.ts index b75ce0469..24c9c3cfe 100644 --- a/frontend/src/constants/ui-contents/models-content.ts +++ b/frontend/src/constants/ui-contents/models-content.ts @@ -117,7 +117,7 @@ export const MODELS_CONTENT: TModelsContent = { mainInstruction: "Drag 'n' drop some files here, or click to select files", fleSizeInstruction: - "Supports only GeoJSON (.geojson) files. (5MB max.)", + "Supports only GeoJSON (.geojson) files. (1MB max.)", }, pageDescription: "Make sure you create at least one training area and data is accurate for each training area", diff --git a/frontend/src/constants/ui-contents/shared-content.ts b/frontend/src/constants/ui-contents/shared-content.ts index 849fb159d..d6599d254 100644 --- a/frontend/src/constants/ui-contents/shared-content.ts +++ b/frontend/src/constants/ui-contents/shared-content.ts @@ -19,6 +19,7 @@ export const SHARED_CONTENT: TSharedContent = { models: "My Models", settings: "Settings", logout: "Log Out", + offlinePredictions: "Offline Predictions", }, }, footer: { diff --git a/frontend/src/enums/common.ts b/frontend/src/enums/common.ts index 2430bd581..802fd89c2 100644 --- a/frontend/src/enums/common.ts +++ b/frontend/src/enums/common.ts @@ -81,6 +81,7 @@ export enum ModelTrainingStatus { IN_PROGRESS = "IN_PROGRESS", FINISHED = "FINISHED", FAILED = "FAILED", + RUNNING = "RUNNING", } export enum TileServiceType { diff --git a/frontend/src/enums/start-mapping.ts b/frontend/src/enums/start-mapping.ts index a4e749631..63b2d458d 100644 --- a/frontend/src/enums/start-mapping.ts +++ b/frontend/src/enums/start-mapping.ts @@ -19,8 +19,13 @@ export enum PredictedFeatureStatus { UNTOUCHED = "untouched", } - export enum FeedbackType { ACCEPT = "ACCEPT", - REJECT = "REJECT" -} \ No newline at end of file + REJECT = "REJECT", +} + +export enum MapMode { + ONLINE = "online", + // This is enabled when the user is trying to draw an AOI for offline predictions. + OFFLINE = "offline", +} diff --git a/frontend/src/features/datasets/components/dataset-area-content.tsx b/frontend/src/features/datasets/components/dataset-area-content.tsx index b9fed79a3..c57611a12 100644 --- a/frontend/src/features/datasets/components/dataset-area-content.tsx +++ b/frontend/src/features/datasets/components/dataset-area-content.tsx @@ -7,43 +7,43 @@ import { TTrainingDataset } from "@/types"; import { SHOELACE_SIZES } from "@/enums"; export const DatasetAreaContent: React.FC<{ - trainingDataset: TTrainingDataset; + trainingDataset: TTrainingDataset; }> = ({ trainingDataset }) => { - const { map, mapContainerRef } = useMapInstance(); + const { map, mapContainerRef } = useMapInstance(); - const { - data: trainingAreasData, - isPending: trainingAreaIsPending, - isError, - refetch, - } = useGetTrainingAreas(trainingDataset.id, 0); + const { + data: trainingAreasData, + isPending: trainingAreaIsPending, + isError, + refetch, + } = useGetTrainingAreas(trainingDataset.id, 0); - return ( -
- {trainingAreaIsPending && ( -
- - Loading dataset area... -
- )} - - {isError && ( -
-

Error loading dataset area.

- -
- )} + return ( +
+ {trainingAreaIsPending && ( +
+ + Loading dataset area... +
+ )} - + {isError && ( +
+

Error loading dataset area.

+
- ); + )} + + +
+ ); }; diff --git a/frontend/src/features/datasets/components/dataset-area-map.tsx b/frontend/src/features/datasets/components/dataset-area-map.tsx index 8c81f5cae..e5f0cf35d 100644 --- a/frontend/src/features/datasets/components/dataset-area-map.tsx +++ b/frontend/src/features/datasets/components/dataset-area-map.tsx @@ -76,7 +76,6 @@ export const DatasetAreaMap = ({ { //reset offset back to 0 when searching or when ID filtering is applied from the map. useEffect(() => { if ( - query[SEARCH_PARAMS.searchQuery] !== "" || - (query[SEARCH_PARAMS.id] !== "" && - (query[SEARCH_PARAMS.offset] as number) > 0) + (query[SEARCH_PARAMS.searchQuery] !== "" || + query[SEARCH_PARAMS.id] !== "") && + (query[SEARCH_PARAMS.offset] as number) > 0 ) { updateQuery({ [SEARCH_PARAMS.offset]: 0 }); } - }, [query]); + }, [ + [ + query[SEARCH_PARAMS.searchQuery], + query[SEARCH_PARAMS.offset], + query[SEARCH_PARAMS.id], + ], + ]); useEffect(() => { const newQuery = { diff --git a/frontend/src/features/mapswipe/components/mapswipe-project-creation-dialog.tsx b/frontend/src/features/mapswipe/components/mapswipe-project-creation-dialog.tsx new file mode 100644 index 000000000..e53436bdb --- /dev/null +++ b/frontend/src/features/mapswipe/components/mapswipe-project-creation-dialog.tsx @@ -0,0 +1,292 @@ +import { useState } from "react"; +import { Dialog } from "@/components/ui/dialog"; +import { Divider } from "@/components/ui/divider"; +import { Input, TextArea } from "@/components/ui/form"; +import { Button } from "@/components/ui/button"; +import { ButtonVariant, INPUT_TYPES } from "@/enums"; +import { TOfflinePrediction } from "@/types"; +import { BASE_API_URL, MATOMO_APP_DOMAIN } from "@/config"; +import { APPLICATION_ROUTES } from "@/constants"; +import { API_ENDPOINTS } from "@/services"; +import { MapswipeProjectCreationuccess } from "./mapswipe-project-success-dialog"; +import { useDialog } from "@/hooks/use-dialog"; +import { useFirebase } from "@/hooks/use-firebase"; +import { showErrorToast } from "@/utils"; + +const DESCRIPTION_MAX_LENGTH = 500; +const DESCRIPTION_MIN_LENGTH = 10; +const PROJECT_TOPIC_MAX_LENGTH = 50; +const PROJECT_TOPIC_MIN_LENGTH = 5; +const PROJECT_REGION_MAX_LENGTH = 50; +const PROJECT_REGION_MIN_LENGTH = 5; + +export const CreateMapswipeProjectDialog = ({ + isOpened, + closeDialog, + predictionResult, +}: { + isOpened: boolean; + closeDialog: () => void; + predictionResult: TOfflinePrediction; +}) => { + const { + isOpened: isSuccessDialogOpened, + closeDialog: closeSuccessDialog, + openDialog: openSuccessDialog, + } = useDialog(); + + const { pushToDatabase } = useFirebase(); + const [loading, setLoading] = useState(false); + + const DEFAULT_FORMDATA = { + topic: "", + region: "", + details: "", + organisation: "HOT", + visibility: "Public", + tutorial: "tutorial_-MQsj5VWpNcJxCTVTOyH", + infoUrl: + MATOMO_APP_DOMAIN + + APPLICATION_ROUTES.MODELS + + "/" + + predictionResult.config.model_id, + verification: "3", + groupSize: 25, + inputGeometryUrl: + BASE_API_URL + + API_ENDPOINTS.DOWNLOAD_PREDICTION_LABELS_FILE(predictionResult.id), + tileServiceUrl: predictionResult.config.source, + }; + const [form, setForm] = useState(DEFAULT_FORMDATA); + + const updateField = (key: keyof typeof form, value: string) => { + setForm((prev) => ({ ...prev, [key]: value })); + }; + + const [descriptionIsValid, setDescriptionIsValid] = useState({ + valid: + form.details.length >= DESCRIPTION_MIN_LENGTH && + form.details.length <= DESCRIPTION_MAX_LENGTH, + message: "", + }); + + const [topicIsValid, setTopicIsValid] = useState({ + valid: + form.topic.length >= PROJECT_TOPIC_MIN_LENGTH && + form.topic.length <= PROJECT_TOPIC_MAX_LENGTH, + message: "", + }); + + const [regionIsValid, setRegionIsValid] = useState({ + valid: + form.region.length >= PROJECT_REGION_MIN_LENGTH && + form.region.length <= PROJECT_REGION_MAX_LENGTH, + message: "", + }); + + const handleCloseDialog = () => { + closeDialog(); + setLoading(false); + setForm(DEFAULT_FORMDATA); + setDescriptionIsValid({ + valid: + form.details.length >= DESCRIPTION_MIN_LENGTH && + form.details.length <= DESCRIPTION_MAX_LENGTH, + message: "", + }); + setTopicIsValid({ + valid: + form.topic.length >= PROJECT_TOPIC_MIN_LENGTH && + form.topic.length <= PROJECT_TOPIC_MAX_LENGTH, + message: "", + }); + setRegionIsValid({ + valid: + form.region.length >= PROJECT_REGION_MIN_LENGTH && + form.region.length <= PROJECT_REGION_MAX_LENGTH, + message: "", + }); + }; + + const handleProjectCreation = async () => { + try { + setLoading(true); + const newProjectDraftsRef = await pushToDatabase(); + if (newProjectDraftsRef.key) { + handleCloseDialog(); + openSuccessDialog(); + // send actual project data to the database here + // patch this prediction result with the mapswipe project id + } else { + showErrorToast("Failed to create MapSwipe project."); + } + } catch (error) { + setLoading(false); + showErrorToast(error); + } + }; + + return ( + <> + + + + +
+
+ updateField("topic", e.target.value)} + showBorder + maxLength={PROJECT_TOPIC_MAX_LENGTH} + labelWithTooltip + toolTipContent="Starts with the project type title by MapSwipe convention (Conflate ..., Compare ..., etc)" + placeholder="Conflate fAIr buildings" + minLength={PROJECT_TOPIC_MIN_LENGTH} + validationStateUpdateCallback={setTopicIsValid} + isValid={form.topic.length > 0 && topicIsValid.valid} + /> + updateField("region", e.target.value)} + showBorder + maxLength={PROJECT_REGION_MAX_LENGTH} + labelWithTooltip + toolTipContent="The region where the project is located, e.g., Banepa, Nepal" + placeholder="Banepa, Nepal" + minLength={PROJECT_REGION_MIN_LENGTH} + validationStateUpdateCallback={setRegionIsValid} + isValid={form.region.length > 0 && regionIsValid.valid} + /> +
+ +