diff --git a/frontend/src/app/providers/models-provider.tsx b/frontend/src/app/providers/models-provider.tsx
index a6c66ab5..30a8a94c 100644
--- a/frontend/src/app/providers/models-provider.tsx
+++ b/frontend/src/app/providers/models-provider.tsx
@@ -5,16 +5,16 @@ import {
TOAST_NOTIFICATIONS,
} from "@/constants";
import { BASE_MODELS, TrainingDatasetOption, TrainingType } from "@/enums";
-import { HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY } from "@/config";
+import { HOT_FAIR_MODEL_CREATION_LOCAL_STORAGE_KEY } from "@/config";
import { LngLatBoundsLike } from "maplibre-gl";
import { useCreateTrainingDataset } from "@/features/model-creation/hooks/use-training-datasets";
-import { useGetTrainingDataset } from "@/features/models/hooks/use-dataset";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { useModelDetails } from "@/features/models/hooks/use-models";
import { UseMutationResult } from "@tanstack/react-query";
-import { useSessionStorage } from "@/hooks/use-storage";
+import { useLocalStorage } from "@/hooks/use-storage";
import {
+ TModelDetails,
TTrainingAreaFeature,
TTrainingDataset,
TTrainingDetails,
@@ -26,6 +26,7 @@ import {
} from "@/utils";
import React, {
createContext,
+ useCallback,
useContext,
useEffect,
useMemo,
@@ -41,6 +42,8 @@ import {
useCreateModelTrainingRequest,
useUpdateModel,
} from "@/features/model-creation/hooks/use-models";
+import axios from "axios";
+import { useAuth } from "./auth-provider";
/**
* The names here are the same with the `initialFormState` object keys.
@@ -229,6 +232,10 @@ const ModelsContext = createContext<{
handleModelCreationAndUpdate: () => void;
handleTrainingDatasetCreation: () => void;
validateEditMode: boolean;
+ isError: boolean;
+ isPending: boolean;
+ data: TModelDetails;
+ isModelOwner: boolean;
}>({
formData: initialFormState,
setFormData: () => {},
@@ -255,6 +262,10 @@ const ModelsContext = createContext<{
trainingDatasetCreationInProgress: false,
handleTrainingDatasetCreation: () => {},
validateEditMode: false,
+ isPending: false,
+ isError: false,
+ data: {} as TModelDetails,
+ isModelOwner: false,
});
export const ModelsProvider: React.FC<{
@@ -262,16 +273,13 @@ export const ModelsProvider: React.FC<{
}> = ({ children }) => {
const navigate = useNavigate();
const { pathname } = useLocation();
- const { modelId } = useParams();
- const { getSessionValue, setSessionValue, removeSessionValue } =
- useSessionStorage();
-
- const storedFormData = getSessionValue(
- HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY,
- );
+ const { modelId, id } = useParams();
+ const { setValue, removeValue, getValue } = useLocalStorage();
+ const storedFormData = getValue(HOT_FAIR_MODEL_CREATION_LOCAL_STORAGE_KEY);
const [formData, setFormData] = useState(
storedFormData ? JSON.parse(storedFormData) : initialFormState,
);
+ const { user, isAuthenticated } = useAuth();
const handleChange = (
field: string,
@@ -285,8 +293,8 @@ export const ModelsProvider: React.FC<{
) => {
setFormData((prev) => {
const updatedData = { ...prev, [field]: value };
- setSessionValue(
- HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY,
+ setValue(
+ HOT_FAIR_MODEL_CREATION_LOCAL_STORAGE_KEY,
JSON.stringify(updatedData),
);
return updatedData;
@@ -300,31 +308,43 @@ export const ModelsProvider: React.FC<{
const isEditMode = Boolean(modelId && !pathname.includes("new"));
- const { data, isPending, isError } = useModelDetails(
- modelId as string,
- isEditMode,
+ const { data, isPending, isError, error } = useModelDetails(
+ id ?? (modelId as string),
+ !!id || !!modelId,
+ 10000,
);
- const {
- data: trainingDataset,
- isPending: trainingDatasetIsPending,
- isError: trainingDatasetIsError,
- } = useGetTrainingDataset(
- Number(data?.dataset),
- Boolean(isEditMode && data?.dataset),
- );
+ const isModelOwner = isAuthenticated && data?.user?.osm_id === user?.osm_id;
// Will be used in the route validator component to delay the redirection for a while until the data are retrieved
const validateEditMode =
formData.selectedTrainingDatasetId !== "" && formData.tmsURL !== "";
- // Fetch and prefill model details
useEffect(() => {
- if (!isEditMode || isPending || !data) return;
-
- if (isError) {
- navigate(APPLICATION_ROUTES.NOTFOUND);
+ if (isError && error) {
+ const currentPath = pathname;
+ if (axios.isAxiosError(error)) {
+ navigate(APPLICATION_ROUTES.NOTFOUND, {
+ state: {
+ from: currentPath,
+ error: error.response?.data?.detail,
+ },
+ });
+ } else {
+ const err = error as Error;
+ navigate(APPLICATION_ROUTES.NOTFOUND, {
+ state: {
+ from: currentPath,
+ error: err.message,
+ },
+ });
+ }
}
+ }, [isError, error, navigate]);
+
+ // Fetch and prefill model details and training dataset
+ useEffect(() => {
+ if (!isEditMode || isPending || !data || isError) return;
handleChange(MODEL_CREATION_FORM_NAME.BASE_MODELS, data.base_model);
handleChange(
@@ -334,50 +354,36 @@ export const ModelsProvider: React.FC<{
handleChange(MODEL_CREATION_FORM_NAME.MODEL_NAME, data.name ?? "");
handleChange(
MODEL_CREATION_FORM_NAME.SELECTED_TRAINING_DATASET_ID,
- data.dataset,
+ data.dataset.id,
);
- }, [isEditMode, isError, isPending, data]);
-
- // Fetch and prefill training dataset
- useEffect(() => {
- if (!isEditMode || trainingDatasetIsPending || trainingDatasetIsError)
- return;
handleChange(
MODEL_CREATION_FORM_NAME.DATASET_NAME,
- trainingDataset.name ?? "",
+ data.dataset.name ?? "",
);
handleChange(
MODEL_CREATION_FORM_NAME.TMS_URL,
- trainingDataset.source_imagery ?? "",
+ data.dataset.source_imagery ?? "",
);
- }, [
- isEditMode,
- trainingDatasetIsPending,
- trainingDataset,
- trainingDatasetIsError,
- ]);
+ }, [isEditMode, isError, isPending, data]);
+
+ const resetState = () => {
+ removeValue(HOT_FAIR_MODEL_CREATION_LOCAL_STORAGE_KEY);
+ setFormData(initialFormState);
+ };
useEffect(() => {
// Cleanup the timeout on component unmount
return () => {
- removeSessionValue(HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY);
if (timeOutRef.current) {
clearTimeout(timeOutRef.current);
}
};
}, []);
- const resetState = () => {
- removeSessionValue(HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY);
- setFormData(initialFormState);
- };
-
const createNewTrainingRequestMutation = useCreateModelTrainingRequest({
mutationConfig: {
onSuccess: () => {
showSuccessToast(TOAST_NOTIFICATIONS.trainingRequestSubmittedSuccess);
- // Reset the state after 2 second on the model success page.
- resetState();
},
onError: (error) => {
showErrorToast(error);
@@ -405,22 +411,29 @@ export const ModelsProvider: React.FC<{
},
});
- const handleModelCreationOrUpdateSuccess = (modelId: string) => {
- showSuccessToast(
- isEditMode
- ? TOAST_NOTIFICATIONS.modelUpdateSuccess
- : TOAST_NOTIFICATIONS.modelCreationSuccess,
- );
- navigate(`${getFullPath(MODELS_ROUTES.CONFIRMATION)}?id=${modelId}`);
- // Submit the model for training request
+ const submitTrainingRequest = useCallback(() => {
createNewTrainingRequestMutation.mutate({
- model: modelId,
+ model: modelId as string,
input_boundary_width: formData.boundaryWidth,
input_contact_spacing: formData.contactSpacing,
epochs: formData.epoch,
batch_size: formData.batchSize,
zoom_level: formData.zoomLevels,
});
+ }, [formData, modelId]);
+
+ const handleModelCreationOrUpdateSuccess = (id?: string) => {
+ if (isModelOwner) {
+ showSuccessToast(
+ isEditMode
+ ? TOAST_NOTIFICATIONS.modelUpdateSuccess
+ : TOAST_NOTIFICATIONS.modelCreationSuccess,
+ );
+ }
+
+ navigate(`${getFullPath(MODELS_ROUTES.CONFIRMATION)}?id=${id ?? modelId}`);
+ // Submit the model for training request
+ submitTrainingRequest();
};
const modelCreateMutation = useCreateModel({
@@ -446,19 +459,23 @@ export const ModelsProvider: React.FC<{
modelId: modelId as string,
});
- // Confirm that all the training areas labels has been retrieved
- const hasLabeledTrainingAreas =
- formData.trainingAreas.length > 0 &&
- formData.trainingAreas.filter(
- (aoi: TTrainingAreaFeature) => aoi.properties.label_fetched === null,
- ).length === 0;
+ // Confirm that all the training areas labels have been fetched.
+ const hasLabeledTrainingAreas = useMemo(
+ () =>
+ formData.trainingAreas.every(
+ (aoi: TTrainingAreaFeature) => aoi.properties.label_fetched !== null,
+ ),
+ [formData],
+ );
- // Confirm that all of the training areas has a geometry
- const hasAOIsWithGeometry =
- formData.trainingAreas.length > 0 &&
- formData.trainingAreas.filter(
- (aoi: TTrainingAreaFeature) => aoi.geometry === null,
- ).length === 0;
+ // Confirm that all of the training areas have a geometry.
+ const hasAOIsWithGeometry = useMemo(
+ () =>
+ formData.trainingAreas.every(
+ (aoi: TTrainingAreaFeature) => aoi.geometry !== null,
+ ),
+ [formData],
+ );
const handleTrainingDatasetCreation = () => {
createNewTrainingDatasetMutation.mutate({
@@ -470,7 +487,9 @@ export const ModelsProvider: React.FC<{
createNewTrainingDatasetMutation.isPending;
const handleModelCreationAndUpdate = () => {
- if (isEditMode) {
+ // The user is trying to edit their model.
+ // In this case, send a PATCH request and submit a training request.
+ if (isEditMode && isModelOwner) {
modelUpdateMutation.mutate({
dataset: formData.selectedTrainingDatasetId,
name: formData.modelName,
@@ -478,6 +497,10 @@ export const ModelsProvider: React.FC<{
base_model: formData.baseModel as BASE_MODELS,
modelId: modelId as string,
});
+ // The user is trying to edit another users model training area and settings.
+ // In this case, directly submit a training request.
+ } else if (isEditMode && !isModelOwner) {
+ handleModelCreationOrUpdateSuccess();
} else {
modelCreateMutation.mutate({
dataset: formData.selectedTrainingDatasetId,
@@ -496,7 +519,6 @@ export const ModelsProvider: React.FC<{
hasLabeledTrainingAreas,
hasAOIsWithGeometry,
formData,
- resetState,
createNewTrainingRequestMutation,
isEditMode,
modelId,
@@ -505,6 +527,11 @@ export const ModelsProvider: React.FC<{
handleTrainingDatasetCreation,
trainingDatasetCreationInProgress,
validateEditMode,
+ resetState,
+ data,
+ isPending,
+ isError,
+ isModelOwner,
}),
[
setFormData,
@@ -513,15 +540,19 @@ export const ModelsProvider: React.FC<{
createNewTrainingDatasetMutation,
hasLabeledTrainingAreas,
hasAOIsWithGeometry,
- resetState,
createNewTrainingRequestMutation,
isEditMode,
modelId,
+ resetState,
getFullPath,
handleModelCreationAndUpdate,
handleTrainingDatasetCreation,
trainingDatasetCreationInProgress,
validateEditMode,
+ data,
+ isPending,
+ isError,
+ isModelOwner,
],
);
diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx
index 69d8e030..cec0af41 100644
--- a/frontend/src/app/router.tsx
+++ b/frontend/src/app/router.tsx
@@ -7,6 +7,7 @@ import {
RouterProvider,
createBrowserRouter,
} from "react-router-dom";
+import { ModelsProvider } from "@/app/providers/models-provider";
const router = createBrowserRouter([
{
@@ -71,7 +72,12 @@ const router = createBrowserRouter([
"@/app/routes/models/model-details-card"
);
return {
- Component: () => ,
+ Component: () => (
+
+ {" "}
+
+
+ ),
};
},
},
@@ -333,11 +339,10 @@ const router = createBrowserRouter([
return { Component: AuthenticationCallbackPage };
},
},
-
/**
* Auth route ends.
*/
-
+
/**
* 404 route
*/
diff --git a/frontend/src/app/routes/models/confirmation.tsx b/frontend/src/app/routes/models/confirmation.tsx
index bf7ec6d6..85632dce 100644
--- a/frontend/src/app/routes/models/confirmation.tsx
+++ b/frontend/src/app/routes/models/confirmation.tsx
@@ -5,13 +5,22 @@ import { Image } from "@/components/ui/image";
import { Link } from "@/components/ui/link";
import { ModelFormConfirmation } from "@/assets/images";
import { useModelsContext } from "@/app/providers/models-provider";
-import { useSearchParams } from "react-router-dom";
+import { useNavigate, useSearchParams } from "react-router-dom";
+import { useEffect } from "react";
export const ModelConfirmationPage = () => {
const [searchParams] = useSearchParams();
-
+ const navigate = useNavigate();
const modelId = searchParams.get("id");
- const { isEditMode } = useModelsContext();
+ const { isEditMode, resetState } = useModelsContext();
+
+ // Reset the state on this page.
+ useEffect(() => {
+ if (!modelId) {
+ navigate(APPLICATION_ROUTES.CREATE_NEW_MODEL);
+ }
+ resetState();
+ }, []);
return (
{
Model {modelId} is {isEditMode ? "Updated" : "Created"}!
- {MODELS_CONTENT.modelCreation.confirmation.description}
+ {isEditMode
+ ? MODELS_CONTENT.modelCreation.confirmation.updateDescription
+ : MODELS_CONTENT.modelCreation.confirmation.description}
{
- const { id } = useParams<{ id: string }>();
-
const {
isOpened: isModelFilesDialogOpened,
closeDialog: closeModelFilesDialog,
openDialog: openModelFilesDialog,
} = useDialog();
- const navigate = useNavigate();
+ const { data, isPending, isError } = useModelsContext();
- const { data, isPending, isError, error } = useModelDetails(
- id as string,
- !!id,
- 10000,
- );
const { isAuthenticated } = useAuth();
- useEffect(() => {
- if (isError && error) {
- const currentPath = window.location.pathname;
- if (axios.isAxiosError(error)) {
- navigate(APPLICATION_ROUTES.NOTFOUND, {
- state: {
- from: currentPath,
- error: error.response?.data?.detail,
- },
- });
- } else {
- const err = error as Error;
- navigate(APPLICATION_ROUTES.NOTFOUND, {
- state: {
- from: currentPath,
- error: err.message,
- },
- });
- }
- }
- }, [isError, error, navigate]);
-
const {
isOpened: isModelEnhancementDialogOpened,
closeDialog: closeModelEnhancementDialog,
openDialog: openModelEnhancementDialog,
} = useDialog();
- const {
- isPending: isTrainingDatasetPending,
- data: trainingDataset,
- isError: isTrainingDatasetError,
- } = useGetTrainingDataset(data?.dataset, !!data);
-
const { isOpened, closeDialog, openDialog } = useDialog();
- if (
- isPending ||
- isError ||
- isTrainingDatasetPending ||
- isTrainingDatasetError
- ) {
+ if (isPending || isError) {
return ;
}
@@ -91,20 +47,20 @@ export const ModelDetailsPage = () => {
@@ -112,9 +68,7 @@ export const ModelDetailsPage = () => {
data={data as TModelDetails}
openModelFilesDialog={openModelFilesDialog}
openTrainingAreaDrawer={openDialog}
- isError={isTrainingDatasetError}
- isPending={isTrainingDatasetPending}
- trainingDataset={trainingDataset as TTrainingDataset}
+ trainingDataset={data?.dataset}
/>
{
) : (
)}
@@ -168,9 +122,9 @@ export const ModelDetailsPage = () => {
modelId={data?.id as string}
trainingId={data?.published_training as number}
modelOwner={data?.user.username as string}
- datasetId={data?.dataset as number}
+ datasetId={data?.dataset.id as number}
baseModel={data?.base_model as string}
- tmsUrl={trainingDataset.source_imagery}
+ tmsUrl={data?.dataset?.source_imagery}
/>
diff --git a/frontend/src/app/routes/start-mapping.tsx b/frontend/src/app/routes/start-mapping.tsx
index 425a7f96..c47f1b6f 100644
--- a/frontend/src/app/routes/start-mapping.tsx
+++ b/frontend/src/app/routes/start-mapping.tsx
@@ -8,20 +8,14 @@ import { FitToBounds, LayerControl, ZoomLevel } from "@/components/map";
import { Head } from "@/components/seo";
import { LngLatBoundsLike } from "maplibre-gl";
import { ModelDetailsPopUp } from "@/features/start-mapping/components";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import { useDropdownMenu } from "@/hooks/use-dropdown-menu";
import { useGetTMSTileJSON } from "@/features/model-creation/hooks/use-tms-tilejson";
-import { useGetTrainingDataset } from "@/features/models/hooks/use-dataset";
import { useMapInstance } from "@/hooks/use-map-instance";
import { useModelDetails } from "@/features/models/hooks/use-models";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { UserProfile } from "@/components/layout";
-import {
- Feature,
- TileJSON,
- TModelPredictions,
- TTrainingDataset,
-} from "@/types";
+import { Feature, TileJSON, TModelPredictions } from "@/types";
import {
BrandLogoWithDropDown,
Legend,
@@ -43,7 +37,9 @@ import {
ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
ALL_MODEL_PREDICTIONS_FILL_LAYER_ID,
ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
+ HOT_FAIR_MODEL_PREDICTIONS_LOCAL_STORAGE_KEY,
} from "@/config";
+import { useLocalStorage } from "@/hooks/use-storage";
export type TDownloadOptions = {
name: string;
@@ -75,24 +71,23 @@ export const StartMappingPage = () => {
const {
isError,
- isPending,
+ isPending: modelInfoRequestIspending,
data: modelInfo,
error,
} = useModelDetails(modelId as string, !!modelId);
- const {
- data: trainingDataset,
- isPending: trainingDatasetIsPending,
- isError: trainingDatasetIsError,
- } = useGetTrainingDataset(modelInfo?.dataset as number, !isPending);
-
- const tileJSONURL = extractTileJSONURL(trainingDataset?.source_imagery ?? "");
+ const tileJSONURL = useMemo(
+ () =>
+ modelInfo?.dataset?.source_imagery
+ ? extractTileJSONURL(modelInfo?.dataset?.source_imagery)
+ : undefined,
+ [modelInfo?.dataset?.source_imagery],
+ );
- const {
- data: oamTileJSON,
- isError: oamTileJSONIsError,
- error: oamTileJSONError,
- } = useGetTMSTileJSON(tileJSONURL);
+ const { data: oamTileJSON, isError: oamTileJSONIsError } = useGetTMSTileJSON(
+ tileJSONURL as string,
+ !!tileJSONURL,
+ );
useEffect(() => {
if (isError) {
@@ -117,18 +112,38 @@ export const StartMappingPage = () => {
[SEARCH_PARAMS.area]: searchParams.get(SEARCH_PARAMS.area) || 4,
};
});
+ const { setValue, getValue } = useLocalStorage();
- // Todo - move to local storage.
- const [modelPredictions, setModelPredictions] = useState
({
- all: [],
+ const emptyPredictionState = {
accepted: [],
rejected: [],
- });
+ all: [],
+ };
+ const [modelPredictions, setModelPredictions] = useState(
+ () => {
+ const savedPredictions = getValue(
+ HOT_FAIR_MODEL_PREDICTIONS_LOCAL_STORAGE_KEY(modelId as string),
+ );
+ return savedPredictions
+ ? JSON.parse(savedPredictions)
+ : emptyPredictionState;
+ },
+ );
+
+ useEffect(() => {
+ setValue(
+ HOT_FAIR_MODEL_PREDICTIONS_LOCAL_STORAGE_KEY(modelId as string),
+ JSON.stringify(modelPredictions),
+ );
+ }, [modelPredictions, modelId]);
- const modelPredictionsExist =
- modelPredictions.accepted.length > 0 ||
- modelPredictions.rejected.length > 0 ||
- modelPredictions.all.length > 0;
+ const modelPredictionsExist = useMemo(() => {
+ return (
+ modelPredictions.accepted.length > 0 ||
+ modelPredictions.rejected.length > 0 ||
+ modelPredictions.all.length > 0
+ );
+ }, [modelPredictions]);
const updateQuery = useCallback(
(newParams: TQueryParams) => {
@@ -154,47 +169,50 @@ export const StartMappingPage = () => {
const popupAnchorId = "model-details";
- const mapLayers = [
- ...(modelPredictions.accepted.length > 0
- ? [
- {
- value:
- START_MAPPING_PAGE_CONTENT.map.controls.legendControl
- .acceptedPredictions,
- subLayers: [
- ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID,
- ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
- ],
- },
- ]
- : []),
- ...(modelPredictions.rejected.length > 0
- ? [
- {
- value:
- START_MAPPING_PAGE_CONTENT.map.controls.legendControl
- .rejectedPredictions,
- subLayers: [
- REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID,
- REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
- ],
- },
- ]
- : []),
- ...(modelPredictions.all.length > 0
- ? [
- {
- value:
- START_MAPPING_PAGE_CONTENT.map.controls.legendControl
- .predictionResults,
- subLayers: [
- ALL_MODEL_PREDICTIONS_FILL_LAYER_ID,
- ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
- ],
- },
- ]
- : []),
- ];
+ const mapLayers = useMemo(
+ () => [
+ ...(modelPredictions.accepted.length > 0
+ ? [
+ {
+ value:
+ START_MAPPING_PAGE_CONTENT.map.controls.legendControl
+ .acceptedPredictions,
+ subLayers: [
+ ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID,
+ ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
+ ],
+ },
+ ]
+ : []),
+ ...(modelPredictions.rejected.length > 0
+ ? [
+ {
+ value:
+ START_MAPPING_PAGE_CONTENT.map.controls.legendControl
+ .rejectedPredictions,
+ subLayers: [
+ REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID,
+ REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
+ ],
+ },
+ ]
+ : []),
+ ...(modelPredictions.all.length > 0
+ ? [
+ {
+ value:
+ START_MAPPING_PAGE_CONTENT.map.controls.legendControl
+ .predictionResults,
+ subLayers: [
+ ALL_MODEL_PREDICTIONS_FILL_LAYER_ID,
+ ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
+ ],
+ },
+ ]
+ : []),
+ ],
+ [modelPredictions],
+ );
const handleAllFeaturesDownload = useCallback(async () => {
geoJSONDowloader(
@@ -206,7 +224,7 @@ export const StartMappingPage = () => {
...modelPredictions.all,
],
},
- `all_predictions_${modelInfo.dataset}`,
+ `all_predictions_${modelInfo.dataset.id}`,
);
showSuccessToast(TOAST_NOTIFICATIONS.startMapping.fileDownloadSuccess);
}, [modelPredictions, modelInfo]);
@@ -214,23 +232,22 @@ export const StartMappingPage = () => {
const handleAcceptedFeaturesDownload = useCallback(async () => {
geoJSONDowloader(
{ type: "FeatureCollection", features: modelPredictions.accepted },
- `accepted_predictions_${modelInfo.dataset}`,
+ `accepted_predictions_${modelInfo.dataset.id}`,
);
showSuccessToast(TOAST_NOTIFICATIONS.startMapping.fileDownloadSuccess);
}, [modelPredictions, modelInfo]);
const handleFeaturesDownloadToJOSM = useCallback(
(features: Feature[]) => {
- if (!map || !trainingDataset?.name || !trainingDataset?.source_imagery)
- return;
+ if (!map || !modelInfo?.dataset) return;
openInJOSM(
- trainingDataset.name,
- trainingDataset.source_imagery,
+ modelInfo.dataset.name,
+ modelInfo.dataset.source_imagery,
features,
true,
);
},
- [map, oamTileJSON, trainingDataset],
+ [map, modelInfo],
);
const handleAllFeaturesDownloadToJOSM = useCallback(() => {
@@ -288,18 +305,14 @@ export const StartMappingPage = () => {
}, [setShowModelDetailsPopup]);
const clearPredictions = useCallback(() => {
- setModelPredictions({
- accepted: [],
- rejected: [],
- all: [],
- });
+ setModelPredictions(emptyPredictionState);
}, [setModelPredictions]);
return (
<>
- {/* Mobile dialog */}
+ {/* Mobile dialog */}
{
updateQuery={updateQuery}
modelDetailsPopupIsActive={showModelDetailsPopup}
clearPredictions={clearPredictions}
- trainingDataset={trainingDataset as TTrainingDataset}
currentZoom={currentZoom}
modelInfo={modelInfo}
/>
{/* Model Details Popup */}
- {modelInfo && (
- setShowModelDetailsPopup(false)}
- anchor={popupAnchorId}
- model={modelInfo}
- trainingDataset={trainingDataset}
- trainingDatasetIsPending={trainingDatasetIsPending}
- trainingDatasetIsError={trainingDatasetIsError}
- />
- )}
+ setShowModelDetailsPopup(false)}
+ anchor={popupAnchorId}
+ modelInfo={modelInfo}
+ modelInfoRequestIsPending={modelInfoRequestIspending}
+ modelInfoRequestIsError={isError}
+ />
{/* Web Header */}
{
handleModelDetailsPopup={handleModelDetailsPopup}
downloadOptions={downloadOptions}
clearPredictions={clearPredictions}
- trainingDataset={trainingDataset as TTrainingDataset}
currentZoom={currentZoom}
/>
@@ -381,12 +389,11 @@ export const StartMappingPage = () => {
{/* Map Component */}
{
layers={mapLayers}
tmsBounds={oamTileJSON?.bounds as LngLatBoundsLike}
trainingId={modelInfo?.published_training}
+ modelInfoRequestIsPending={modelInfoRequestIspending}
/>
diff --git a/frontend/src/components/map/layers/open-aerial-map.tsx b/frontend/src/components/map/layers/open-aerial-map.tsx
index 95f8b9e9..37219210 100644
--- a/frontend/src/components/map/layers/open-aerial-map.tsx
+++ b/frontend/src/components/map/layers/open-aerial-map.tsx
@@ -1,6 +1,6 @@
import { Map } from "maplibre-gl";
import { TMS_LAYER_ID, TMS_SOURCE_ID } from "@/config";
-import { useMapLayers } from "@/hooks/use-map-layer";
+import { useEffect } from "react";
export const OpenAerialMap = ({
tileJSONURL,
@@ -9,26 +9,24 @@ export const OpenAerialMap = ({
tileJSONURL?: string;
map: Map | null;
}) => {
- useMapLayers(
- [
- {
+ useEffect(() => {
+ if (!map) return;
+ if (!map.getSource(TMS_SOURCE_ID)) {
+ map.addSource(TMS_SOURCE_ID, {
+ type: "raster",
+ url: tileJSONURL,
+ tileSize: 256,
+ });
+ }
+ if (!map.getLayer(TMS_LAYER_ID)) {
+ map.addLayer({
id: TMS_LAYER_ID,
type: "raster",
source: TMS_SOURCE_ID,
layout: { visibility: "visible" },
- },
- ],
- [
- {
- id: TMS_SOURCE_ID,
- spec: {
- type: "raster",
- url: tileJSONURL,
- tileSize: 256,
- },
- },
- ],
- map,
- );
+ });
+ }
+ }, [map, tileJSONURL]);
+
return null;
};
diff --git a/frontend/src/config/index.ts b/frontend/src/config/index.ts
index 81c9cde3..e09058b8 100644
--- a/frontend/src/config/index.ts
+++ b/frontend/src/config/index.ts
@@ -114,9 +114,9 @@ export const HOT_FAIR_LOGIN_SUCCESSFUL_SESSION_KEY: string =
/**
* The key used to store the model form data in session storage to preserve the state incase the user
* visits ID Editor or JOSM to map a training area.
- * Session storage is used to allow users to be able to open fAIr on a new tab and start on a clean slate.
+ * Local storage is used to withstand reloads and network issues.
*/
-export const HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY: string =
+export const HOT_FAIR_MODEL_CREATION_LOCAL_STORAGE_KEY: string =
"__hot_fair_model_creation_formdata";
/**
@@ -126,10 +126,11 @@ export const HOT_FAIR_BANNER_LOCAL_STORAGE_KEY: string =
"__hot_fair_banner_closed";
/**
- * The key used to store the model predictions in the session storage for the application.
+ * The key used to store the predictions for the specific model in the users local storage.
*/
-export const HOT_FAIR_MODEL_PREDICTIONS_SESSION_STORAGE_KEY: string =
- "__hot_fair_model_predictions";
+export const HOT_FAIR_MODEL_PREDICTIONS_LOCAL_STORAGE_KEY = (
+ modelId: string,
+): string => `__hot_fair_model_predictions_for_model_${modelId}`;
// ==============================================================================================================================
// Training Area Configurations
@@ -190,8 +191,10 @@ export const MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE: number =
/**
* The maximum zoom level for the map.
+ * Model predictions require a max zoom of 22.
+ * 21 is used here because 1 is already added to the 'currentZoom' in the useMapInstance() hook.
*/
-export const MAX_ZOOM_LEVEL: number = parseIntEnv(ENVS.MAX_ZOOM_LEVEL, 22);
+export const MAX_ZOOM_LEVEL: number = parseIntEnv(ENVS.MAX_ZOOM_LEVEL, 21);
/**
* The minimum zoom level for the map before the prediction components can be activated.
diff --git a/frontend/src/constants/ui-contents/models-content.ts b/frontend/src/constants/ui-contents/models-content.ts
index 1e852fe2..aba34e98 100644
--- a/frontend/src/constants/ui-contents/models-content.ts
+++ b/frontend/src/constants/ui-contents/models-content.ts
@@ -155,6 +155,8 @@ export const MODELS_CONTENT: TModelsContent = {
},
description:
"Your created model was successful, and it is now undergoing a training.",
+ updateDescription:
+ "Model update was successful, and it is now undergoing a training.",
},
trainingSettings: {
form: {
diff --git a/frontend/src/features/model-creation/api/factory.ts b/frontend/src/features/model-creation/api/factory.ts
index 690cbe99..d83627a3 100644
--- a/frontend/src/features/model-creation/api/factory.ts
+++ b/frontend/src/features/model-creation/api/factory.ts
@@ -9,6 +9,7 @@ import {
getTrainingDatasetLabels,
getTrainingDatasets,
} from "@/features/model-creation/api/get-trainings";
+import { QUERY_KEYS } from "@/services";
export const getTrainingDatasetsQueryOptions = (searchQuery: string) => {
return queryOptions({
@@ -22,7 +23,7 @@ export const getTrainingAreasQueryOptions = (
offset: number,
) => {
return queryOptions({
- queryKey: ["training-areas", datasetId, offset],
+ queryKey: [QUERY_KEYS.TRAINING_AREAS(datasetId, offset)],
queryFn: () => getTrainingAreas(datasetId, offset),
placeholderData: keepPreviousData,
});
diff --git a/frontend/src/features/model-creation/components/progress-bar.tsx b/frontend/src/features/model-creation/components/progress-bar.tsx
index a5300297..6f9c0683 100644
--- a/frontend/src/features/model-creation/components/progress-bar.tsx
+++ b/frontend/src/features/model-creation/components/progress-bar.tsx
@@ -3,6 +3,7 @@ import { cn } from "@/utils";
import { useEffect, useRef } from "react";
import { useModelsContext } from "@/app/providers/models-provider";
import { useNavigate } from "react-router-dom";
+import { MODELS_ROUTES } from "@/constants";
type ProgressBarProps = {
currentPath: string;
@@ -16,7 +17,7 @@ const ProgressBar: React.FC = ({
pages,
}) => {
const navigate = useNavigate();
- const { getFullPath } = useModelsContext();
+ const { getFullPath, isModelOwner } = useModelsContext();
const activeStepRef = useRef(null);
const containerRef = useRef(null);
@@ -43,13 +44,23 @@ const ProgressBar: React.FC = ({
>
{pages.map((step, index) => {
const activeStep = currentPath.includes(step.path);
+ // Disable the confirmation page button from been clickable.
const isLastPage = index === pages.length - 1;
+ // Disable other buttons when the user is on the confirmation page.
+ const isConfirmationPage = currentPath.includes(
+ MODELS_ROUTES.CONFIRMATION,
+ );
+ // Disable the model details and training dataset if the user is not the owner of the model
+ const disableButton =
+ isLastPage ||
+ isConfirmationPage ||
+ (!isModelOwner && [0, 1].includes(index));
return (
!isLastPage && navigate(getFullPath(step.path))}
>
{step.id < currentPageIndex + 1 ? (
diff --git a/frontend/src/features/model-creation/components/progress-buttons.tsx b/frontend/src/features/model-creation/components/progress-buttons.tsx
index 3c8b8cb7..bd2dca2f 100644
--- a/frontend/src/features/model-creation/components/progress-buttons.tsx
+++ b/frontend/src/features/model-creation/components/progress-buttons.tsx
@@ -34,6 +34,7 @@ const ProgressButtons: React.FC = ({
handleTrainingDatasetCreation,
trainingDatasetCreationInProgress,
isEditMode,
+ isModelOwner,
} = useModelsContext();
const nextPage = () => {
@@ -150,6 +151,15 @@ const ProgressButtons: React.FC = ({
label={MODELS_CONTENT.modelCreation.progressButtons.back}
iconClassName="rotate-90"
onClick={prevPage}
+ /**
+ * If the user is not the owner of the model and they are in edit mode, then don't let them go back on
+ * the training area page. Since the back page is training dataset, which they're not authorized to change.
+ */
+ disabled={
+ !isModelOwner &&
+ isEditMode &&
+ currentPath.includes(MODELS_ROUTES.TRAINING_AREA)
+ }
/>
{
const { handleChange } = useModelsContext();
- const { isPending, data, isError } = useGetTMSTileJSON(tileJSONURL);
+ const { isPending, data, isError } = useGetTMSTileJSON(
+ tileJSONURL,
+ !!trainingDatasetId,
+ );
const { data: trainingDataset, isError: trainingDatasetFetchError } =
useGetTrainingDataset(trainingDatasetId);
diff --git a/frontend/src/features/model-creation/components/training-area/training-area-item.tsx b/frontend/src/features/model-creation/components/training-area/training-area-item.tsx
index 777ac68c..fa9c2cf6 100644
--- a/frontend/src/features/model-creation/components/training-area/training-area-item.tsx
+++ b/frontend/src/features/model-creation/components/training-area/training-area-item.tsx
@@ -6,7 +6,7 @@ import { LabelStatus } from "@/enums/training-area";
import { Map } from "maplibre-gl";
import { ToolTip } from "@/components/ui/tooltip";
import { TRAINING_AREA_LABELS_FETCH_POOLING_TIME_MS } from "@/config";
-import { useCallback, useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { useDialog } from "@/hooks/use-dialog";
import { useDropdownMenu } from "@/hooks/use-dropdown-menu";
import { useModelsContext } from "@/app/providers/models-provider";
@@ -38,8 +38,9 @@ import {
useGetTrainingArea,
useGetTrainingAreaLabels,
useGetTrainingAreaLabelsFromOSM,
- useGetTrainingAreas,
} from "@/features/model-creation/hooks/use-training-areas";
+import { useQueryClient } from "@tanstack/react-query";
+import { QUERY_KEYS } from "@/services";
type LabelState = {
isFetching: boolean;
@@ -50,19 +51,15 @@ type LabelState = {
shouldPoll: boolean;
};
-export function useInterval(callback: () => void, delay: number | null) {
- const savedCallback = useRef(callback);
-
- useEffect(() => {
- savedCallback.current = callback;
- }, [callback]);
-
- useEffect(() => {
- if (delay === null) return;
- const id = setInterval(() => savedCallback.current(), delay);
- return () => clearInterval(id);
- }, [delay]);
-}
+type TDropdownMenuItems = {
+ tooltip: string;
+ isIcon?: boolean;
+ imageSrc?: string;
+ disabled: boolean;
+ onClick: () => void;
+ Icon?: React.FC;
+ isDelete?: boolean;
+}[];
const LabelFetchStatus = ({
fetchedDate,
@@ -109,15 +106,6 @@ const LabelFetchStatus = ({
);
};
-type TDropdownMenuItems = {
- tooltip: string;
- isIcon?: boolean;
- imageSrc?: string;
- disabled: boolean;
- onClick: () => void;
- Icon?: React.FC;
- isDelete?: boolean;
-}[];
const DropdownMenu = ({
dropdownMenuItems,
@@ -176,6 +164,7 @@ export const TrainingAreaItem: React.FC<
map: Map | null;
}
> = ({ datasetId, offset, map, ...trainingArea }) => {
+ const queryClient = useQueryClient();
const initialLabelState: LabelState = {
isFetching: false,
error: false,
@@ -265,10 +254,10 @@ export const TrainingAreaItem: React.FC<
},
});
- const { refetch: refetchTrainingAreas } = useGetTrainingAreas(
- datasetId,
- offset,
- );
+ // const { refetch: refetchTrainingAreas } = useGetTrainingAreas(
+ // datasetId,
+ // offset,
+ // );
const handleFetchLabels = useCallback(() => {
setLabelState((prev) => ({
@@ -288,7 +277,10 @@ export const TrainingAreaItem: React.FC<
shouldPoll: false,
errorToastShown: false,
}));
- refetchTrainingAreas();
+ // refetchTrainingAreas();
+ queryClient.invalidateQueries({
+ queryKey: [QUERY_KEYS.TRAINING_AREAS(datasetId, offset)],
+ });
showSuccessToast(
`Training labels for Training Area ${trainingArea.id} have been successfully fetched.`,
);
diff --git a/frontend/src/features/model-creation/hooks/use-models.ts b/frontend/src/features/model-creation/hooks/use-models.ts
index cf16056f..f66d249e 100644
--- a/frontend/src/features/model-creation/hooks/use-models.ts
+++ b/frontend/src/features/model-creation/hooks/use-models.ts
@@ -3,12 +3,11 @@ import {
createModel,
TCreateModelArgs,
} from "@/features/model-creation/api/create-models";
-import { MutationConfig, queryKeys } from "@/services";
+import { MutationConfig, QUERY_KEYS } from "@/services";
import {
createTrainingRequest,
TCreateTrainingRequestArgs,
} from "@/features/model-creation/api/create-trainings";
-import { useModelDetails } from "@/features/models/hooks/use-models";
import {
TUpdateModelArgs,
updateModel,
@@ -58,7 +57,6 @@ export const useUpdateModel = ({
mutationConfig,
modelId,
}: useUpdateModelOptions) => {
- const { refetch: refetchModelDetails } = useModelDetails(modelId);
const queryClient = useQueryClient();
const { onSuccess, ...restConfig } = mutationConfig || {};
@@ -66,9 +64,8 @@ export const useUpdateModel = ({
mutationFn: (args: TUpdateModelArgs) => updateModel(args),
onSuccess: (...args) => {
queryClient.invalidateQueries({
- queryKey: [queryKeys.MODEL_DETAILS(modelId)],
+ queryKey: [QUERY_KEYS.MODEL_DETAILS(modelId)],
});
- refetchModelDetails();
onSuccess?.(...args);
},
...restConfig,
diff --git a/frontend/src/features/model-creation/hooks/use-tms-tilejson.ts b/frontend/src/features/model-creation/hooks/use-tms-tilejson.ts
index 9378e1f2..9e3071ef 100644
--- a/frontend/src/features/model-creation/hooks/use-tms-tilejson.ts
+++ b/frontend/src/features/model-creation/hooks/use-tms-tilejson.ts
@@ -1,8 +1,9 @@
import { getTMSTileJSONQueryOptions } from "@/features/model-creation/api/factory";
import { useQuery } from "@tanstack/react-query";
-export const useGetTMSTileJSON = (url: string) => {
+export const useGetTMSTileJSON = (url: string, enabled: boolean) => {
return useQuery({
...getTMSTileJSONQueryOptions(url),
+ enabled: enabled,
});
};
diff --git a/frontend/src/features/models/api/factory.ts b/frontend/src/features/models/api/factory.ts
index f1a832e8..d7b744f9 100644
--- a/frontend/src/features/models/api/factory.ts
+++ b/frontend/src/features/models/api/factory.ts
@@ -1,5 +1,5 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
-import { queryKeys } from "@/services";
+import { QUERY_KEYS } from "@/services";
import {
getModels,
getModelDetails,
@@ -60,12 +60,14 @@ export const getModelsQueryOptions = ({
export const getModelDetailsQueryOptions = (
id: string,
refetchInterval: boolean | number,
+ enabled: boolean,
) => {
return queryOptions({
- queryKey: [queryKeys.MODEL_DETAILS(id)],
+ queryKey: [QUERY_KEYS.MODEL_DETAILS(id)],
queryFn: () => getModelDetails(id),
//@ts-expect-error bad type definition
refetchInterval: refetchInterval,
+ enabled: enabled,
});
};
diff --git a/frontend/src/features/models/api/update-trainings.ts b/frontend/src/features/models/api/update-trainings.ts
index 83e20048..9d19be46 100644
--- a/frontend/src/features/models/api/update-trainings.ts
+++ b/frontend/src/features/models/api/update-trainings.ts
@@ -17,7 +17,10 @@ export const useUpdateTraining = ({
mutationConfig,
modelId,
}: UseUpdateTrainingOptions) => {
- const { refetch: refetchModelDetails } = useModelDetails(String(modelId));
+ const { refetch: refetchModelDetails } = useModelDetails(
+ String(modelId),
+ !!modelId,
+ );
const { refetch: refetchTrainingHistory } = useTrainingHistory(
String(modelId),
0,
diff --git a/frontend/src/features/models/components/dialogs/model-enhancement-dialog.tsx b/frontend/src/features/models/components/dialogs/model-enhancement-dialog.tsx
index ecd4b9da..fb70a394 100644
--- a/frontend/src/features/models/components/dialogs/model-enhancement-dialog.tsx
+++ b/frontend/src/features/models/components/dialogs/model-enhancement-dialog.tsx
@@ -64,6 +64,7 @@ const ModelEnhancementDialog: React.FC = ({
closeDialog={handleClose}
modelId={modelId}
/>
+
{options.map((option, id) => (
= ({
closeDialog,
modelId,
}) => {
- const { data, isPending, isError } = useModelDetails(modelId);
-
- const { handleChange, formData, createNewTrainingRequestMutation } =
- useModelsContext();
+ const {
+ handleChange,
+ formData,
+ createNewTrainingRequestMutation,
+ data,
+ isPending,
+ isError,
+ } = useModelsContext();
{
/*
diff --git a/frontend/src/features/models/components/model-details-info.tsx b/frontend/src/features/models/components/model-details-info.tsx
index b3b04466..f7156830 100644
--- a/frontend/src/features/models/components/model-details-info.tsx
+++ b/frontend/src/features/models/components/model-details-info.tsx
@@ -19,15 +19,11 @@ const ModelDetailsInfo = ({
openModelFilesDialog,
openTrainingAreaDrawer,
trainingDataset,
- isError,
- isPending,
}: {
data: TModelDetails;
openModelFilesDialog: () => void;
openTrainingAreaDrawer: () => void;
trainingDataset: TTrainingDataset;
- isError: boolean;
- isPending: boolean;
}) => {
const { isOpened, openDialog, closeDialog } = useDialog();
const { user, isAuthenticated } = useAuth();
@@ -105,21 +101,15 @@ const ModelDetailsInfo = ({
{MODELS_CONTENT.models.modelsDetailsCard.datasetName}
- {isPending ? (
-
- ) : isError ? (
- Error retrieving dataset info
- ) : (
-
- {truncateString(trainingDataset?.name, 40)}
-
- )}
+
+ {truncateString(trainingDataset?.name, 40)}
+
{MODELS_CONTENT.models.modelsDetailsCard.datasetId}
-
{data?.dataset}
+
{data?.dataset.id}
{
return useQuery({
- ...getModelDetailsQueryOptions(id, refetchInterval),
+ ...getModelDetailsQueryOptions(id, refetchInterval, enabled),
retry: (_, error) => {
// When a model is not found, don't retry.
//@ts-expect-error bad type definition
return error.response?.status !== 404;
},
- enabled: enabled,
});
};
diff --git a/frontend/src/features/start-mapping/components/header.tsx b/frontend/src/features/start-mapping/components/header.tsx
index c8737df5..884caf66 100644
--- a/frontend/src/features/start-mapping/components/header.tsx
+++ b/frontend/src/features/start-mapping/components/header.tsx
@@ -11,7 +11,7 @@ import { ModelPredictionsTracker } from "@/features/start-mapping/components/mod
import { ModelSettings } from "@/features/start-mapping/components/model-settings";
import { SkeletonWrapper } from "@/components/ui/skeleton";
import { TDownloadOptions, TQueryParams } from "@/app/routes/start-mapping";
-import { TModel, TModelPredictions, TTrainingDataset } from "@/types";
+import { TModelDetails, TModelPredictions } from "@/types";
import { ToolTip } from "@/components/ui/tooltip";
import { useDropdownMenu } from "@/hooks/use-dropdown-menu";
import { UserProfile } from "@/components/layout";
@@ -21,8 +21,8 @@ const StartMappingHeader = ({
modelInfo,
modelPredictions,
modelPredictionsExist,
- trainingDatasetIsPending,
- trainingDatasetIsError,
+ modelInfoRequestIsPending,
+ modelInfoRequestIsError,
query,
updateQuery,
setModelPredictions,
@@ -33,13 +33,12 @@ const StartMappingHeader = ({
modelDetailsPopupIsActive,
downloadOptions,
clearPredictions,
- trainingDataset,
currentZoom,
}: {
modelPredictionsExist: boolean;
- trainingDatasetIsPending: boolean;
- trainingDatasetIsError: boolean;
- modelInfo: TModel;
+ modelInfoRequestIsPending: boolean;
+ modelInfoRequestIsError: boolean;
+ modelInfo: TModelDetails;
modelPredictions: TModelPredictions;
query: TQueryParams;
updateQuery: (newParams: TQueryParams) => void;
@@ -51,7 +50,6 @@ const StartMappingHeader = ({
modelDetailsPopupIsActive: boolean;
downloadOptions: TDownloadOptions;
clearPredictions: () => void;
- trainingDataset: TTrainingDataset;
currentZoom: number;
}) => {
const { onDropdownHide, onDropdownShow, dropdownIsOpened } =
@@ -65,7 +63,7 @@ const StartMappingHeader = ({
return (
@@ -138,7 +136,6 @@ const StartMappingHeader = ({
map={map}
disablePrediction={disablePrediction}
query={query}
- trainingDataset={trainingDataset}
currentZoom={currentZoom}
modelInfo={modelInfo}
/>
diff --git a/frontend/src/features/start-mapping/components/map/layers/accepted-prediction-layer.tsx b/frontend/src/features/start-mapping/components/map/layers/accepted-prediction-layer.tsx
new file mode 100644
index 00000000..37cf114c
--- /dev/null
+++ b/frontend/src/features/start-mapping/components/map/layers/accepted-prediction-layer.tsx
@@ -0,0 +1,70 @@
+import {
+ ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID,
+ ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
+ ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID,
+} from "@/config";
+import { Feature, GeoJSONType } from "@/types";
+import { GeoJSONSource, Map } from "maplibre-gl";
+import { useEffect, useMemo } from "react";
+
+export const AcceptedPredictionsLayer = ({
+ map,
+ features,
+}: {
+ map: Map | null;
+ features: Feature[];
+}) => {
+ const geoJsonData = useMemo(
+ () => ({
+ type: "FeatureCollection",
+ features: features,
+ }),
+ [features],
+ );
+
+ useEffect(() => {
+ if (!map) return;
+ if (!map.getSource(ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID)) {
+ map.addSource(ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID, {
+ type: "geojson",
+ data: { type: "FeatureCollection", features: [] },
+ });
+ }
+ if (!map.getLayer(ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID)) {
+ map.addLayer({
+ id: ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID,
+ type: "fill",
+ source: ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID,
+ paint: {
+ "fill-color": "#23C16B",
+ "fill-opacity": 0.2,
+ },
+ layout: { visibility: "visible" },
+ });
+ }
+ if (!map.getLayer(ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID)) {
+ map.addLayer({
+ id: ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
+ type: "line",
+ source: ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID,
+ paint: {
+ "line-color": "#23C16B",
+ "line-width": 2,
+ },
+ layout: { visibility: "visible" },
+ });
+ }
+ }, [map]);
+
+ useEffect(() => {
+ if (!map || !features) return;
+ const source = map.getSource(
+ ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID,
+ ) as GeoJSONSource;
+ if (source) {
+ source.setData(geoJsonData as GeoJSONType);
+ }
+ }, [map, geoJsonData]);
+
+ return null;
+};
diff --git a/frontend/src/features/start-mapping/components/map/layers/all-prediction-layer.tsx b/frontend/src/features/start-mapping/components/map/layers/all-prediction-layer.tsx
new file mode 100644
index 00000000..f5e948c9
--- /dev/null
+++ b/frontend/src/features/start-mapping/components/map/layers/all-prediction-layer.tsx
@@ -0,0 +1,74 @@
+import {
+ ALL_MODEL_PREDICTIONS_FILL_LAYER_ID,
+ ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
+ ALL_MODEL_PREDICTIONS_SOURCE_ID,
+} from "@/config";
+import { Feature, GeoJSONType } from "@/types";
+import { GeoJSONSource, Map } from "maplibre-gl";
+import { useEffect, useMemo } from "react";
+
+type AllPredictionsLayerProps = {
+ map: Map | null;
+ features: Feature[];
+};
+
+export const AllPredictionsLayer = ({
+ map,
+ features,
+}: AllPredictionsLayerProps) => {
+ const geoJsonData = useMemo(
+ () => ({
+ type: "FeatureCollection",
+ features: features,
+ }),
+ [features],
+ );
+
+ useEffect(() => {
+ if (!map) return;
+ if (!map.getSource(ALL_MODEL_PREDICTIONS_SOURCE_ID)) {
+ map.addSource(ALL_MODEL_PREDICTIONS_SOURCE_ID, {
+ type: "geojson",
+ data: { type: "FeatureCollection", features: [] },
+ });
+ }
+
+ if (!map.getLayer(ALL_MODEL_PREDICTIONS_FILL_LAYER_ID)) {
+ map.addLayer({
+ id: ALL_MODEL_PREDICTIONS_FILL_LAYER_ID,
+ type: "fill",
+ source: ALL_MODEL_PREDICTIONS_SOURCE_ID,
+ paint: {
+ "fill-color": "#A243DC",
+ "fill-opacity": 0.2,
+ },
+ layout: { visibility: "visible" },
+ });
+ }
+
+ if (!map.getLayer(ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID)) {
+ map.addLayer({
+ id: ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
+ type: "line",
+ source: ALL_MODEL_PREDICTIONS_SOURCE_ID,
+ paint: {
+ "line-color": "#A243DC",
+ "line-width": 2,
+ },
+ layout: { visibility: "visible" },
+ });
+ }
+ }, [map]);
+
+ useEffect(() => {
+ if (!map || !features) return;
+ const source = map.getSource(
+ ALL_MODEL_PREDICTIONS_SOURCE_ID,
+ ) as GeoJSONSource;
+ if (source) {
+ source.setData(geoJsonData as GeoJSONType);
+ }
+ }, [map, geoJsonData]);
+
+ return null;
+};
diff --git a/frontend/src/features/start-mapping/components/map/layers/index.ts b/frontend/src/features/start-mapping/components/map/layers/index.ts
new file mode 100644
index 00000000..1fa43343
--- /dev/null
+++ b/frontend/src/features/start-mapping/components/map/layers/index.ts
@@ -0,0 +1,3 @@
+export { AcceptedPredictionsLayer } from "./accepted-prediction-layer";
+export { RejectedPredictionsLayer } from "./rejected-prediction-layer";
+export { AllPredictionsLayer } from "./all-prediction-layer";
diff --git a/frontend/src/features/start-mapping/components/map/layers/rejected-prediction-layer.tsx b/frontend/src/features/start-mapping/components/map/layers/rejected-prediction-layer.tsx
new file mode 100644
index 00000000..41f29cb6
--- /dev/null
+++ b/frontend/src/features/start-mapping/components/map/layers/rejected-prediction-layer.tsx
@@ -0,0 +1,72 @@
+import {
+ REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID,
+ REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
+ REJECTED_MODEL_PREDICTIONS_SOURCE_ID,
+} from "@/config";
+import { Feature, GeoJSONType } from "@/types";
+import { GeoJSONSource, Map } from "maplibre-gl";
+import { useEffect, useMemo } from "react";
+
+export const RejectedPredictionsLayer = ({
+ map,
+ features,
+}: {
+ map: Map | null;
+ features: Feature[];
+}) => {
+ const geoJsonData = useMemo(
+ () => ({
+ type: "FeatureCollection",
+ features: features,
+ }),
+ [features],
+ );
+
+ useEffect(() => {
+ if (!map) return;
+
+ if (!map.getSource(REJECTED_MODEL_PREDICTIONS_SOURCE_ID)) {
+ map.addSource(REJECTED_MODEL_PREDICTIONS_SOURCE_ID, {
+ type: "geojson",
+ data: { type: "FeatureCollection", features: [] },
+ });
+ }
+
+ if (!map.getLayer(REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID)) {
+ map.addLayer({
+ id: REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID,
+ type: "fill",
+ source: REJECTED_MODEL_PREDICTIONS_SOURCE_ID,
+ paint: {
+ "fill-color": "#D63F40",
+ "fill-opacity": 0.2,
+ },
+ layout: { visibility: "visible" },
+ });
+ }
+ if (!map.getLayer(REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID)) {
+ map.addLayer({
+ id: REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
+ type: "line",
+ source: REJECTED_MODEL_PREDICTIONS_SOURCE_ID,
+ paint: {
+ "line-color": "#D63F40",
+ "line-width": 2,
+ },
+ layout: { visibility: "visible" },
+ });
+ }
+ }, [map]);
+
+ useEffect(() => {
+ if (!map || !features) return;
+ const source = map.getSource(
+ REJECTED_MODEL_PREDICTIONS_SOURCE_ID,
+ ) as GeoJSONSource;
+ if (source) {
+ source.setData(geoJsonData as GeoJSONType);
+ }
+ }, [map, features]);
+
+ return null;
+};
diff --git a/frontend/src/features/start-mapping/components/map/map.tsx b/frontend/src/features/start-mapping/components/map/map.tsx
index 48c004af..ae426356 100644
--- a/frontend/src/features/start-mapping/components/map/map.tsx
+++ b/frontend/src/features/start-mapping/components/map/map.tsx
@@ -2,23 +2,20 @@ import PredictedFeatureActionPopup from "@/features/start-mapping/components/fea
import useScreenSize from "@/hooks/use-screen-size";
import { ControlsPosition } from "@/enums";
import { extractTileJSONURL, showErrorToast } from "@/utils";
-import { GeoJSONSource, LngLatBoundsLike, Map } from "maplibre-gl";
+import { LngLatBoundsLike, Map } from "maplibre-gl";
import { Legend } from "@/features/start-mapping/components";
import { MapComponent, MapCursorToolTip } from "@/components/map";
import { TOAST_NOTIFICATIONS } from "@/constants";
-import { useMapLayers } from "@/hooks/use-map-layer";
import { useToolTipVisibility } from "@/hooks/use-tooltip-visibility";
import {
Dispatch,
RefObject,
SetStateAction,
- useCallback,
useEffect,
useState,
} from "react";
import {
- GeoJSONType,
TileJSON,
TModelPredictionFeature,
TModelPredictions,
@@ -26,17 +23,17 @@ import {
} from "@/types";
import {
ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID,
- ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
- ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID,
ALL_MODEL_PREDICTIONS_FILL_LAYER_ID,
- ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
- ALL_MODEL_PREDICTIONS_SOURCE_ID,
MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION,
MINIMUM_ZOOM_LEVEL_INSTRUCTION_FOR_PREDICTION,
REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID,
- REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
- REJECTED_MODEL_PREDICTIONS_SOURCE_ID,
} from "@/config";
+import bbox from "@turf/bbox";
+import {
+ AcceptedPredictionsLayer,
+ RejectedPredictionsLayer,
+ AllPredictionsLayer,
+} from "@/features/start-mapping/components/map/layers";
export const StartMappingMapComponent = ({
trainingDataset,
@@ -44,7 +41,6 @@ export const StartMappingMapComponent = ({
setModelPredictions,
oamTileJSONIsError,
oamTileJSON,
- oamTileJSONError,
modelPredictionsExist,
map,
mapContainerRef,
@@ -52,10 +48,12 @@ export const StartMappingMapComponent = ({
layers,
tmsBounds,
trainingId,
+ modelInfoRequestIsPending,
}: {
trainingId: number;
trainingDataset?: TTrainingDataset;
modelPredictions: TModelPredictions;
+
setModelPredictions: Dispatch<
SetStateAction<{
all: TModelPredictionFeature[];
@@ -66,7 +64,6 @@ export const StartMappingMapComponent = ({
oamTileJSONIsError: boolean;
oamTileJSON: TileJSON;
- oamTileJSONError: any;
modelPredictionsExist: boolean;
map: Map | null;
currentZoom: number;
@@ -76,6 +73,7 @@ export const StartMappingMapComponent = ({
subLayers: string[];
}[];
tmsBounds: LngLatBoundsLike;
+ modelInfoRequestIsPending: boolean;
}) => {
const tileJSONURL = extractTileJSONURL(trainingDataset?.source_imagery ?? "");
const [showPopup, setShowPopup] = useState
(false);
@@ -90,156 +88,39 @@ export const StartMappingMapComponent = ({
useEffect(() => {
if (!oamTileJSONIsError) return;
showErrorToast(undefined, TOAST_NOTIFICATIONS.trainingDataset.error);
- }, [oamTileJSONIsError, oamTileJSONError]);
+ }, [oamTileJSONIsError]);
useEffect(() => {
- if (!map || !tmsBounds || oamTileJSONIsError) return;
- map.fitBounds(tmsBounds);
- }, [map, tmsBounds, oamTileJSONIsError, oamTileJSON]);
-
- // Add the map layers
- useMapLayers(
- // layers
- [
- // accepted
- {
- id: ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID,
- type: "fill",
- source: ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID,
- paint: {
- "fill-color": "#23C16B",
- "fill-opacity": 0.2,
- },
- layout: { visibility: "visible" },
- },
- {
- id: ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
- type: "line",
- source: ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID,
- paint: {
- "line-color": "#23C16B",
- "line-width": 2,
- },
- layout: { visibility: "visible" },
- },
- // rejected
- {
- id: REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID,
- type: "fill",
- source: REJECTED_MODEL_PREDICTIONS_SOURCE_ID,
- paint: {
- "fill-color": "#D63F40",
- "fill-opacity": 0.2,
- },
- layout: { visibility: "visible" },
- },
- {
- id: REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
- type: "line",
- source: REJECTED_MODEL_PREDICTIONS_SOURCE_ID,
- paint: {
- "line-color": "#D63F40",
- "line-width": 2,
- },
- layout: { visibility: "visible" },
- },
- // all
- {
- id: ALL_MODEL_PREDICTIONS_FILL_LAYER_ID,
- type: "fill",
- source: ALL_MODEL_PREDICTIONS_SOURCE_ID,
- paint: {
- "fill-color": "#A243DC",
- "fill-opacity": 0.2,
- },
- layout: { visibility: "visible" },
- },
- {
- id: ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID,
- type: "line",
- source: ALL_MODEL_PREDICTIONS_SOURCE_ID,
- paint: {
- "line-color": "#A243DC",
- "line-width": 2,
- },
- layout: { visibility: "visible" },
- },
- ],
- // sources
- [
- {
- id: ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID,
- spec: {
- type: "geojson",
- data: { type: "FeatureCollection", features: [] },
- },
- },
- {
- id: REJECTED_MODEL_PREDICTIONS_SOURCE_ID,
- spec: {
- type: "geojson",
- data: { type: "FeatureCollection", features: [] },
- },
- },
- {
- id: ALL_MODEL_PREDICTIONS_SOURCE_ID,
- spec: {
- type: "geojson",
- data: { type: "FeatureCollection", features: [] },
- },
- },
- ],
- map,
- );
-
- const updateLayers = useCallback(() => {
- if (map) {
- if (map?.getSource(ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID)) {
- const source = map.getSource(
- ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID,
- ) as GeoJSONSource;
- source.setData({
- type: "FeatureCollection",
- features: modelPredictions.accepted,
- } as GeoJSONType);
- }
-
- if (map?.getSource(REJECTED_MODEL_PREDICTIONS_SOURCE_ID)) {
- const source = map.getSource(
- REJECTED_MODEL_PREDICTIONS_SOURCE_ID,
- ) as GeoJSONSource;
- source.setData({
- type: "FeatureCollection",
- features: modelPredictions.rejected,
- } as GeoJSONType);
- }
-
- if (map?.getSource(ALL_MODEL_PREDICTIONS_SOURCE_ID)) {
- const source = map.getSource(
- ALL_MODEL_PREDICTIONS_SOURCE_ID,
- ) as GeoJSONSource;
- source.setData({
+ if (!map || !tmsBounds || oamTileJSONIsError || modelInfoRequestIsPending)
+ return;
+
+ // if there are predictions that the user hasn't interacted with, zoom to them.
+ if (modelPredictions.all.length > 0) {
+ // get the bbox of the features with turf.
+ map.fitBounds(
+ bbox({
type: "FeatureCollection",
features: modelPredictions.all,
- } as GeoJSONType);
- }
+ }) as LngLatBoundsLike,
+ );
+ } else {
+ map.fitBounds(tmsBounds);
}
- }, [map, modelPredictions]);
-
- useEffect(() => {
- if (!map) return;
- updateLayers();
- }, [map, updateLayers, modelPredictions]);
+ }, [
+ map,
+ tmsBounds,
+ oamTileJSONIsError,
+ oamTileJSON,
+ modelInfoRequestIsPending,
+ ]);
useEffect(() => {
if (!map) return;
-
const layerIds = [
ALL_MODEL_PREDICTIONS_FILL_LAYER_ID,
ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID,
REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID,
];
-
const handleMouseEnter = () => {
map.getCanvas().style.cursor = "pointer";
};
@@ -270,10 +151,10 @@ export const StartMappingMapComponent = ({
const showTooltip =
currentZoom < MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION && tooltipVisible;
+
return (
+ {!modelInfoRequestIsPending && (
+ <>
+
+
+
+ >
+ )}
+
{showPopup && (
void;
clearPredictions: () => void;
- trainingDataset: TTrainingDataset;
currentZoom: number;
- modelInfo: TModel;
+ modelInfo: TModelDetails;
}) => {
const [showDownloadOptions, setShowDownloadOptions] =
useState(false);
@@ -62,7 +60,6 @@ export const StartMappingMobileDrawer = ({
map={map}
disablePrediction={disablePrediction}
modelPredictions={modelPredictions}
- trainingDataset={trainingDataset}
currentZoom={currentZoom}
modelInfo={modelInfo}
/>
diff --git a/frontend/src/features/start-mapping/components/model-action.tsx b/frontend/src/features/start-mapping/components/model-action.tsx
index 807ab78d..3f86b81f 100644
--- a/frontend/src/features/start-mapping/components/model-action.tsx
+++ b/frontend/src/features/start-mapping/components/model-action.tsx
@@ -3,11 +3,10 @@ import { Map } from "maplibre-gl";
import { START_MAPPING_PAGE_CONTENT, TOAST_NOTIFICATIONS } from "@/constants";
import {
BBOX,
- TModel,
+ TModelDetails,
TModelPredictions,
TModelPredictionsConfig,
TQueryParams,
- TTrainingDataset,
} from "@/types";
import { ToolTip } from "@/components/ui/tooltip";
import { useCallback } from "react";
@@ -23,7 +22,6 @@ const ModelAction = ({
map,
disablePrediction,
query,
- trainingDataset,
currentZoom,
modelInfo,
}: {
@@ -32,9 +30,8 @@ const ModelAction = ({
map: Map | null;
disablePrediction: boolean;
query: TQueryParams;
- trainingDataset: TTrainingDataset;
currentZoom: number;
- modelInfo: TModel;
+ modelInfo: TModelDetails;
}) => {
const { modelId } = useParams();
@@ -44,11 +41,11 @@ const ModelAction = ({
area_threshold: query[SEARCH_PARAMS.area] as number,
use_josm_q: query[SEARCH_PARAMS.useJOSMQ] as boolean,
confidence: query[SEARCH_PARAMS.confidenceLevel] as number,
- checkpoint: `/mnt/efsmount/data/trainings/dataset_${modelInfo?.dataset}/output/training_${modelInfo?.published_training}/checkpoint${PREDICTION_API_FILE_EXTENSIONS[modelInfo?.base_model as BASE_MODELS]}`,
+ checkpoint: `/mnt/efsmount/data/trainings/dataset_${modelInfo?.dataset?.id}/output/training_${modelInfo?.published_training}/checkpoint${PREDICTION_API_FILE_EXTENSIONS[modelInfo?.base_model as BASE_MODELS]}`,
max_angle_change: 15,
model_id: modelId as string,
skew_tolerance: 15,
- source: trainingDataset?.source_imagery as string,
+ source: modelInfo?.dataset?.source_imagery as string,
zoom_level: currentZoom,
bbox: [
map?.getBounds().getWest(),
@@ -57,7 +54,7 @@ const ModelAction = ({
map?.getBounds().getNorth(),
] as BBOX,
};
- }, [map, query, currentZoom, trainingDataset, modelInfo]);
+ }, [map, query, currentZoom, modelInfo]);
const modelPredictionMutation = useGetModelPredictions({
mutationConfig: {
diff --git a/frontend/src/features/start-mapping/components/model-details-popup.tsx b/frontend/src/features/start-mapping/components/model-details-popup.tsx
index 0066f472..eac55f0f 100644
--- a/frontend/src/features/start-mapping/components/model-details-popup.tsx
+++ b/frontend/src/features/start-mapping/components/model-details-popup.tsx
@@ -4,8 +4,7 @@ import { extractDatePart, roundNumber, truncateString } from "@/utils";
import { MobileDrawer } from "@/components/ui/drawer";
import { Popup } from "@/components/ui/popup";
import { SkeletonWrapper } from "@/components/ui/skeleton";
-import { TModelDetails, TTrainingDataset } from "@/types";
-import { useModelDetails } from "@/features/models/hooks/use-models";
+import { TModelDetails } from "@/types";
import { useTrainingDetails } from "@/features/models/hooks/use-training";
import { START_MAPPING_PAGE_CONTENT } from "@/constants";
@@ -13,84 +12,67 @@ const ModelDetailsPopUp = ({
showPopup,
anchor,
handlePopup,
- modelId,
- model,
- trainingDatasetIsError,
- trainingDataset,
- trainingDatasetIsPending,
+ modelInfo,
+ modelInfoRequestIsError,
+ modelInfoRequestIsPending,
closeMobileDrawer,
}: {
showPopup: boolean;
anchor: string;
handlePopup: () => void;
- modelId?: string;
- model?: TModelDetails;
- trainingDataset?: TTrainingDataset;
- trainingDatasetIsPending?: boolean;
- trainingDatasetIsError?: boolean;
+ modelInfo?: TModelDetails;
+ modelInfoRequestIsPending?: boolean;
+ modelInfoRequestIsError?: boolean;
closeMobileDrawer: () => void;
}) => {
- const { data, isPending, isError } = useModelDetails(
- modelId as string,
- modelId ? modelId !== undefined : false,
- );
-
const {
data: trainingDetails,
isPending: trainingDetailsIsPending,
isError: trainingDetailsError,
- } = useTrainingDetails(
- model?.published_training ?? (data?.published_training as number),
- );
+ } = useTrainingDetails(modelInfo?.published_training as number);
const { isSmallViewport } = useScreenSize();
const popupContent = (
{START_MAPPING_PAGE_CONTENT.modelDetails.popover.modelId}:{" "}
- {model?.id ?? data?.id}
+ {modelInfo?.id}
{START_MAPPING_PAGE_CONTENT.modelDetails.popover.description}:{" "}
-
- {model?.description ?? data?.description}
-
+ {modelInfo?.description}
{START_MAPPING_PAGE_CONTENT.modelDetails.popover.lastModified}:{" "}
- {extractDatePart(
- model?.last_modified ?? (data?.last_modified as string),
- )}
+ {extractDatePart(modelInfo?.last_modified as string)}
{START_MAPPING_PAGE_CONTENT.modelDetails.popover.trainingId}:{" "}
-
- {model?.published_training ?? data?.published_training}
-
+ {modelInfo?.published_training}
{START_MAPPING_PAGE_CONTENT.modelDetails.popover.datasetId}:{" "}
- {model?.dataset ?? data?.dataset}
+ {modelInfo?.dataset.id}
{START_MAPPING_PAGE_CONTENT.modelDetails.popover.datasetName}:{" "}
- {trainingDatasetIsError
+ {modelInfoRequestIsError
? "N/A"
- : truncateString(trainingDataset?.name, 40)}{" "}
+ : truncateString(modelInfo?.dataset?.name, 40)}{" "}
@@ -112,14 +94,12 @@ const ModelDetailsPopUp = ({
{START_MAPPING_PAGE_CONTENT.modelDetails.popover.accuracy}:{" "}
- {roundNumber(model?.accuracy ?? (data?.accuracy as number), 2)}%
+ {roundNumber(modelInfo?.accuracy as number, 2)}%
{START_MAPPING_PAGE_CONTENT.modelDetails.popover.baseModel}:{" "}
-
- {model?.base_model ?? data?.base_model}
-
+ {modelInfo?.base_model}
@@ -135,7 +115,7 @@ const ModelDetailsPopUp = ({
canClose
>
- {!model && isError ? (
+ {!modelInfo && modelInfoRequestIsError ? (
{START_MAPPING_PAGE_CONTENT.modelDetails.error}
) : (
@@ -160,7 +140,7 @@ const ModelDetailsPopUp = ({
- {!model && isError ? (
+ {!modelInfo && modelInfoRequestIsError ? (
{START_MAPPING_PAGE_CONTENT.modelDetails.error}
) : (
diff --git a/frontend/src/hooks/use-map-instance.ts b/frontend/src/hooks/use-map-instance.ts
index d83b69e3..a83364bc 100644
--- a/frontend/src/hooks/use-map-instance.ts
+++ b/frontend/src/hooks/use-map-instance.ts
@@ -45,6 +45,7 @@ export const useMapInstance = (pmtiles: boolean = false) => {
if (!map) return;
// There is a mismatch of 1 in the mag.getZoom() results and the actual zoom level of the map.
// Adding 1 to the result resolves it.
+
setCurrentZoom(Math.round(map.getZoom()) + 1);
}, [map]);
diff --git a/frontend/src/layouts/model-forms-layout.tsx b/frontend/src/layouts/model-forms-layout.tsx
index 365cd8f3..8a32d96f 100644
--- a/frontend/src/layouts/model-forms-layout.tsx
+++ b/frontend/src/layouts/model-forms-layout.tsx
@@ -83,7 +83,7 @@ export const ModelFormsLayout = () => {
/>
-
+
{
-
- {!pathname.includes(APPLICATION_ROUTES.AUTH_CALLBACK) && }
-
+
{!pathname.includes(APPLICATION_ROUTES.AUTH_CALLBACK) &&
!pathname.includes(APPLICATION_ROUTES.START_MAPPING_BASE) && (
-
+ <>
+
+
+ >
)}
[0]
>;
-export const queryKeys = {
+export const QUERY_KEYS = {
MODEL_DETAILS: (modelId: string) => `model-details-${modelId}`,
+ TRAINING_AREAS: (datasetId: number, offset: number) =>
+ `training-areas-${datasetId}-${offset}`,
};
diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts
index 46dd7680..f0bbba76 100644
--- a/frontend/src/types/api.ts
+++ b/frontend/src/types/api.ts
@@ -70,6 +70,7 @@ export type PaginatedTrainings = {
export type TModelDetails = TModel & {
description: string;
+ dataset: TTrainingDataset;
};
export type TTrainingAreaFeature = {
diff --git a/frontend/src/types/ui-contents.ts b/frontend/src/types/ui-contents.ts
index 5f77c180..0f34a1ba 100644
--- a/frontend/src/types/ui-contents.ts
+++ b/frontend/src/types/ui-contents.ts
@@ -126,6 +126,7 @@ export type TModelsContent = {
exploreModels: string;
};
description: string;
+ updateDescription: string;
};
trainingSettings: {