From 4a65df4a7ba5e575a54635bd3f8ff891fb929225 Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Tue, 25 Feb 2025 14:31:50 +0100 Subject: [PATCH 1/5] fix: fixed predictions not showing at some zoom levels and invalid bbox --- frontend/src/app/routes/start-mapping.tsx | 64 +++++++------------ .../start-mapping/components/header.tsx | 21 +++--- .../components/mobile-drawer.tsx | 15 +++-- .../start-mapping/components/model-action.tsx | 52 +++++++++++++-- 4 files changed, 94 insertions(+), 58 deletions(-) diff --git a/frontend/src/app/routes/start-mapping.tsx b/frontend/src/app/routes/start-mapping.tsx index d02a2585..7c284f1c 100644 --- a/frontend/src/app/routes/start-mapping.tsx +++ b/frontend/src/app/routes/start-mapping.tsx @@ -4,7 +4,6 @@ import { START_MAPPING_PAGE_CONTENT, TOAST_NOTIFICATIONS, } from "@/constants"; -import { BASE_MODELS } from "@/enums"; import { FitToBounds, LayerControl, ZoomLevel } from "@/components/map"; import { Head } from "@/components/seo"; import { LngLatBoundsLike } from "maplibre-gl"; @@ -18,11 +17,10 @@ import { useModelDetails } from "@/features/models/hooks/use-models"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { UserProfile } from "@/components/layout"; import { - BBOX, Feature, TileJSON, TModelPredictions, - TModelPredictionsConfig, + TTrainingDataset, } from "@/types"; import { BrandLogoWithDropDown, @@ -38,7 +36,6 @@ import { showSuccessToast, } from "@/utils"; import { - PREDICTION_API_FILE_EXTENSIONS, REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID, REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION, @@ -70,22 +67,24 @@ export const StartMappingPage = () => { const { map, mapContainerRef, currentZoom } = useMapInstance(); const { isSmallViewport } = useScreenSize(); const navigate = useNavigate(); - const bounds = map?.getBounds(); + const [showModelDetailsPopup, setShowModelDetailsPopup] = useState(false); const { dropdownIsOpened, onDropdownHide, onDropdownShow } = useDropdownMenu(); - const { isError, isPending, data, error } = useModelDetails( - modelId as string, - !!modelId, - ); + const { + isError, + isPending, + data: modelInfo, + error, + } = useModelDetails(modelId as string, !!modelId); const { data: trainingDataset, isPending: trainingDatasetIsPending, isError: trainingDatasetIsError, - } = useGetTrainingDataset(data?.dataset as number, !isPending); + } = useGetTrainingDataset(modelInfo?.dataset as number, !isPending); const tileJSONURL = extractTileJSONURL(trainingDataset?.source_imagery ?? ""); @@ -119,6 +118,7 @@ export const StartMappingPage = () => { }; }); + // Todo - move to local storage const [modelPredictions, setModelPredictions] = useState({ all: [], accepted: [], @@ -149,25 +149,6 @@ export const StartMappingPage = () => { [searchParams, setSearchParams], ); - const trainingConfig: TModelPredictionsConfig = { - tolerance: query[SEARCH_PARAMS.tolerance] as number, - 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_${data?.dataset}/output/training_${data?.published_training}/checkpoint${PREDICTION_API_FILE_EXTENSIONS[data?.base_model as BASE_MODELS]}`, - max_angle_change: 15, - model_id: modelId as string, - skew_tolerance: 15, - source: trainingDataset?.source_imagery as string, - zoom_level: currentZoom, - bbox: [ - bounds?.getWest(), - bounds?.getSouth(), - bounds?.getEast(), - bounds?.getNorth(), - ] as BBOX, - }; - const disablePrediction = currentZoom < MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION; @@ -225,18 +206,18 @@ export const StartMappingPage = () => { ...modelPredictions.all, ], }, - `all_predictions_${data.dataset}`, + `all_predictions_${modelInfo.dataset}`, ); showSuccessToast(TOAST_NOTIFICATIONS.startMapping.fileDownloadSuccess); - }, [modelPredictions]); + }, [modelPredictions, modelInfo]); const handleAcceptedFeaturesDownload = useCallback(async () => { geoJSONDowloader( { type: "FeatureCollection", features: modelPredictions.accepted }, - `accepted_predictions_${data.dataset}`, + `accepted_predictions_${modelInfo.dataset}`, ); showSuccessToast(TOAST_NOTIFICATIONS.startMapping.fileDownloadSuccess); - }, [modelPredictions]); + }, [modelPredictions, modelInfo]); const handleFeaturesDownloadToJOSM = useCallback( (features: Feature[]) => { @@ -316,13 +297,12 @@ export const StartMappingPage = () => { return ( <> - + {/* Mobile dialog */}
{ updateQuery={updateQuery} modelDetailsPopupIsActive={showModelDetailsPopup} clearPredictions={clearPredictions} + trainingDataset={trainingDataset as TTrainingDataset} + currentZoom={currentZoom} + modelInfo={modelInfo} />
{/* Model Details Popup */} - {data && ( + {modelInfo && ( setShowModelDetailsPopup(false)} anchor={popupAnchorId} - model={data} + model={modelInfo} trainingDataset={trainingDataset} trainingDatasetIsPending={trainingDatasetIsPending} trainingDatasetIsError={trainingDatasetIsError} @@ -349,14 +332,13 @@ export const StartMappingPage = () => { )} {/* Web Header */} { handleModelDetailsPopup={handleModelDetailsPopup} downloadOptions={downloadOptions} clearPredictions={clearPredictions} + trainingDataset={trainingDataset as TTrainingDataset} + currentZoom={currentZoom} />
@@ -408,7 +392,7 @@ export const StartMappingPage = () => { currentZoom={currentZoom} layers={mapLayers} tmsBounds={oamTileJSON?.bounds as LngLatBoundsLike} - trainingId={data?.published_training} + trainingId={modelInfo?.published_training} />
diff --git a/frontend/src/features/start-mapping/components/header.tsx b/frontend/src/features/start-mapping/components/header.tsx index 9fc260f6..b6bd2273 100644 --- a/frontend/src/features/start-mapping/components/header.tsx +++ b/frontend/src/features/start-mapping/components/header.tsx @@ -11,21 +11,20 @@ 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, TModelPredictionsConfig } from "@/types"; +import { TModel, TModelPredictions, TTrainingDataset } from "@/types"; import { ToolTip } from "@/components/ui/tooltip"; import { useDropdownMenu } from "@/hooks/use-dropdown-menu"; import { UserProfile } from "@/components/layout"; import { START_MAPPING_PAGE_CONTENT } from "@/constants"; const StartMappingHeader = ({ - data, + modelInfo, modelPredictions, modelPredictionsExist, trainingDatasetIsPending, trainingDatasetIsError, query, updateQuery, - trainingConfig, setModelPredictions, disablePrediction, map, @@ -34,15 +33,16 @@ const StartMappingHeader = ({ modelDetailsPopupIsActive, downloadOptions, clearPredictions, + trainingDataset, + currentZoom, }: { modelPredictionsExist: boolean; trainingDatasetIsPending: boolean; trainingDatasetIsError: boolean; - data: TModel; + modelInfo: TModel; modelPredictions: TModelPredictions; query: TQueryParams; updateQuery: (newParams: TQueryParams) => void; - trainingConfig: TModelPredictionsConfig; setModelPredictions: React.Dispatch>; map: Map | null; disablePrediction: boolean; @@ -51,6 +51,8 @@ const StartMappingHeader = ({ modelDetailsPopupIsActive: boolean; downloadOptions: TDownloadOptions; clearPredictions: () => void; + trainingDataset: TTrainingDataset; + currentZoom: number; }) => { const { onDropdownHide, onDropdownShow, dropdownIsOpened } = useDropdownMenu(); @@ -75,10 +77,10 @@ const StartMappingHeader = ({ />

- {data?.name ?? "N/A"} + {modelInfo?.name ?? "N/A"}

diff --git a/frontend/src/features/start-mapping/components/mobile-drawer.tsx b/frontend/src/features/start-mapping/components/mobile-drawer.tsx index 556af391..ac9ea72e 100644 --- a/frontend/src/features/start-mapping/components/mobile-drawer.tsx +++ b/frontend/src/features/start-mapping/components/mobile-drawer.tsx @@ -7,7 +7,7 @@ import { ModelDetailsButton } from "@/features/start-mapping/components/model-de import { ModelPredictionsTracker } from "@/features/start-mapping/components/model-predictions-tracker"; import { ModelSettings } from "@/features/start-mapping/components/model-settings"; import { TDownloadOptions, TQueryParams } from "@/app/routes/start-mapping"; -import { TModelPredictions, TModelPredictionsConfig } from "@/types"; +import { TModel, TModelPredictions, TTrainingDataset } from "@/types"; import { ToolTip } from "@/components/ui/tooltip"; import { useState } from "react"; import { START_MAPPING_PAGE_CONTENT } from "@/constants"; @@ -15,7 +15,6 @@ import { START_MAPPING_PAGE_CONTENT } from "@/constants"; export const StartMappingMobileDrawer = ({ isOpen, disablePrediction, - trainingConfig, setModelPredictions, map, modelPredictions, @@ -25,10 +24,12 @@ export const StartMappingMobileDrawer = ({ updateQuery, modelDetailsPopupIsActive, clearPredictions, + trainingDataset, + currentZoom, + modelInfo, }: { isOpen: boolean; disablePrediction: boolean; - trainingConfig: TModelPredictionsConfig; modelPredictions: TModelPredictions; setModelPredictions: React.Dispatch>; map: Map | null; @@ -38,6 +39,9 @@ export const StartMappingMobileDrawer = ({ query: TQueryParams; updateQuery: (newParams: TQueryParams) => void; clearPredictions: () => void; + trainingDataset: TTrainingDataset; + currentZoom: number; + modelInfo: TModel; }) => { const [showDownloadOptions, setShowDownloadOptions] = useState(false); @@ -53,11 +57,14 @@ export const StartMappingMobileDrawer = ({
diff --git a/frontend/src/features/start-mapping/components/model-action.tsx b/frontend/src/features/start-mapping/components/model-action.tsx index 4edf3cb0..d7e8aa02 100644 --- a/frontend/src/features/start-mapping/components/model-action.tsx +++ b/frontend/src/features/start-mapping/components/model-action.tsx @@ -1,24 +1,64 @@ import { handleConflation, showErrorToast, showSuccessToast } from "@/utils"; import { Map } from "maplibre-gl"; import { START_MAPPING_PAGE_CONTENT, TOAST_NOTIFICATIONS } from "@/constants"; -import { TModelPredictions, TModelPredictionsConfig } from "@/types"; +import { + BBOX, + TModel, + TModelPredictions, + TModelPredictionsConfig, + TQueryParams, + TTrainingDataset, +} from "@/types"; import { ToolTip } from "@/components/ui/tooltip"; import { useCallback } from "react"; import { useGetModelPredictions } from "@/features/start-mapping/hooks/use-model-predictions"; +import { SEARCH_PARAMS } from "@/app/routes/start-mapping"; +import { PREDICTION_API_FILE_EXTENSIONS } from "@/config"; +import { BASE_MODELS } from "@/enums"; +import { useParams } from "react-router-dom"; const ModelAction = ({ setModelPredictions, modelPredictions, - trainingConfig, map, disablePrediction, + query, + trainingDataset, + currentZoom, + modelInfo, }: { - trainingConfig: TModelPredictionsConfig; modelPredictions: TModelPredictions; setModelPredictions: React.Dispatch>; map: Map | null; disablePrediction: boolean; + query: TQueryParams; + trainingDataset: TTrainingDataset; + currentZoom: number; + modelInfo: TModel; }) => { + const { modelId } = useParams(); + + const getTrainingConfig = useCallback((): TModelPredictionsConfig => { + return { + tolerance: query[SEARCH_PARAMS.tolerance] as number, + 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]}`, + max_angle_change: 15, + model_id: modelId as string, + skew_tolerance: 15, + source: trainingDataset?.source_imagery as string, + zoom_level: currentZoom, + bbox: [ + map?.getBounds().getWest(), + map?.getBounds().getSouth(), + map?.getBounds().getEast(), + map?.getBounds().getNorth(), + ] as BBOX, + }; + }, [map, query, currentZoom]); + const modelPredictionMutation = useGetModelPredictions({ mutationConfig: { onSuccess: (data) => { @@ -28,7 +68,7 @@ const ModelAction = ({ const conflatedResults = handleConflation( modelPredictions, data.features, - trainingConfig, + getTrainingConfig(), ); setModelPredictions(conflatedResults); }, @@ -38,8 +78,8 @@ const ModelAction = ({ const handlePrediction = useCallback(async () => { if (!map) return; - await modelPredictionMutation.mutateAsync(trainingConfig); - }, [trainingConfig]); + await modelPredictionMutation.mutateAsync(getTrainingConfig()); + }, [getTrainingConfig, modelPredictionMutation, map]); return (
From 62d7e6e521597b3074fc0fa760799b803dc3037c Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Tue, 25 Feb 2025 15:18:56 +0100 Subject: [PATCH 2/5] chore: optimized conflation + tests --- .../start-mapping/components/model-action.tsx | 2 +- .../__tests__/geo/geometry-utils.test.ts | 323 ++++++++++++++++++ frontend/src/utils/geo/geometry-utils.ts | 69 +++- 3 files changed, 376 insertions(+), 18 deletions(-) create mode 100644 frontend/src/utils/__tests__/geo/geometry-utils.test.ts diff --git a/frontend/src/features/start-mapping/components/model-action.tsx b/frontend/src/features/start-mapping/components/model-action.tsx index d7e8aa02..807ab78d 100644 --- a/frontend/src/features/start-mapping/components/model-action.tsx +++ b/frontend/src/features/start-mapping/components/model-action.tsx @@ -57,7 +57,7 @@ const ModelAction = ({ map?.getBounds().getNorth(), ] as BBOX, }; - }, [map, query, currentZoom]); + }, [map, query, currentZoom, trainingDataset, modelInfo]); const modelPredictionMutation = useGetModelPredictions({ mutationConfig: { diff --git a/frontend/src/utils/__tests__/geo/geometry-utils.test.ts b/frontend/src/utils/__tests__/geo/geometry-utils.test.ts new file mode 100644 index 00000000..c75cf6f4 --- /dev/null +++ b/frontend/src/utils/__tests__/geo/geometry-utils.test.ts @@ -0,0 +1,323 @@ +import { Feature } from "geojson"; +import { describe, expect, it } from "vitest"; + +import { TModelPredictions, TModelPredictionsConfig } from "@/types"; +import { + calculateGeoJSONArea, + formatAreaInAppropriateUnit, + getGeoJSONFeatureBounds, + handleConflation, +} from "@/utils"; + +const predictionConfig: TModelPredictionsConfig = { + area_threshold: 6, + bbox: [0, 0, 0, 0], + checkpoint: "", + confidence: 95, + max_angle_change: 0, + model_id: "", + use_josm_q: true, + skew_tolerance: 0, + zoom_level: 21, + source: "", + tolerance: 0, +}; + +describe("geometry-utils", () => { + it("should calculate the area of a GeoJSON Feature", () => { + const feature: Feature = { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [-10, -10], + [10, -10], + [10, 10], + [-10, 10], + [-10, -10], + ], + ], + }, + properties: {}, + }; + const result = calculateGeoJSONArea(feature); + expect(result).toBeGreaterThan(0); + }); + + it("should format area into human readable string", () => { + const result = formatAreaInAppropriateUnit(12222000); + expect(result).toBe("12.2km²"); + }); + + it("should compute the bounding box of a GeoJSON Feature", () => { + const feature: Feature = { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [-10, -10], + [10, -10], + [10, 10], + [-10, 10], + [-10, -10], + ], + ], + }, + properties: {}, + }; + const result = getGeoJSONFeatureBounds(feature); + expect(result).toEqual([-10, -10, 10, 10]); + }); + + it("should handle conflation of new features with existing predictions", () => { + const existingPredictions = { + all: [], + accepted: [], + rejected: [], + }; + const newFeatures: Feature[] = [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [10, 10], + [20, 20], + [30, 30], + [10, 10], + ], + ], + }, + properties: {}, + }, + ]; + + const result = handleConflation( + existingPredictions, + newFeatures, + predictionConfig + ); + expect(result.all.length).toBe(1); + }); + + it("should replace existing feature if it intersects with new feature", () => { + const existingPredictions = { + all: [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [15, 15], + [25, 25], + [35, 35], + [15, 15], + ], + ], + }, + properties: { config: predictionConfig, _id: "existing" }, + }, + ], + accepted: [], + rejected: [], + } as TModelPredictions; + const newFeatures: Feature[] = [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [20, 30], + [25, 25], + [35, 35], + [20, 30], + ], + ], + }, + properties: {}, + }, + ]; + + const result = handleConflation( + existingPredictions, + newFeatures, + predictionConfig + ); + expect(result.all.length).toBe(1); + expect(result.all[0].properties?._id).not.toBe("existing"); + }); + + it("should replace the correct feature if multiple features intersect", () => { + const existingPredictions = { + all: [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [5, 5], + [15, 5], + [15, 15], + [5, 15], + [5, 5], + ], + ], + }, + properties: { _id: "existing1", config: predictionConfig }, + }, + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [20, 20], + [30, 20], + [30, 30], + [20, 30], + [20, 20], + ], + ], + }, + properties: { _id: "existing2", config: predictionConfig }, + }, + ], + accepted: [], + rejected: [], + } as TModelPredictions; + const newFeatures: Feature[] = [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [20, 20], + [30, 20], + [30, 30], + [20, 30], + [20, 20], + ], + ], + }, + properties: {}, + }, + ]; + + const result = handleConflation( + existingPredictions, + newFeatures, + predictionConfig + ); + expect(result.all.length).toBe(2); + expect(result.all[1].properties?._id).not.toBe("existing2"); + expect(result.all[0].properties?._id).toBe("existing1"); + }); + + it("should not add new feature if it intersects with accepted feature", () => { + const existingPredictions = { + all: [], + accepted: [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [10, 10], + [20, 20], + [30, 30], + [10, 10], + ], + ], + }, + properties: { config: predictionConfig }, + }, + ], + rejected: [], + } as TModelPredictions; + const newFeatures: Feature[] = [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [15, 15], + [25, 25], + [35, 35], + [15, 15], + ], + ], + }, + properties: {}, + }, + ]; + + const result = handleConflation( + existingPredictions, + newFeatures, + predictionConfig + ); + expect(result.all.length).toBe(0); + expect(result.accepted.length).toBe(1); + expect(result.rejected.length).toBe(0); + }); + + it("should not add new feature if it intersects with rejected feature", () => { + const existingPredictions = { + all: [], + accepted: [], + rejected: [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [10, 10], + [20, 20], + [30, 30], + [10, 10], + ], + ], + }, + properties: { config: predictionConfig }, + }, + ], + } as TModelPredictions; + + const newFeatures: Feature[] = [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [15, 15], + [25, 25], + [35, 35], + [15, 15], + ], + ], + }, + properties: {}, + }, + ]; + + const result = handleConflation( + existingPredictions, + newFeatures, + predictionConfig + ); + expect(result.all.length).toBe(0); + expect(result.rejected.length).toBe(1); + expect(result.accepted.length).toBe(0); + }); +}); \ No newline at end of file diff --git a/frontend/src/utils/geo/geometry-utils.ts b/frontend/src/utils/geo/geometry-utils.ts index 03134cd4..78988b64 100644 --- a/frontend/src/utils/geo/geometry-utils.ts +++ b/frontend/src/utils/geo/geometry-utils.ts @@ -312,12 +312,32 @@ export const snapGeoJSONPolygonToClosestTile = (geometry: Polygon) => { return geometry; }; -/* - Logic. - | 1 - Purple: Will be replaced with new feature if it intersects with new features, otherwise, it'll be appended. - | 2 - Red: No touch - | 3 - Green: No touch -*/ +/** + * Conflates new features with existing predictions. + * + * Existing Predictions: + * accepted: [A1, A2, A3] + * rejected: [R1, R2] + * all: [E1, E2, E3, E4] + * + * New Features: + * newFeatures: [N1, N2, N3] + * + * Logic: + * 1. If N1 intersects with any feature in 'all', replace the intersecting feature in 'all' with N1. + * 2. If N2 does not intersect with any feature in 'accepted' or 'rejected', append N2 to 'all'. + * 3. If N3 intersects with any feature in 'accepted' or 'rejected', do not add N3 to 'all'. + * + * Example: + * - N1 intersects with E2 -> Replace E2 with N1 in 'all'. + * - N2 does not intersect with any in 'accepted' or 'rejected' -> Append N2 to 'all'. + * - N3 intersects with A2 -> Do not add N3 to 'all'. + * + * Result: + * all: [E1, N1, E3, E4, N2] + * accepted: [A1, A2, A3] + * rejected: [R1, R2] + */ export const handleConflation = ( existingPredictions: TModelPredictions, newFeatures: Feature[], @@ -325,16 +345,29 @@ export const handleConflation = ( ): TModelPredictions => { let updatedAll = [...existingPredictions.all]; - newFeatures.forEach((newFeature) => { - const intersectsWithAccepted = existingPredictions.accepted.some( - (acceptedFeature) => booleanIntersects(newFeature, acceptedFeature), - ); - const intersectsWithRejected = existingPredictions.rejected.some( - (rejectedFeature) => booleanIntersects(newFeature, rejectedFeature), - ); + for (const newFeature of newFeatures) { + let intersectsAccepted = false; + let intersectsRejected = false; - const intersectingIndex = updatedAll.findIndex((existingFeature) => - booleanIntersects(newFeature, existingFeature), + // Check for intersections in accepted features with early exit. + for (const acceptedFeature of existingPredictions.accepted) { + if (booleanIntersects(newFeature, acceptedFeature)) { + intersectsAccepted = true; + break; + } + } + + // Check for intersections in rejected features with early exit. + for (const rejectedFeature of existingPredictions.rejected) { + if (booleanIntersects(newFeature, rejectedFeature)) { + intersectsRejected = true; + break; + } + } + + // Check if the new feature intersects with any feature in updatedAll. + const intersectingIndex = updatedAll.findIndex(existingFeature => + booleanIntersects(newFeature, existingFeature) ); if (intersectingIndex !== -1) { @@ -342,11 +375,12 @@ export const handleConflation = ( ...newFeature, properties: { ...newFeature.properties, + // Reuse existing id if available, or generate a new one. id: updatedAll[intersectingIndex].properties?.id || uuid4(), config: predictionConfig, }, }; - } else if (!intersectsWithAccepted && !intersectsWithRejected) { + } else if (!intersectsAccepted && !intersectsRejected) { updatedAll.push({ ...newFeature, properties: { @@ -356,7 +390,8 @@ export const handleConflation = ( }, }); } - }); + + } return { all: updatedAll, From c0c0df098cf044a3769bf8a83279c69aefe89820 Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Tue, 25 Feb 2025 15:19:11 +0100 Subject: [PATCH 3/5] chore: formating --- .../__tests__/geo/geometry-utils.test.ts | 590 +++++++++--------- frontend/src/utils/geo/geometry-utils.ts | 5 +- 2 files changed, 297 insertions(+), 298 deletions(-) diff --git a/frontend/src/utils/__tests__/geo/geometry-utils.test.ts b/frontend/src/utils/__tests__/geo/geometry-utils.test.ts index c75cf6f4..4f56f286 100644 --- a/frontend/src/utils/__tests__/geo/geometry-utils.test.ts +++ b/frontend/src/utils/__tests__/geo/geometry-utils.test.ts @@ -3,321 +3,321 @@ import { describe, expect, it } from "vitest"; import { TModelPredictions, TModelPredictionsConfig } from "@/types"; import { - calculateGeoJSONArea, - formatAreaInAppropriateUnit, - getGeoJSONFeatureBounds, - handleConflation, + calculateGeoJSONArea, + formatAreaInAppropriateUnit, + getGeoJSONFeatureBounds, + handleConflation, } from "@/utils"; const predictionConfig: TModelPredictionsConfig = { - area_threshold: 6, - bbox: [0, 0, 0, 0], - checkpoint: "", - confidence: 95, - max_angle_change: 0, - model_id: "", - use_josm_q: true, - skew_tolerance: 0, - zoom_level: 21, - source: "", - tolerance: 0, + area_threshold: 6, + bbox: [0, 0, 0, 0], + checkpoint: "", + confidence: 95, + max_angle_change: 0, + model_id: "", + use_josm_q: true, + skew_tolerance: 0, + zoom_level: 21, + source: "", + tolerance: 0, }; describe("geometry-utils", () => { - it("should calculate the area of a GeoJSON Feature", () => { - const feature: Feature = { - type: "Feature", - geometry: { - type: "Polygon", - coordinates: [ - [ - [-10, -10], - [10, -10], - [10, 10], - [-10, 10], - [-10, -10], - ], - ], - }, - properties: {}, - }; - const result = calculateGeoJSONArea(feature); - expect(result).toBeGreaterThan(0); - }); + it("should calculate the area of a GeoJSON Feature", () => { + const feature: Feature = { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [-10, -10], + [10, -10], + [10, 10], + [-10, 10], + [-10, -10], + ], + ], + }, + properties: {}, + }; + const result = calculateGeoJSONArea(feature); + expect(result).toBeGreaterThan(0); + }); - it("should format area into human readable string", () => { - const result = formatAreaInAppropriateUnit(12222000); - expect(result).toBe("12.2km²"); - }); + it("should format area into human readable string", () => { + const result = formatAreaInAppropriateUnit(12222000); + expect(result).toBe("12.2km²"); + }); - it("should compute the bounding box of a GeoJSON Feature", () => { - const feature: Feature = { - type: "Feature", - geometry: { - type: "Polygon", - coordinates: [ - [ - [-10, -10], - [10, -10], - [10, 10], - [-10, 10], - [-10, -10], - ], - ], - }, - properties: {}, - }; - const result = getGeoJSONFeatureBounds(feature); - expect(result).toEqual([-10, -10, 10, 10]); - }); + it("should compute the bounding box of a GeoJSON Feature", () => { + const feature: Feature = { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [-10, -10], + [10, -10], + [10, 10], + [-10, 10], + [-10, -10], + ], + ], + }, + properties: {}, + }; + const result = getGeoJSONFeatureBounds(feature); + expect(result).toEqual([-10, -10, 10, 10]); + }); - it("should handle conflation of new features with existing predictions", () => { - const existingPredictions = { - all: [], - accepted: [], - rejected: [], - }; - const newFeatures: Feature[] = [ - { - type: "Feature", - geometry: { - type: "Polygon", - coordinates: [ - [ - [10, 10], - [20, 20], - [30, 30], - [10, 10], - ], - ], - }, - properties: {}, - }, - ]; + it("should handle conflation of new features with existing predictions", () => { + const existingPredictions = { + all: [], + accepted: [], + rejected: [], + }; + const newFeatures: Feature[] = [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [10, 10], + [20, 20], + [30, 30], + [10, 10], + ], + ], + }, + properties: {}, + }, + ]; - const result = handleConflation( - existingPredictions, - newFeatures, - predictionConfig - ); - expect(result.all.length).toBe(1); - }); + const result = handleConflation( + existingPredictions, + newFeatures, + predictionConfig, + ); + expect(result.all.length).toBe(1); + }); - it("should replace existing feature if it intersects with new feature", () => { - const existingPredictions = { - all: [ - { - type: "Feature", - geometry: { - type: "Polygon", - coordinates: [ - [ - [15, 15], - [25, 25], - [35, 35], - [15, 15], - ], - ], - }, - properties: { config: predictionConfig, _id: "existing" }, - }, + it("should replace existing feature if it intersects with new feature", () => { + const existingPredictions = { + all: [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [15, 15], + [25, 25], + [35, 35], + [15, 15], + ], + ], + }, + properties: { config: predictionConfig, _id: "existing" }, + }, + ], + accepted: [], + rejected: [], + } as TModelPredictions; + const newFeatures: Feature[] = [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [20, 30], + [25, 25], + [35, 35], + [20, 30], ], - accepted: [], - rejected: [], - } as TModelPredictions; - const newFeatures: Feature[] = [ - { - type: "Feature", - geometry: { - type: "Polygon", - coordinates: [ - [ - [20, 30], - [25, 25], - [35, 35], - [20, 30], - ], - ], - }, - properties: {}, - }, - ]; + ], + }, + properties: {}, + }, + ]; - const result = handleConflation( - existingPredictions, - newFeatures, - predictionConfig - ); - expect(result.all.length).toBe(1); - expect(result.all[0].properties?._id).not.toBe("existing"); - }); + const result = handleConflation( + existingPredictions, + newFeatures, + predictionConfig, + ); + expect(result.all.length).toBe(1); + expect(result.all[0].properties?._id).not.toBe("existing"); + }); - it("should replace the correct feature if multiple features intersect", () => { - const existingPredictions = { - all: [ - { - type: "Feature", - geometry: { - type: "Polygon", - coordinates: [ - [ - [5, 5], - [15, 5], - [15, 15], - [5, 15], - [5, 5], - ], - ], - }, - properties: { _id: "existing1", config: predictionConfig }, - }, - { - type: "Feature", - geometry: { - type: "Polygon", - coordinates: [ - [ - [20, 20], - [30, 20], - [30, 30], - [20, 30], - [20, 20], - ], - ], - }, - properties: { _id: "existing2", config: predictionConfig }, - }, + it("should replace the correct feature if multiple features intersect", () => { + const existingPredictions = { + all: [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [5, 5], + [15, 5], + [15, 15], + [5, 15], + [5, 5], + ], + ], + }, + properties: { _id: "existing1", config: predictionConfig }, + }, + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [20, 20], + [30, 20], + [30, 30], + [20, 30], + [20, 20], + ], ], - accepted: [], - rejected: [], - } as TModelPredictions; - const newFeatures: Feature[] = [ - { - type: "Feature", - geometry: { - type: "Polygon", - coordinates: [ - [ - [20, 20], - [30, 20], - [30, 30], - [20, 30], - [20, 20], - ], - ], - }, - properties: {}, - }, - ]; + }, + properties: { _id: "existing2", config: predictionConfig }, + }, + ], + accepted: [], + rejected: [], + } as TModelPredictions; + const newFeatures: Feature[] = [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [20, 20], + [30, 20], + [30, 30], + [20, 30], + [20, 20], + ], + ], + }, + properties: {}, + }, + ]; - const result = handleConflation( - existingPredictions, - newFeatures, - predictionConfig - ); - expect(result.all.length).toBe(2); - expect(result.all[1].properties?._id).not.toBe("existing2"); - expect(result.all[0].properties?._id).toBe("existing1"); - }); + const result = handleConflation( + existingPredictions, + newFeatures, + predictionConfig, + ); + expect(result.all.length).toBe(2); + expect(result.all[1].properties?._id).not.toBe("existing2"); + expect(result.all[0].properties?._id).toBe("existing1"); + }); - it("should not add new feature if it intersects with accepted feature", () => { - const existingPredictions = { - all: [], - accepted: [ - { - type: "Feature", - geometry: { - type: "Polygon", - coordinates: [ - [ - [10, 10], - [20, 20], - [30, 30], - [10, 10], - ], - ], - }, - properties: { config: predictionConfig }, - }, + it("should not add new feature if it intersects with accepted feature", () => { + const existingPredictions = { + all: [], + accepted: [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [10, 10], + [20, 20], + [30, 30], + [10, 10], + ], + ], + }, + properties: { config: predictionConfig }, + }, + ], + rejected: [], + } as TModelPredictions; + const newFeatures: Feature[] = [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [15, 15], + [25, 25], + [35, 35], + [15, 15], ], - rejected: [], - } as TModelPredictions; - const newFeatures: Feature[] = [ - { - type: "Feature", - geometry: { - type: "Polygon", - coordinates: [ - [ - [15, 15], - [25, 25], - [35, 35], - [15, 15], - ], - ], - }, - properties: {}, - }, - ]; + ], + }, + properties: {}, + }, + ]; - const result = handleConflation( - existingPredictions, - newFeatures, - predictionConfig - ); - expect(result.all.length).toBe(0); - expect(result.accepted.length).toBe(1); - expect(result.rejected.length).toBe(0); - }); + const result = handleConflation( + existingPredictions, + newFeatures, + predictionConfig, + ); + expect(result.all.length).toBe(0); + expect(result.accepted.length).toBe(1); + expect(result.rejected.length).toBe(0); + }); - it("should not add new feature if it intersects with rejected feature", () => { - const existingPredictions = { - all: [], - accepted: [], - rejected: [ - { - type: "Feature", - geometry: { - type: "Polygon", - coordinates: [ - [ - [10, 10], - [20, 20], - [30, 30], - [10, 10], - ], - ], - }, - properties: { config: predictionConfig }, - }, + it("should not add new feature if it intersects with rejected feature", () => { + const existingPredictions = { + all: [], + accepted: [], + rejected: [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [10, 10], + [20, 20], + [30, 30], + [10, 10], + ], ], - } as TModelPredictions; + }, + properties: { config: predictionConfig }, + }, + ], + } as TModelPredictions; - const newFeatures: Feature[] = [ - { - type: "Feature", - geometry: { - type: "Polygon", - coordinates: [ - [ - [15, 15], - [25, 25], - [35, 35], - [15, 15], - ], - ], - }, - properties: {}, - }, - ]; + const newFeatures: Feature[] = [ + { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [15, 15], + [25, 25], + [35, 35], + [15, 15], + ], + ], + }, + properties: {}, + }, + ]; - const result = handleConflation( - existingPredictions, - newFeatures, - predictionConfig - ); - expect(result.all.length).toBe(0); - expect(result.rejected.length).toBe(1); - expect(result.accepted.length).toBe(0); - }); -}); \ No newline at end of file + const result = handleConflation( + existingPredictions, + newFeatures, + predictionConfig, + ); + expect(result.all.length).toBe(0); + expect(result.rejected.length).toBe(1); + expect(result.accepted.length).toBe(0); + }); +}); diff --git a/frontend/src/utils/geo/geometry-utils.ts b/frontend/src/utils/geo/geometry-utils.ts index 78988b64..44d2da51 100644 --- a/frontend/src/utils/geo/geometry-utils.ts +++ b/frontend/src/utils/geo/geometry-utils.ts @@ -366,8 +366,8 @@ export const handleConflation = ( } // Check if the new feature intersects with any feature in updatedAll. - const intersectingIndex = updatedAll.findIndex(existingFeature => - booleanIntersects(newFeature, existingFeature) + const intersectingIndex = updatedAll.findIndex((existingFeature) => + booleanIntersects(newFeature, existingFeature), ); if (intersectingIndex !== -1) { @@ -390,7 +390,6 @@ export const handleConflation = ( }, }); } - } return { From 2e5cc115628d9e9efef9198b59e6f443781f761e Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Tue, 25 Feb 2025 15:26:02 +0100 Subject: [PATCH 4/5] chore: synced with upstream --- frontend/src/layouts/root-layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/layouts/root-layout.tsx b/frontend/src/layouts/root-layout.tsx index c3b65b5d..75c19e96 100644 --- a/frontend/src/layouts/root-layout.tsx +++ b/frontend/src/layouts/root-layout.tsx @@ -39,7 +39,7 @@ export const RootLayout = () => { >
- {!pathname.includes(APPLICATION_ROUTES.AUTH_CALLBACK) && + {!pathname.includes(APPLICATION_ROUTES.START_MAPPING_BASE) && !pathname.includes(APPLICATION_ROUTES.AUTH_CALLBACK) &&