Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PSP-7158 Allow moving the position of map markers (within the file) #4233

Merged
merged 15 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,448 changes: 2,398 additions & 50 deletions source/frontend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions source/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"@bcgov/design-tokens": "3.0.0-rc1",
"@react-keycloak/web": "3.4.0",
"@reduxjs/toolkit": "1.8.6",
"@turf/turf": "7.0.0",
"@types/polylabel": "1.0.5",
"@xstate/react": "3.2.2",
"axios": "1.6.7",
Expand Down
5 changes: 5 additions & 0 deletions source/frontend/src/assets/images/pins/icon-relocate.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface IMapStateMachineContext {
mapLocationSelected: LatLngLiteral | null;
mapLocationFeatureDataset: LocationFeatureDataset | null;
selectedFeatureDataset: LocationFeatureDataset | null;
repositioningFeatureDataset: LocationFeatureDataset | null;
repositioningPropertyIndex: number | null;
showPopup: boolean;
isLoading: boolean;
mapSearchCriteria: IPropertyFilter | null;
Expand All @@ -35,6 +37,7 @@ export interface IMapStateMachineContext {
pendingFitBounds: boolean;
requestedFitBounds: LatLngBounds;
isSelecting: boolean;
isRepositioning: boolean;
selectingComponentId: string | null;
isFiltering: boolean;
isShowingMapLayers: boolean;
Expand All @@ -60,6 +63,12 @@ export interface IMapStateMachineContext {
prepareForCreation: () => void;
startSelection: (selectingComponentId?: string) => void;
finishSelection: () => void;
startReposition: (
repositioningFeatureDataset: LocationFeatureDataset,
index: number,
selectingComponentId?: string,
) => void;
finishReposition: () => void;
toggleMapFilter: () => void;
toggleMapLayer: () => void;
setFilePropertyLocations: (locations: LatLngLiteral[]) => void;
Expand Down Expand Up @@ -255,6 +264,26 @@ export const MapStateMachineProvider: React.FC<React.PropsWithChildren<unknown>>
serviceSend({ type: 'FINISH_SELECTION' });
}, [serviceSend]);

const startReposition = useCallback(
(
repositioningFeatureDataset: LocationFeatureDataset,
index: number,
selectingComponentId?: string,
) => {
serviceSend({
type: 'START_REPOSITION',
repositioningFeatureDataset,
repositioningPropertyIndex: index,
selectingComponentId,
});
},
[serviceSend],
);

const finishReposition = useCallback(() => {
serviceSend({ type: 'FINISH_REPOSITION' });
}, [serviceSend]);

const setFilePropertyLocations = useCallback(
(locations: LatLngLiteral[]) => {
serviceSend({ type: 'SET_FILE_PROPERTY_LOCATIONS', locations });
Expand Down Expand Up @@ -316,9 +345,14 @@ export const MapStateMachineProvider: React.FC<React.PropsWithChildren<unknown>>
serviceSend({ type: 'TOGGLE_LAYERS' });
}, [serviceSend]);

const isRepositioning = useMemo(() => {
return state.matches({ mapVisible: { featureView: 'repositioning' } });
}, [state]);

// disable map popup when repositioning file markers
const showPopup = useMemo(() => {
return state.context.mapLocationFeatureDataset !== null;
}, [state.context.mapLocationFeatureDataset]);
return state.context.mapLocationFeatureDataset !== null && !isRepositioning;
}, [isRepositioning, state.context.mapLocationFeatureDataset]);

