diff --git a/docs/plugins/interact.md b/docs/plugins/interact.md index 4136b808..352b4861 100644 --- a/docs/plugins/interact.md +++ b/docs/plugins/interact.md @@ -295,7 +295,6 @@ Emitted when the user confirms their selection (clicks "Done"). // If features were selected: selectedFeatures: [...], - selectionBounds: [west, south, east, north], // If markers were selected: selectedMarkers: ['...'] @@ -340,10 +339,9 @@ Emitted whenever the selected features or selected markers change. ```js { selectedFeatures: [ - { featureId: '...', layerId: '...', properties: {...}, geometry: {...} } + { featureId: '...', layerId: '...', idProperty: '...', properties: {...} } ], selectedMarkers: ['...'], // array of selected marker IDs - selectionBounds: [west, south, east, north] | null, contiguous: boolean // true when 2+ features are selected and all form a single contiguous group } ``` @@ -351,7 +349,6 @@ Emitted whenever the selected features or selected markers change. ```js interactiveMap.on('interact:selectionchange', (e) => { console.log('Selected features:', e.selectedFeatures) - console.log('Bounds:', e.selectionBounds) }) ``` diff --git a/plugins/interact/src/InteractInit.jsx b/plugins/interact/src/InteractInit.jsx index e75af2b9..a954e547 100755 --- a/plugins/interact/src/InteractInit.jsx +++ b/plugins/interact/src/InteractInit.jsx @@ -27,7 +27,7 @@ export const InteractInit = ({ mapProvider, pluginState }) => { - const { dispatch, enabled, selectedFeatures, interactionModes, layers } = pluginState + const { enabled, selectedFeatures, interactionModes, layers } = pluginState const { eventBus } = services const { crossHair, mapStyle, markers } = mapState @@ -44,13 +44,11 @@ export const InteractInit = ({ mapProvider }) - // Highlight features and sync state selectedBounds from mapProvider useHighlightSync({ mapProvider, mapStyle, pluginState, selectedFeatures, - dispatch, events: EVENTS, eventBus }) diff --git a/plugins/interact/src/InteractInit.test.js b/plugins/interact/src/InteractInit.test.js index 7451c664..938793f6 100644 --- a/plugins/interact/src/InteractInit.test.js +++ b/plugins/interact/src/InteractInit.test.js @@ -37,7 +37,6 @@ beforeEach(() => { enabled: true, selectedFeatures: [], selectedMarkers: [], - selectionBounds: {}, interactionModes: ['selectFeature'], layers: [] } @@ -63,7 +62,6 @@ describe('InteractInit — hook delegation', () => { mapStyle: props.mapState.mapStyle, pluginState: props.pluginState, selectedFeatures: props.pluginState.selectedFeatures, - dispatch: props.pluginState.dispatch, events: EVENTS, eventBus: props.services.eventBus })) diff --git a/plugins/interact/src/events.test.js b/plugins/interact/src/events.test.js index a348510c..6c2d3e35 100644 --- a/plugins/interact/src/events.test.js +++ b/plugins/interact/src/events.test.js @@ -5,7 +5,7 @@ const MOCK_COORDS = [1, 2] const createParams = () => { const appState = { layoutRefs: { viewportRef: { current: document.body } }, disabledButtons: new Set() } - const pluginState = { dispatch: jest.fn(), selectionBounds: null, selectedFeatures: [], selectedMarkers: [], multiSelect: false } + const pluginState = { dispatch: jest.fn(), selectedFeatures: [], selectedMarkers: [], multiSelect: false } const clickReadyRef = { current: false } return { appState, diff --git a/plugins/interact/src/hooks/useHighlightSync.js b/plugins/interact/src/hooks/useHighlightSync.js index 5b6450fa..57687018 100755 --- a/plugins/interact/src/hooks/useHighlightSync.js +++ b/plugins/interact/src/hooks/useHighlightSync.js @@ -9,16 +9,12 @@ import { buildStylesMap } from '../utils/buildStylesMap.js' * (shown with the keyboard cursor ring) as separate arguments so the provider can style them * differently. Also re-applies highlights after a map style reload, since highlight layers * are removed when the base style changes. - * - * Dispatches UPDATE_SELECTED_BOUNDS with the bounding box returned by the provider so - * downstream consumers (e.g. interact:done) receive up-to-date bounds. */ export const useHighlightSync = ({ mapProvider, mapStyle, pluginState, selectedFeatures, - dispatch, events, eventBus }) => { @@ -37,12 +33,7 @@ export const useHighlightSync = ({ const activeFeatures = listboxActiveItem ? [{ featureId: listboxActiveItem.featureId, layerId: listboxActiveItem.layerId, idProperty: listboxActiveItem.idProperty, geometry: listboxActiveItem.geometry }] : [] - const bounds = mapProvider.updateHighlightedFeatures?.(selectedFeatures, activeFeatures, stylesMap) - - dispatch({ - type: 'UPDATE_SELECTED_BOUNDS', - payload: bounds - }) + mapProvider.updateHighlightedFeatures?.(selectedFeatures, activeFeatures, stylesMap) } useEffect(() => { diff --git a/plugins/interact/src/hooks/useHighlightSync.test.js b/plugins/interact/src/hooks/useHighlightSync.test.js index 24e56c9e..291d6cb8 100644 --- a/plugins/interact/src/hooks/useHighlightSync.test.js +++ b/plugins/interact/src/hooks/useHighlightSync.test.js @@ -30,7 +30,6 @@ beforeEach(() => { layers: [{ layerId: 'layer1' }] }, selectedFeatures: [], - dispatch: jest.fn(), events: { MAP_STYLE_CHANGE: STYLE_CHANGE, MAP_DATA_CHANGE: DATA_CHANGE }, eventBus: { on: jest.fn((event, handler) => { @@ -51,7 +50,7 @@ beforeEach(() => { // ─── useHighlightSync — highlighting ───────────────────────────────────────── describe('useHighlightSync — highlighting', () => { - it('updates map highlights and dispatches bounds', () => { + it('updates map highlights', () => { mockDeps.selectedFeatures = [{ featureId: 'F1', layerId: 'layer1' }] render() @@ -61,22 +60,6 @@ describe('useHighlightSync — highlighting', () => { [], expect.any(Object) ) - expect(mockDeps.dispatch).toHaveBeenCalledWith({ - type: 'UPDATE_SELECTED_BOUNDS', - payload: { sw: [0, 0], ne: [1, 1] } - }) - }) - - it('dispatches null bounds when provider returns null', () => { - mockDeps.selectedFeatures = [{ featureId: 'F1' }] - mockDeps.mapProvider.updateHighlightedFeatures.mockReturnValue(null) - - render() - - expect(mockDeps.dispatch).toHaveBeenCalledWith({ - type: 'UPDATE_SELECTED_BOUNDS', - payload: null - }) }) }) @@ -144,9 +127,7 @@ describe('useHighlightSync — guards', () => { mockDeps.mapProvider = null mockDeps.selectedFeatures = [{ featureId: 'F1' }] - render() - - expect(mockDeps.dispatch).not.toHaveBeenCalled() + expect(() => render()).not.toThrow() }) it('does nothing when mapStyle is null', () => { diff --git a/plugins/interact/src/hooks/useInteractionHandlers.js b/plugins/interact/src/hooks/useInteractionHandlers.js index 7de4f5fa..7a0718a1 100755 --- a/plugins/interact/src/hooks/useInteractionHandlers.js +++ b/plugins/interact/src/hooks/useInteractionHandlers.js @@ -37,15 +37,10 @@ const findMarkerAtPoint = (markers, point, scale) => { return null } -const useSelectionChangeEmitter = (eventBus, selectedFeatures, selectedMarkers, selectionBounds) => { +const useSelectionChangeEmitter = (eventBus, selectedFeatures, selectedMarkers) => { const lastEmittedSelectionChange = useRef(null) useEffect(() => { - const awaitingBounds = selectedFeatures.length > 0 && !selectionBounds - if (awaitingBounds) { - return - } - const prev = lastEmittedSelectionChange.current const wasEmpty = prev === null || (prev.features.length === 0 && prev.markers.length === 0) if (wasEmpty && selectedFeatures.length === 0 && selectedMarkers.length === 0) { @@ -53,14 +48,13 @@ const useSelectionChangeEmitter = (eventBus, selectedFeatures, selectedMarkers, } eventBus.emit('interact:selectionchange', { - selectedFeatures, + selectedFeatures: selectedFeatures.map(({ featureId, layerId, idProperty, properties }) => ({ featureId, layerId, idProperty, properties })), selectedMarkers, - selectionBounds, contiguous: areAllContiguous(selectedFeatures) }) lastEmittedSelectionChange.current = { features: selectedFeatures, markers: selectedMarkers } - }, [selectedFeatures, selectedMarkers, selectionBounds]) + }, [selectedFeatures, selectedMarkers]) } /** @@ -220,7 +214,7 @@ export const useInteractionHandlers = ({ mapState, pluginState, services, mapPro const { dispatch, layers, interactionModes, multiSelect, contiguous, marker: markerOptions, tolerance, selectedFeatures, selectedMarkers, - selectionBounds, deselectOnClickOutside + deselectOnClickOutside } = pluginState const { eventBus } = services const layerConfigMap = buildLayerConfigMap(layers) @@ -272,6 +266,6 @@ export const useInteractionHandlers = ({ mapState, pluginState, services, mapPro scale }) - useSelectionChangeEmitter(eventBus, selectedFeatures, selectedMarkers, selectionBounds) + useSelectionChangeEmitter(eventBus, selectedFeatures, selectedMarkers) return { handleInteraction } } diff --git a/plugins/interact/src/hooks/useInteractionHandlers.test.js b/plugins/interact/src/hooks/useInteractionHandlers.test.js index b9b059ce..01e8d8de 100644 --- a/plugins/interact/src/hooks/useInteractionHandlers.test.js +++ b/plugins/interact/src/hooks/useInteractionHandlers.test.js @@ -65,7 +65,6 @@ const setup = (pluginOverrides = {}, markerItems = [], markerRefs = new Map()) = marker: { symbol: 'pin', backgroundColor: 'red' }, selectedFeatures: [], selectedMarkers: [], - selectionBounds: null, ...pluginOverrides }, services: { @@ -345,30 +344,12 @@ it('does not check markers when selectMarker is not in interactionModes', () => /* Selection change event */ /* ------------------------------------------------------------------ */ -it('does not emit selectionchange when features are selected but bounds not yet calculated', () => { +it('emits selectionchange when features are selected', () => { const deps = { mapState: { markers: { add: jest.fn(), remove: jest.fn(), items: [], markerRefs: new Map() } }, pluginState: { - selectedFeatures: [{ featureId: 'F1' }], - selectedMarkers: [], - selectionBounds: null - }, - services: { eventBus: { emit: jest.fn() } }, - mapProvider: { getFeatureGeometry: jest.fn(() => null) } - } - - renderHook(() => useInteractionHandlers(deps)) - - expect(deps.services.eventBus.emit).not.toHaveBeenCalled() -}) - -it('emits selectionchange once when bounds exist', () => { - const deps = { - mapState: { markers: { add: jest.fn(), remove: jest.fn(), items: [], markerRefs: new Map() } }, - pluginState: { - selectedFeatures: [{ featureId: 'F1' }], - selectedMarkers: [], - selectionBounds: { sw: [0, 0], ne: [1, 1] } + selectedFeatures: [{ featureId: 'F1', layerId: 'l1', idProperty: 'id', properties: { name: 'A' }, geometry: { type: 'Point', coordinates: [0, 0] } }], + selectedMarkers: [] }, services: { eventBus: { emit: jest.fn() } }, mapProvider: { getFeatureGeometry: jest.fn(() => null) } @@ -379,9 +360,8 @@ it('emits selectionchange once when bounds exist', () => { expect(deps.services.eventBus.emit).toHaveBeenCalledWith( 'interact:selectionchange', expect.objectContaining({ - selectedFeatures: deps.pluginState.selectedFeatures, + selectedFeatures: [{ featureId: 'F1', layerId: 'l1', idProperty: 'id', properties: { name: 'A' } }], selectedMarkers: [], - selectionBounds: deps.pluginState.selectionBounds, contiguous: false }) ) @@ -394,7 +374,7 @@ it('skips emission when selection remains empty after being cleared', () => { const { rerender } = renderHook( ({ features }) => useInteractionHandlers({ mapState: { markers: { items: [], markerRefs: new Map() } }, - pluginState: { selectedFeatures: features, selectedMarkers: [], selectionBounds: { b: 1 } }, + pluginState: { selectedFeatures: features, selectedMarkers: [] }, services: { eventBus }, mapProvider: { getFeatureGeometry: jest.fn(() => null) } }), diff --git a/plugins/interact/src/reducer.js b/plugins/interact/src/reducer.js index 92d1a927..bc182ccf 100755 --- a/plugins/interact/src/reducer.js +++ b/plugins/interact/src/reducer.js @@ -7,7 +7,6 @@ const initialState = { deselectOnClickOutside: false, selectedFeatures: [], selectedMarkers: [], - selectionBounds: null, closeOnAction: true, // Done or Cancel listboxActiveItem: null // { featureId, layerId, idProperty, geometry } | null — ring shown but no selectionchange } @@ -26,7 +25,6 @@ const disable = (state) => { enabled: false, selectedFeatures: [], selectedMarkers: [], - selectionBounds: null, listboxActiveItem: null } } @@ -46,7 +44,7 @@ const toggleSelectedFeatures = (state, payload) => { // 1. Handle explicit unselect if (addToExisting === false) { const filtered = currentSelected.filter((_, i) => i !== existingIndex) - return { ...state, selectedFeatures: filtered, selectionBounds: null } + return { ...state, selectedFeatures: filtered } } // Define the feature object once to avoid repetition @@ -70,18 +68,7 @@ const toggleSelectedFeatures = (state, payload) => { nextSelected = isSameSingle ? [] : [featureObj] } - return { ...state, selectedFeatures: nextSelected, selectedMarkers: multiSelect && !replaceAll ? state.selectedMarkers : [], selectionBounds: null } -} - -// Update bounds (called from useEffect after map provider calculates them) -const updateSelectedBounds = (state, payload) => { - if (JSON.stringify(payload) === JSON.stringify(state.selectionBounds)) { - return state - } - return { - ...state, - selectionBounds: payload - } + return { ...state, selectedFeatures: nextSelected, selectedMarkers: multiSelect && !replaceAll ? state.selectedMarkers : [] } } const toggleSelectedMarkers = (state, { markerId, multiSelect }) => { @@ -94,7 +81,6 @@ const toggleSelectedMarkers = (state, { markerId, multiSelect }) => { return { ...state, selectedFeatures: [], - selectionBounds: null, selectedMarkers: exists && current.length === 1 ? [] : [markerId] } } @@ -103,8 +89,7 @@ const clearSelectedFeatures = (state) => { return { ...state, selectedFeatures: [], - selectedMarkers: [], - selectionBounds: null + selectedMarkers: [] } } @@ -124,7 +109,6 @@ const selectMarker = (state, { markerId, multiSelect }) => { return { ...state, selectedFeatures: multiSelect ? state.selectedFeatures : [], - selectionBounds: null, selectedMarkers: nextMarkers } } @@ -145,8 +129,7 @@ const unselectMarker = (state, { markerId }) => { const setSelectedFeatures = (state, features) => ({ ...state, - selectedFeatures: features, - selectionBounds: null + selectedFeatures: features }) const actions = { @@ -154,7 +137,6 @@ const actions = { DISABLE: disable, TOGGLE_SELECTED_FEATURES: toggleSelectedFeatures, TOGGLE_SELECTED_MARKERS: toggleSelectedMarkers, - UPDATE_SELECTED_BOUNDS: updateSelectedBounds, CLEAR_SELECTED_FEATURES: clearSelectedFeatures, SET_SELECTED_FEATURES: setSelectedFeatures, SELECT_MARKER: selectMarker, diff --git a/plugins/interact/src/reducer.test.js b/plugins/interact/src/reducer.test.js index 6039705b..e8c0be59 100644 --- a/plugins/interact/src/reducer.test.js +++ b/plugins/interact/src/reducer.test.js @@ -11,7 +11,6 @@ describe('initialState', () => { deselectOnClickOutside: false, selectedFeatures: [], selectedMarkers: [], - selectionBounds: null, closeOnAction: true, listboxActiveItem: null }) @@ -33,7 +32,7 @@ describe('ENABLE/DISABLE actions', () => { it('DISABLE sets enabled to false, clears selection and markers, and preserves other state', () => { const marker = { symbol: 'pin', backgroundColor: 'red' } - const state = { ...initialState, enabled: true, layers: [1], marker, selectedFeatures: [{ featureId: 'f1' }], selectedMarkers: ['m1'], selectionBounds: [0, 0, 1, 1] } + const state = { ...initialState, enabled: true, layers: [1], marker, selectedFeatures: [{ featureId: 'f1' }], selectedMarkers: ['m1'] } const result = actions.DISABLE(state) expect(result.enabled).toBe(false) @@ -41,7 +40,6 @@ describe('ENABLE/DISABLE actions', () => { expect(result.marker).toEqual(marker) expect(result.selectedFeatures).toEqual([]) expect(result.selectedMarkers).toEqual([]) - expect(result.selectionBounds).toBeNull() expect(result).not.toBe(state) }) }) @@ -56,7 +54,7 @@ describe('TOGGLE_SELECTED_FEATURES action', () => { }) it('handles single-select, multi-select, add/remove, and replaceAll', () => { - let state = { ...initialState, selectionBounds: { sw: [0, 0], ne: [1, 1] } } + let state = { ...initialState } // Single-select: add state = actions.TOGGLE_SELECTED_FEATURES(state, createFeature('f1')) @@ -66,33 +64,27 @@ describe('TOGGLE_SELECTED_FEATURES action', () => { state = actions.TOGGLE_SELECTED_FEATURES(state, createFeature('f2')) expect(state.selectedFeatures[0].featureId).toBe('f2') - // Toggle off same - clears bounds + // Toggle off same state = actions.TOGGLE_SELECTED_FEATURES(state, createFeature('f2')) expect(state.selectedFeatures).toHaveLength(0) - expect(state.selectionBounds).toBeNull() // Multi-select: add multiple - state = { ...state, selectionBounds: { sw: [0, 0], ne: [1, 1] } } state = actions.TOGGLE_SELECTED_FEATURES(state, { ...createFeature('f1'), multiSelect: true }) state = actions.TOGGLE_SELECTED_FEATURES(state, { ...createFeature('f2'), multiSelect: true }) expect(state.selectedFeatures.map(f => f.featureId)).toEqual(['f1', 'f2']) - // Multi-select: remove (not last) - clears bounds for recalculation + // Multi-select: remove (not last) state = actions.TOGGLE_SELECTED_FEATURES(state, { ...createFeature('f1'), multiSelect: true }) expect(state.selectedFeatures.map(f => f.featureId)).toEqual(['f2']) - expect(state.selectionBounds).toBeNull() - // Multi-select: remove last - clears bounds + // Multi-select: remove last state = actions.TOGGLE_SELECTED_FEATURES(state, { ...createFeature('f2'), multiSelect: true }) expect(state.selectedFeatures).toHaveLength(0) - expect(state.selectionBounds).toBeNull() - // addToExisting false removes feature - clears bounds when empty - state = { ...state, selectionBounds: { sw: [0, 0], ne: [1, 1] } } + // addToExisting false removes feature state = actions.TOGGLE_SELECTED_FEATURES(state, { ...createFeature('f2'), multiSelect: true }) state = actions.TOGGLE_SELECTED_FEATURES(state, { ...createFeature('f2'), addToExisting: false }) expect(state.selectedFeatures).toHaveLength(0) - expect(state.selectionBounds).toBeNull() // replaceAll replaces everything state = actions.TOGGLE_SELECTED_FEATURES(state, { ...createFeature('f3'), replaceAll: true }) @@ -131,26 +123,12 @@ describe('TOGGLE_SELECTED_FEATURES action', () => { }) }) -describe('UPDATE_SELECTED_BOUNDS action', () => { - it('updates selectionBounds correctly', () => { - const state = { ...initialState, selectionBounds: { sw: [0, 0], ne: [1, 1] } } - const newBounds = { sw: [0, 0], ne: [2, 2] } - const result = actions.UPDATE_SELECTED_BOUNDS(state, newBounds) - expect(result.selectionBounds).toEqual(newBounds) - - // unchanged bounds returns same state - const result2 = actions.UPDATE_SELECTED_BOUNDS(state, { sw: [0, 0], ne: [1, 1] }) - expect(result2).toBe(state) - }) -}) - describe('TOGGLE_SELECTED_MARKERS action', () => { it('selects a marker in single-select mode and clears features', () => { - const state = { ...initialState, selectedFeatures: [{ featureId: 'f1' }], selectionBounds: { sw: [0, 0], ne: [1, 1] } } + const state = { ...initialState, selectedFeatures: [{ featureId: 'f1' }] } const result = actions.TOGGLE_SELECTED_MARKERS(state, { markerId: 'm1', multiSelect: false }) expect(result.selectedMarkers).toEqual(['m1']) expect(result.selectedFeatures).toEqual([]) - expect(result.selectionBounds).toBeNull() }) it('toggles off the only selected marker in single-select mode', () => { @@ -174,17 +152,15 @@ describe('TOGGLE_SELECTED_MARKERS action', () => { }) describe('CLEAR_SELECTED_FEATURES action', () => { - it('resets features, markers and bounds', () => { + it('resets features and markers', () => { const state = { ...initialState, selectedFeatures: [1], - selectedMarkers: ['m1'], - selectionBounds: { sw: [0, 0], ne: [1, 1] } + selectedMarkers: ['m1'] } const result = actions.CLEAR_SELECTED_FEATURES(state) expect(result.selectedFeatures).toEqual([]) expect(result.selectedMarkers).toEqual([]) - expect(result.selectionBounds).toBeNull() expect(result).not.toBe(state) }) }) @@ -209,7 +185,6 @@ describe('SELECT_MARKER action', () => { const result = actions.SELECT_MARKER(state, { markerId: 'm1', multiSelect: false }) expect(result.selectedMarkers).toEqual(['m1']) expect(result.selectedFeatures).toEqual([]) - expect(result.selectionBounds).toBeNull() }) it('adds a marker in multi-select mode without clearing selectedFeatures', () => { @@ -241,16 +216,14 @@ describe('UNSELECT_MARKER action', () => { }) describe('SET_SELECTED_FEATURES action', () => { - it('replaces selectedFeatures and clears bounds', () => { + it('replaces selectedFeatures', () => { const state = { ...initialState, - selectedFeatures: [{ featureId: 'A' }, { featureId: 'B' }, { featureId: 'C' }], - selectionBounds: [0, 0, 1, 1] + selectedFeatures: [{ featureId: 'A' }, { featureId: 'B' }, { featureId: 'C' }] } const trimmed = [{ featureId: 'A' }] const result = actions.SET_SELECTED_FEATURES(state, trimmed) expect(result.selectedFeatures).toEqual(trimmed) - expect(result.selectionBounds).toBeNull() expect(result).not.toBe(state) }) }) @@ -262,7 +235,6 @@ describe('actions object', () => { 'DISABLE', 'TOGGLE_SELECTED_FEATURES', 'TOGGLE_SELECTED_MARKERS', - 'UPDATE_SELECTED_BOUNDS', 'CLEAR_SELECTED_FEATURES', 'SET_SELECTED_FEATURES', 'SELECT_MARKER',