From f857b09fa1f9282a4051f70a30f54eefee100f4a Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 11 Nov 2024 15:39:37 -0600 Subject: [PATCH] Reset Map (#126) --- .../app/components/sidebar/ResetMapButton.tsx | 11 ++--- app/src/app/store/mapRenderSubs.ts | 8 +++ app/src/app/store/mapStore.ts | 36 +++++++++++++- app/src/app/utils/api/apiHandlers.ts | 28 +++++++++++ app/src/app/utils/api/mutations.ts | 19 +++++++ app/src/app/utils/helpers.ts | 49 +++++++++++++++++++ backend/app/main.py | 20 ++++++++ backend/tests/test_main.py | 9 ++++ 8 files changed, 171 insertions(+), 9 deletions(-) diff --git a/app/src/app/components/sidebar/ResetMapButton.tsx b/app/src/app/components/sidebar/ResetMapButton.tsx index 06f8af11b..14a811fe3 100644 --- a/app/src/app/components/sidebar/ResetMapButton.tsx +++ b/app/src/app/components/sidebar/ResetMapButton.tsx @@ -2,16 +2,11 @@ import {useMapStore} from '@/app/store/mapStore'; import {Button} from '@radix-ui/themes'; export function ResetMapButton() { - const mapStore = useMapStore.getState(); - - const handleClickResetMap = () => { - mapStore.setFreshMap(true); - // clear map metrics - mapStore.setMapMetrics(null); - }; + const handleClickResetMap = useMapStore(state => state.handleReset) + const noZonesAreAssigned = useMapStore(state => !state.zoneAssignments.size) return ( - ); diff --git a/app/src/app/store/mapRenderSubs.ts b/app/src/app/store/mapRenderSubs.ts index 9b906c349..73e80d585 100644 --- a/app/src/app/store/mapRenderSubs.ts +++ b/app/src/app/store/mapRenderSubs.ts @@ -5,6 +5,7 @@ import { PARENT_LAYERS, CHILD_LAYERS, getLayerFilter, + BLOCK_SOURCE_ID, } from '@constants/layers'; import { ColorZoneAssignmentsState, @@ -50,6 +51,13 @@ export const getRenderSubscriptions = (useMapStore: typeof _useMapStore) => { layersToFilter.forEach(layerId => mapRef.setFilter(layerId, getLayerFilter(layerId, shatterIds)) ); + shatterIds.parents.forEach((id) => { + mapRef?.removeFeatureState({ + source: BLOCK_SOURCE_ID, + id, + sourceLayer: state.mapDocument?.parent_layer, + }); + }); mapRef.once('render', () => { setMapLock(false); diff --git a/app/src/app/store/mapStore.ts b/app/src/app/store/mapStore.ts index 5bfd82129..748372ece 100644 --- a/app/src/app/store/mapStore.ts +++ b/app/src/app/store/mapStore.ts @@ -23,10 +23,11 @@ import { LayerVisibility, PaintEventHandler, getFeaturesInBbox, + resetZoneColors, setZones, } from "../utils/helpers"; import { getRenderSubscriptions } from "./mapRenderSubs"; -import { patchShatter } from "../utils/api/mutations"; +import { patchReset, patchShatter } from "../utils/api/mutations"; import { getSearchParamsObersver } from "../utils/api/queryParamsListener"; import { getMapMetricsSubs } from "./metricsSubs"; import { getMapEditSubs } from "./mapEditSubs"; @@ -80,6 +81,7 @@ export interface MapStore { resetZoneAssignments: () => void; zonePopulations: Map; setZonePopulations: (zone: Zone, population: number) => void; + handleReset: () => void; accumulatedGeoids: Set; setAccumulatedGeoids: (geoids: MapStore["accumulatedGeoids"]) => void; brushSize: number; @@ -232,6 +234,38 @@ export const useMapStore = create( zoneAssignments, }); }, + handleReset: async () => { + const {mapDocument, getMapRef, zoneAssignments, shatterIds} = get(); + const document_id = mapDocument?.document_id + + if (!document_id) { + console.log("No document ID to reset."); + return; + } + set({ + mapLock: true, + appLoadingState: "loading", + }); + const resetResponse = await patchReset.mutate(document_id); + + if (resetResponse.document_id === document_id) { + const initialState = useMapStore.getInitialState(); + resetZoneColors({ + zoneAssignments, + mapRef: getMapRef(), + mapDocument, + shatterIds + }) + + set({ + zonePopulations: initialState.zonePopulations, + zoneAssignments: initialState.zoneAssignments, + shatterIds: initialState.shatterIds, + appLoadingState: "loaded", + mapLock: false, + }); + } + }, setShatterIds: ( existingParents, existingChildren, diff --git a/app/src/app/utils/api/apiHandlers.ts b/app/src/app/utils/api/apiHandlers.ts index 377251532..bc4be27ac 100644 --- a/app/src/app/utils/api/apiHandlers.ts +++ b/app/src/app/utils/api/apiHandlers.ts @@ -189,6 +189,17 @@ export interface AssignmentsCreate { assignments_upserted: number; } +/** + * Reset assignments response + * @interface +* @property {boolean} success - Confirming if the operation succeeded + * @property {string} document_id - Document ID where assignments were dropped + */ +export interface AssignmentsReset { + success: boolean; + document_id: string; +} + /** * * @param assignments @@ -206,6 +217,23 @@ export const patchUpdateAssignments: ( }); }; +/** + * + * @param assignments + * @returns server object containing the updated assignments per geoid + */ +export const patchUpdateReset: ( + document_id: string, +) => Promise = async (document_id) => { + return await axios + .patch(`${process.env.NEXT_PUBLIC_API_URL}/api/update_assignments/${document_id}/reset`, { + document_id + }) + .then((res) => { + return res.data; + }); +}; + /** * Shatter result * @interface diff --git a/app/src/app/utils/api/mutations.ts b/app/src/app/utils/api/mutations.ts index 6bdc1d580..4f29e5d58 100644 --- a/app/src/app/utils/api/mutations.ts +++ b/app/src/app/utils/api/mutations.ts @@ -2,9 +2,11 @@ import {MutationObserver} from '@tanstack/query-core'; import {queryClient} from './queryClient'; import { AssignmentsCreate, + AssignmentsReset, createMapDocument, patchShatterParents, patchUpdateAssignments, + patchUpdateReset, } from '@/app/utils/api/apiHandlers'; import {useMapStore} from '@/app/store/mapStore'; import {mapMetrics} from './queries'; @@ -43,6 +45,23 @@ export const patchUpdates = new MutationObserver(queryClient, { }, }); + +export const patchReset = new MutationObserver(queryClient, { + mutationFn: patchUpdateReset, + onMutate: () => { + console.log("Reseting map"); + }, + onError: (error) => { + console.log("Error reseting map: ", error); + }, + onSuccess: (data: AssignmentsReset) => { + console.log( + `Successfully reset ${data.document_id}` + ); + mapMetrics.refetch(); + }, +}); + export const document = new MutationObserver(queryClient, { mutationFn: createMapDocument, onMutate: () => { diff --git a/app/src/app/utils/helpers.ts b/app/src/app/utils/helpers.ts index 8953242a4..48a6c5c2e 100644 --- a/app/src/app/utils/helpers.ts +++ b/app/src/app/utils/helpers.ts @@ -322,6 +322,55 @@ export const colorZoneAssignments = ( }); }; +/** + * resetZoneColors + * Resets the zone colors for the specified feature IDs on the map. + * + * This function sets the feature state for each ID in the provided set or array to indicate that + * the zone color should be reset. It checks if the map document is available and determines + * the appropriate source layer based on the existence of child layers and shatter IDs. + * + * @param {Set | string[]} ids - A set or array of feature IDs for which to reset the zone colors. + * @param {ReturnType} mapRef - The maplibre map instance used to set the feature state. + * @param {MapStore['mapDocument']} mapDocument - The map document containing layer information. + * @param {MapStore['shatterIds']} shatterIds - The shatter IDs used to determine layer types. + */ +export const resetZoneColors = ({ + ids, zoneAssignments, mapRef, mapDocument, shatterIds +}: { + ids?: Set | string[], + zoneAssignments?: MapStore['zoneAssignments'] + mapRef: ReturnType, + mapDocument: MapStore['mapDocument'], + shatterIds: MapStore['shatterIds'] +}) => { + const idsToReset = ids ? Array.from(ids) : zoneAssignments ? Array.from(zoneAssignments.keys()) : null + if (!mapDocument || !mapRef || !idsToReset) return + const childLayerExists = mapDocument?.child_layer + const shatterIdsExist = shatterIds.parents.size + const getSourceLayer = childLayerExists && shatterIdsExist + ? (id: string) => { + return shatterIds.children.has(id) + ? mapDocument.child_layer! + : mapDocument.parent_layer + } + : (_: string) => mapDocument.parent_layer + idsToReset.forEach(id => { + const sourceLayer = getSourceLayer(id) + mapRef?.setFeatureState( + { + source: BLOCK_SOURCE_ID, + id, + sourceLayer, + }, + { + selected: true, + zone: null, + } + ); + }) +} + // property changes on which to re-color assignments export const colorZoneAssignmentTriggers = [ 'zoneAssignments', diff --git a/backend/app/main.py b/backend/app/main.py index 2b4657a5b..4631839b7 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -174,6 +174,26 @@ async def shatter_parent( return result +@app.patch( + "/api/update_assignments/{document_id}/reset", status_code=status.HTTP_200_OK +) +async def reset_map(document_id: str, session: Session = Depends(get_session)): + # Drop the partition for the given assignments + partition_name = f'"document.assignments_{document_id}"' + session.execute(text(f"DROP TABLE IF EXISTS {partition_name} CASCADE;")) + + # Recreate the partition + session.execute( + text(f""" + CREATE TABLE {partition_name} PARTITION OF document.assignments + FOR VALUES IN ('{document_id}'); + """) + ) + session.commit() + + return {"message": "Assignments partition reset", "document_id": document_id} + + # called by getAssignments in apiHandlers.ts @app.get("/api/get_assignments/{document_id}", response_model=list[AssignmentsResponse]) async def get_assignments(document_id: str, session: Session = Depends(get_session)): diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py index 8ada690af..ae2622cfc 100644 --- a/backend/tests/test_main.py +++ b/backend/tests/test_main.py @@ -374,6 +374,15 @@ def test_patch_assignments_twice(client, document_id): assert data[1]["geo_id"] == "202090434001003" +def test_patch_reset_assignments(client, document_id): + test_patch_assignments(client, document_id) + response = client.patch(f"/api/update_assignments/{document_id}/reset") + assert response.status_code == 200 + assignments = client.get(f"/api/get_assignments/{document_id}") + assert assignments.status_code == 200 + assert len(assignments.json()) == 0 + + def test_get_document_population_totals_null_assignments( client, document_id, ks_demo_view_census_blocks ):