const isFiltering = useMemo(() => {
return state.matches({ mapVisible: { featureView: 'filtering' } });
Expand All @@ -339,6 +373,8 @@ export const MapStateMachineProvider: React.FC<React.PropsWithChildren<unknown>>
mapLocationSelected: state.context.mapLocationSelected,
mapLocationFeatureDataset: state.context.mapLocationFeatureDataset,
selectedFeatureDataset: state.context.selectedFeatureDataset,
repositioningFeatureDataset: state.context.repositioningFeatureDataset,
repositioningPropertyIndex: state.context.repositioningPropertyIndex,
showPopup: showPopup,
isLoading: state.context.isLoading,
mapSearchCriteria: state.context.searchCriteria,
Expand All @@ -347,6 +383,7 @@ export const MapStateMachineProvider: React.FC<React.PropsWithChildren<unknown>>
pendingFitBounds: state.matches({ mapVisible: { mapRequest: 'pendingFitBounds' } }),
requestedFitBounds: state.context.requestedFitBounds,
isSelecting: state.matches({ mapVisible: { featureView: 'selecting' } }),
isRepositioning: isRepositioning,
selectingComponentId: state.context.selectingComponentId,
isFiltering: isFiltering,
isShowingMapLayers: isShowingMapLayers,
Expand All @@ -369,6 +406,8 @@ export const MapStateMachineProvider: React.FC<React.PropsWithChildren<unknown>>
prepareForCreation,
startSelection,
finishSelection,
startReposition,
finishReposition,
toggleMapFilter,
toggleMapLayer,
toggleSidebarDisplay,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ const featureViewStates = {
assign({ selectingComponentId: (_, event: any) => event.selectingComponentId }),
],
},
START_REPOSITION: {
target: 'repositioning',
actions: [
assign({
selectingComponentId: (_, event: any) => event.selectingComponentId,
repositioningFeatureDataset: (_, event: any) => event.repositioningFeatureDataset,
repositioningPropertyIndex: (_, event: any) => event.repositioningPropertyIndex,
}),
],
},
TOGGLE_FILTER: {
target: 'filtering',
},
Expand All @@ -31,7 +41,30 @@ const featureViewStates = {
},
selecting: {
on: {
FINISH_SELECTION: { target: 'browsing' },
FINISH_SELECTION: {
target: 'browsing',
actions: [assign({ selectingComponentId: () => null })],
},
SET_FILE_PROPERTY_LOCATIONS: {
actions: [
assign({ filePropertyLocations: (_, event: any) => event.locations }),
raise('REQUEST_FIT_BOUNDS'),
],
},
},
},
repositioning: {
on: {
FINISH_REPOSITION: {
target: 'browsing',
actions: [
assign({
repositioningFeatureDataset: () => null,
repositioningPropertyIndex: () => null,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does selectingComponentId need to be set to null as well?

selectingComponentId: () => null,
}),
],
},
SET_FILE_PROPERTY_LOCATIONS: {
actions: [
assign({ filePropertyLocations: (_, event: any) => event.locations }),
Expand Down Expand Up @@ -382,6 +415,8 @@ export const mapMachine = createMachine<MachineContext>({
mapFeatureSelected: null,
mapLocationFeatureDataset: null,
selectedFeatureDataset: null,
repositioningFeatureDataset: null,
repositioningPropertyIndex: null,
selectingComponentId: null,
isLoading: false,
searchCriteria: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export type MachineContext = {
mapLocationSelected: LatLngLiteral | null;
mapLocationFeatureDataset: LocationFeatureDataset | null;
selectedFeatureDataset: LocationFeatureDataset | null;
repositioningFeatureDataset: LocationFeatureDataset | null;
repositioningPropertyIndex: number | null;
selectingComponentId: string | null;

mapFeatureData: MapFeatureData;
Expand Down
45 changes: 29 additions & 16 deletions source/frontend/src/components/maps/MapLeafletView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,28 +96,41 @@ const MapLeafletView: React.FC<React.PropsWithChildren<MapLeafletViewProps>> = (
zoom,
]);

const mapMachineMapLocationFeatureDataset = mapMachine.mapLocationFeatureDataset;
const { mapLocationFeatureDataset, repositioningFeatureDataset, isRepositioning } = mapMachine;

useEffect(() => {
activeFeatureLayer?.clearLayers();
if (mapMachineMapLocationFeatureDataset !== null) {
const location = mapMachineMapLocationFeatureDataset.location;

let activeFeature: Feature<Geometry, GeoJsonProperties> = {
geometry: { coordinates: [location.lng, location.lat], type: 'Point' },
type: 'Feature',
properties: {},
};
if (mapMachineMapLocationFeatureDataset.parcelFeature !== null) {
activeFeature = mapMachineMapLocationFeatureDataset.parcelFeature;
activeFeatureLayer?.addData(activeFeature);
} else if (mapMachineMapLocationFeatureDataset.municipalityFeature !== null) {
activeFeature = mapMachineMapLocationFeatureDataset.municipalityFeature;
if (mapMachineMapLocationFeatureDataset.municipalityFeature?.geometry?.type === 'Polygon') {

if (isRepositioning) {
if (
repositioningFeatureDataset !== null &&
repositioningFeatureDataset.pimsFeature !== null
) {
// File marker repositioning is active - highlight the property and the corresponding boundary when user triggers the relocate action.
activeFeatureLayer?.addData(repositioningFeatureDataset.pimsFeature);
}
} else {
// Not repositioning - highlight parcels on map click as usual workflow
if (mapLocationFeatureDataset !== null) {
const location = mapLocationFeatureDataset.location;

let activeFeature: Feature<Geometry, GeoJsonProperties> = {
geometry: { coordinates: [location.lng, location.lat], type: 'Point' },
type: 'Feature',
properties: {},
};
if (mapLocationFeatureDataset.parcelFeature !== null) {
activeFeature = mapLocationFeatureDataset.parcelFeature;
activeFeatureLayer?.addData(activeFeature);
} else if (mapLocationFeatureDataset.municipalityFeature !== null) {
activeFeature = mapLocationFeatureDataset.municipalityFeature;
if (mapLocationFeatureDataset.municipalityFeature?.geometry?.type === 'Polygon') {
activeFeatureLayer?.addData(activeFeature);
}
}
}
}
}, [activeFeatureLayer, mapMachineMapLocationFeatureDataset]);
}, [activeFeatureLayer, isRepositioning, mapLocationFeatureDataset, repositioningFeatureDataset]);

const hasPendingFlyTo = mapMachine.pendingFlyTo;
const requestedFlyTo = mapMachine.requestedFlyTo;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { LatLngLiteral } from 'leaflet';

import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext';
import useDraftMarkerSynchronizer from '@/hooks/useDraftMarkerSynchronizer';
import { usePrevious } from '@/hooks/usePrevious';
Expand All @@ -7,14 +9,22 @@ import { featuresetToMapProperty } from '@/utils/mapPropertyUtils';
import { LocationFeatureDataset } from '../common/mapFSM/useLocationFeatureLoader';

interface IMapClickMonitorProps {
addProperty: (property: LocationFeatureDataset) => void; // TODO: This should be a featureDataset
addProperty: (property: LocationFeatureDataset) => void;
asanchezr marked this conversation as resolved.
Show resolved Hide resolved
repositionProperty: (
property: LocationFeatureDataset,
latLng: LatLngLiteral,
propertyIndex: number | null,
) => void;
modifiedProperties: LocationFeatureDataset[]; // TODO: this should be just a list of lat longs
selectedComponentId: string | null;
}

export const MapClickMonitor: React.FunctionComponent<
React.PropsWithChildren<IMapClickMonitorProps>
> = ({ addProperty, modifiedProperties, selectedComponentId }) => {
export const MapClickMonitor: React.FunctionComponent<IMapClickMonitorProps> = ({
addProperty,
repositionProperty,
modifiedProperties,
selectedComponentId,
}) => {
const mapMachine = useMapStateMachine();

const previous = usePrevious(mapMachine.mapLocationFeatureDataset);
Expand All @@ -32,7 +42,30 @@ export const MapClickMonitor: React.FunctionComponent<
) {
addProperty(mapMachine.mapLocationFeatureDataset);
}
}, [addProperty, mapMachine.isSelecting, mapMachine.mapLocationFeatureDataset, previous]);

if (
mapMachine.isRepositioning &&
mapMachine.repositioningFeatureDataset &&
mapMachine.mapLocationFeatureDataset &&
previous !== mapMachine.mapLocationFeatureDataset &&
previous !== undefined &&
(!selectedComponentId ||
selectedComponentId === mapMachine.mapLocationFeatureDataset.selectingComponentId)
) {
repositionProperty(
mapMachine.repositioningFeatureDataset,
mapMachine.mapLocationFeatureDataset.location,
mapMachine.repositioningPropertyIndex,
);
}
}, [
addProperty,
mapMachine.isSelecting,
mapMachine.isRepositioning,
mapMachine.mapLocationFeatureDataset,
mapMachine.repositioningFeatureDataset,
previous,
]);
return <></>;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { act, screen } from '@testing-library/react';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { Formik } from 'formik';
import noop from 'lodash/noop';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';

import { useMapProperties } from '@/hooks/repositories/useMapProperties';
import { mockFAParcelLayerResponse, mockGeocoderOptions } from '@/mocks/index.mock';
import { fillInput, render, RenderOptions, userEvent } from '@/utils/test-utils';
import { mapMachineBaseMock } from '@/mocks/mapFSM.mock';
import { act, fillInput, render, RenderOptions, screen, userEvent } from '@/utils/test-utils';

import { PropertyForm } from '../../features/mapSideBar/shared/models';
import MapSelectorContainer, { IMapSelectorContainerProps } from './MapSelectorContainer';
import { IMapProperty } from './models';
import { getMockLocationFeatureDataset } from '@/mocks/featureset.mock';
import { useMapProperties } from '@/hooks/repositories/useMapProperties';
import { IMapStateMachineContext } from '../common/mapFSM/MapStateMachineContext';

const mockStore = configureMockStore([thunk]);

Expand All @@ -22,6 +22,7 @@ const mockAxios = new MockAdapter(axios);
const store = mockStore({});

const onSelectedProperties = vi.fn();
const onRepositionSelectedProperty = vi.fn();

const testProperty: IMapProperty = {
propertyId: 123,
Expand Down Expand Up @@ -55,12 +56,14 @@ describe('MapSelectorContainer component', () => {
<Formik initialValues={{ properties: [] }} onSubmit={noop}>
<MapSelectorContainer
addSelectedProperties={onSelectedProperties}
repositionSelectedProperty={onRepositionSelectedProperty}
modifiedProperties={renderOptions.modifiedProperties ?? []}
/>
</Formik>,
{
...renderOptions,
store: store,
mockMapMachine: renderOptions.mockMapMachine ?? mapMachineBaseMock,
},
);

Expand Down Expand Up @@ -295,4 +298,35 @@ describe('MapSelectorContainer component', () => {
);
expect(toast[0]).toBeVisible();
});

it(`calls "repositionSelectedProperty" callback when file marker has been repositioned`, async () => {
const testMapMock: IMapStateMachineContext = { ...mapMachineBaseMock };
const mapProperties = [
PropertyForm.fromMapProperty({ ...testProperty, pid: '009-727-493' }).toFeatureDataset(),
];

const { rerender } = setup({
modifiedProperties: mapProperties,
mockMapMachine: testMapMock,
});

// simulate file marker repositioning via the map state machine
await act(async () => {
testMapMock.isRepositioning = true;
testMapMock.repositioningFeatureDataset = {} as any;
testMapMock.mapLocationFeatureDataset = {} as any;
});

rerender(
<Formik initialValues={{ properties: [] }} onSubmit={noop}>
<MapSelectorContainer
addSelectedProperties={onSelectedProperties}
repositionSelectedProperty={onRepositionSelectedProperty}
modifiedProperties={mapProperties}
/>
</Formik>,
);

expect(onRepositionSelectedProperty).toHaveBeenCalled();
});
});
Loading
Loading