diff --git a/demo/js/index.js b/demo/js/index.js index afe064b4..de122468 100755 --- a/demo/js/index.js +++ b/demo/js/index.js @@ -62,8 +62,7 @@ const interactPlugin = createInteractPlugin({ }], debug: true, interactionModes: ['selectMarker', 'placeMarker', 'selectFeature'], // e.g. ['selectMarker'], ['selectFeature'], ['placeMarker'], or combinations - // multiSelect: true, - contiguous: true, + multiSelect: true, deselectOnClickOutside: true }) diff --git a/docs/plugins/interact.md b/docs/plugins/interact.md index 980e6bf4..8e96921e 100644 --- a/docs/plugins/interact.md +++ b/docs/plugins/interact.md @@ -109,14 +109,6 @@ When `true`, clicking additional features adds them to the selection rather than --- -### `contiguous` -**Type:** `boolean` -**Default:** `false` - -When `true`, only features that touch or overlap the existing selection can be added. Uses spatial intersection to determine contiguity. Works with polygons, lines, and points. - ---- - ### `deselectOnClickOutside` **Type:** `boolean` **Default:** `false` @@ -350,8 +342,7 @@ Emitted whenever the selected features or selected markers change. ], selectedMarkers: ['...'], // array of selected marker IDs selectionBounds: [west, south, east, north] | null, - canMerge: boolean, // true when all selected features are contiguous - canSplit: boolean // true when exactly one Polygon or MultiPolygon is selected + contiguous: boolean // true when 2+ features are selected and all form a single contiguous group } ``` diff --git a/plugins/interact/src/defaults.js b/plugins/interact/src/defaults.js index f50e3172..f765b183 100755 --- a/plugins/interact/src/defaults.js +++ b/plugins/interact/src/defaults.js @@ -2,7 +2,6 @@ export const DEFAULTS = { tolerance: 10, interactionModes: ['selectMarker'], multiSelect: false, - contiguous: false, deselectOnClickOutside: false, marker: {}, selectedStrokeWidth: 3 diff --git a/plugins/interact/src/hooks/useInteractionHandlers.js b/plugins/interact/src/hooks/useInteractionHandlers.js index 9c8f6228..55e0b604 100755 --- a/plugins/interact/src/hooks/useInteractionHandlers.js +++ b/plugins/interact/src/hooks/useInteractionHandlers.js @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef } from 'react' -import { isContiguousWithAny, canSplitFeatures, areAllContiguous } from '../utils/spatial.js' +import { areAllContiguous } from '../utils/spatial.js' import { getFeaturesAtPoint, findMatchingFeature, buildLayerConfigMap } from '../utils/featureQueries.js' import { scaleFactor } from '../../../../src/config/appConfig.js' import { isStandaloneLabel } from '../../../../src/utils/symbolUtils.js' @@ -59,8 +59,7 @@ const useSelectionChangeEmitter = (eventBus, selectedFeatures, selectedMarkers, selectedFeatures, selectedMarkers, selectionBounds, - canMerge: areAllContiguous(selectedFeatures), - canSplit: canSplitFeatures(selectedFeatures) + contiguous: areAllContiguous(selectedFeatures) }) lastEmittedSelectionChange.current = { features: selectedFeatures, markers: selectedMarkers } @@ -84,13 +83,12 @@ const useSelectionChangeEmitter = (eventBus, selectedFeatures, selectedMarkers, */ export const useInteractionHandlers = ({ mapState, pluginState, services, mapProvider }) => { const { markers, mapSize } = mapState - const { dispatch, layers, interactionModes, multiSelect, contiguous, marker: markerOptions, tolerance, selectedFeatures, selectedMarkers, selectionBounds, deselectOnClickOutside } = pluginState + const { dispatch, layers, interactionModes, multiSelect, marker: markerOptions, tolerance, selectedFeatures, selectedMarkers, selectionBounds, deselectOnClickOutside } = pluginState const { eventBus } = services const layerConfigMap = buildLayerConfigMap(layers) const scale = scaleFactor[mapSize] ?? 1 const processFeatureMatch = useCallback(({ feature, config }) => { markers.remove('location') - const isNewContiguous = contiguous && isContiguousWithAny(feature, selectedFeatures) const featureId = feature.properties?.[config.idProperty] ?? feature.id if (featureId == null) { return @@ -103,11 +101,10 @@ export const useInteractionHandlers = ({ mapState, pluginState, services, mapPro layerId: config.layerId, idProperty: config.idProperty, properties: feature.properties, - geometry: feature.geometry, - replaceAll: contiguous && !isNewContiguous + geometry: feature.geometry } }) - }, [markers, contiguous, selectedFeatures, dispatch, multiSelect]) + }, [markers, dispatch, multiSelect]) const processFallback = useCallback(({ coords }) => { const canPlace = interactionModes.includes('placeMarker') diff --git a/plugins/interact/src/hooks/useInteractionHandlers.test.js b/plugins/interact/src/hooks/useInteractionHandlers.test.js index c184a228..10985d26 100644 --- a/plugins/interact/src/hooks/useInteractionHandlers.test.js +++ b/plugins/interact/src/hooks/useInteractionHandlers.test.js @@ -1,15 +1,11 @@ import { renderHook, act } from '@testing-library/react' import { useInteractionHandlers } from './useInteractionHandlers.js' import * as featureQueries from '../utils/featureQueries.js' -import { isContiguousWithAny } from '../utils/spatial.js' - /* ------------------------------------------------------------------ */ /* Mocks */ /* ------------------------------------------------------------------ */ jest.mock('../utils/spatial.js', () => ({ - isContiguousWithAny: jest.fn(), - canSplitFeatures: jest.fn(() => false), areAllContiguous: jest.fn(() => false) })) jest.mock('../utils/featureQueries.js', () => ({ @@ -48,7 +44,6 @@ const setup = (pluginOverrides = {}, markerItems = [], markerRefs = new Map()) = layers: [{ layerId: 'parcels', idProperty: 'parcelId' }], interactionModes: ['selectMarker', 'selectFeature'], multiSelect: false, - contiguous: false, marker: { symbol: 'pin', backgroundColor: 'red' }, selectedFeatures: [], selectedMarkers: [], @@ -257,58 +252,6 @@ it('passes multiSelect flag through to dispatch', () => { ) }) -/* ------------------------------------------------------------------ */ -/* Contiguous selection (FULL COVERAGE) */ -/* ------------------------------------------------------------------ */ - -describe('contiguous selection', () => { - it('does NOT replace selection when feature is contiguous', () => { - isContiguousWithAny.mockReturnValue(true) // contiguous - - const { result, deps } = setup({ - contiguous: true, - selectedFeatures: [{ geometry: { type: 'Polygon' } }] - }) - - click(result) - - expect(deps.pluginState.dispatch).toHaveBeenCalledWith( - expect.objectContaining({ - payload: expect.objectContaining({ - replaceAll: false - }) - }) - ) - }) - - it('replaces selection when feature is NOT contiguous', () => { - isContiguousWithAny.mockReturnValue(false) // disjoint - - const { result, deps } = setup({ - contiguous: true, - selectedFeatures: [{ geometry: { type: 'Polygon' } }] - }) - - click(result) - - expect(deps.pluginState.dispatch).toHaveBeenCalledWith( - expect.objectContaining({ - payload: expect.objectContaining({ - replaceAll: true - }) - }) - ) - }) - - it('does not compute contiguity when contiguous is false', () => { - const { result } = setup({ contiguous: false }) - - click(result) - - expect(isContiguousWithAny).not.toHaveBeenCalled() - }) -}) - /* ------------------------------------------------------------------ */ /* deselectOnClickOutside */ /* ------------------------------------------------------------------ */ @@ -384,6 +327,23 @@ 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', () => { + 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: {} + } + + 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() } }, @@ -404,8 +364,7 @@ it('emits selectionchange once when bounds exist', () => { selectedFeatures: deps.pluginState.selectedFeatures, selectedMarkers: [], selectionBounds: deps.pluginState.selectionBounds, - canMerge: false, - canSplit: false + contiguous: false }) ) }) diff --git a/plugins/interact/src/reducer.js b/plugins/interact/src/reducer.js index 6da54bdc..ee9063e0 100755 --- a/plugins/interact/src/reducer.js +++ b/plugins/interact/src/reducer.js @@ -4,7 +4,6 @@ const initialState = { marker: null, interactionModes: null, multiSelect: false, - contiguous: false, deselectOnClickOutside: false, selectedFeatures: [], selectedMarkers: [], diff --git a/plugins/interact/src/reducer.test.js b/plugins/interact/src/reducer.test.js index 5d72b74f..147afc7b 100644 --- a/plugins/interact/src/reducer.test.js +++ b/plugins/interact/src/reducer.test.js @@ -8,7 +8,6 @@ describe('initialState', () => { marker: null, interactionModes: null, multiSelect: false, - contiguous: false, deselectOnClickOutside: false, selectedFeatures: [], selectedMarkers: [], diff --git a/plugins/interact/src/utils/spatial.js b/plugins/interact/src/utils/spatial.js index 0b132e67..97fdb969 100644 --- a/plugins/interact/src/utils/spatial.js +++ b/plugins/interact/src/utils/spatial.js @@ -41,20 +41,6 @@ function isContiguousWithAny (feature, features) { return features.some(f => !booleanDisjoint(toTurfGeometry(f), toTurfGeometry(feature))) } -/** - * Check if a single polygon/multipolygon feature can be split. - * - * @param {Array} features - Array of features - * @returns {boolean} True if exactly one polygon or multipolygon feature - */ -function canSplitFeatures (features) { - if (features.length !== 1) { - return false - } - const type = features[0].geometry?.type - return type === 'Polygon' || type === 'MultiPolygon' -} - /** * Check if all features form a single contiguous group (can be merged). * Uses flood-fill to find connected components. @@ -94,6 +80,5 @@ function areAllContiguous (features) { export { toTurfGeometry, isContiguousWithAny, - canSplitFeatures, areAllContiguous } diff --git a/plugins/interact/src/utils/spatial.test.js b/plugins/interact/src/utils/spatial.test.js index ad94f134..d7bcf194 100644 --- a/plugins/interact/src/utils/spatial.test.js +++ b/plugins/interact/src/utils/spatial.test.js @@ -1,4 +1,4 @@ -import { toTurfGeometry, isContiguousWithAny, canSplitFeatures, areAllContiguous } from './spatial.js' +import { toTurfGeometry, isContiguousWithAny, areAllContiguous } from './spatial.js' import { polygon, multiPolygon, lineString, multiLineString, point, multiPoint } from '@turf/helpers' describe('toTurfGeometry', () => { @@ -61,18 +61,6 @@ describe('isContiguousWithAny', () => { }) }) -describe('canSplitFeatures', () => { - it.each([ - [[{ geometry: { type: 'Polygon' } }], true], - [[{ geometry: { type: 'MultiPolygon' } }], true], - [[{ geometry: { type: 'LineString' } }], false], - [[], false], - [[{ geometry: { type: 'Polygon' } }, { geometry: { type: 'Polygon' } }], false] - ])('returns expected result for %j', (features, expected) => { - expect(canSplitFeatures(features)).toBe(expected) - }) -}) - describe('areAllContiguous', () => { const poly = (coords) => ({ geometry: { type: 'Polygon', coordinates: [coords] } }) const A = poly([[0, 0], [2, 0], [2, 2], [0, 2], [0, 0]])