@defra/interactive-map 0.0.17-alpha → 0.0.18-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/context.md +53 -7
- package/docs/api/map-style-config.md +41 -2
- package/docs/api/marker-config.md +53 -11
- package/docs/api/symbol-config.md +160 -0
- package/docs/api/symbol-registry.md +115 -0
- package/docs/api.md +22 -19
- package/docs/plugins/datasets.md +105 -9
- package/docs/plugins/interact.md +68 -43
- package/docs/plugins/search.md +15 -3
- package/package.json +1 -1
- package/plugins/beta/datasets/dist/css/index.css +32 -14
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/esm/index.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/index.js +1 -1
- package/plugins/beta/datasets/src/DatasetsInit.jsx +9 -4
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +57 -11
- package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +14 -8
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +155 -53
- package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +27 -0
- package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +31 -0
- package/plugins/beta/datasets/src/api/addDataset.js +1 -1
- package/plugins/beta/datasets/src/api/setData.js +4 -2
- package/plugins/beta/datasets/src/api/setStyle.js +2 -2
- package/plugins/beta/datasets/src/components/EmptyKey.jsx +7 -0
- package/plugins/beta/datasets/src/components/EmptyKey.test.jsx +21 -0
- package/plugins/beta/datasets/src/components/KeySvg.jsx +24 -0
- package/plugins/beta/datasets/src/components/KeySvgLine.jsx +19 -0
- package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +15 -0
- package/plugins/beta/datasets/src/components/KeySvgRect.jsx +22 -0
- package/plugins/beta/datasets/src/components/KeySvgSymbol.jsx +16 -0
- package/plugins/beta/datasets/src/components/svgProperties.js +20 -0
- package/plugins/beta/datasets/src/datasets.js +13 -4
- package/plugins/beta/datasets/src/defaults.js +4 -2
- package/plugins/beta/datasets/src/index.js +2 -1
- package/plugins/beta/datasets/src/manifest.js +1 -1
- package/plugins/beta/datasets/src/panels/Key.jsx +11 -89
- package/plugins/beta/datasets/src/panels/Key.module.scss +24 -13
- package/plugins/beta/datasets/src/panels/Layers.module.scss +13 -7
- package/plugins/beta/datasets/src/reducer.js +6 -0
- package/plugins/beta/datasets/src/reducers/keyReducer.js +34 -0
- package/plugins/beta/datasets/src/utils/mergeSublayer.js +8 -0
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/DrawInit.jsx +3 -2
- package/plugins/beta/draw-ml/dist/css/index.css +21 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/index.js +1 -1
- package/plugins/beta/draw-ml/src/DrawInit.jsx +4 -3
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/beta/map-styles/src/MapStyles.jsx +5 -4
- package/plugins/beta/map-styles/src/MapStylesInit.jsx +5 -4
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +14 -5
- package/plugins/interact/src/InteractInit.test.js +26 -6
- package/plugins/interact/src/api/enable.test.js +7 -7
- package/plugins/interact/src/defaults.js +4 -6
- package/plugins/interact/src/events.js +9 -6
- package/plugins/interact/src/events.test.js +28 -4
- package/plugins/interact/src/hooks/useHighlightSync.js +3 -3
- package/plugins/interact/src/hooks/useHighlightSync.test.js +6 -6
- package/plugins/interact/src/hooks/useHoverCursor.js +10 -0
- package/plugins/interact/src/hooks/useHoverCursor.test.js +44 -0
- package/plugins/interact/src/hooks/useInteractionHandlers.js +111 -69
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +147 -32
- package/plugins/interact/src/reducer.js +23 -4
- package/plugins/interact/src/reducer.test.js +60 -11
- package/plugins/interact/src/utils/buildStylesMap.js +17 -4
- package/plugins/interact/src/utils/buildStylesMap.test.js +16 -2
- package/plugins/interact/src/utils/featureQueries.js +11 -6
- package/plugins/interact/src/utils/featureQueries.test.js +8 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/src/Search.jsx +3 -1
- package/plugins/search/src/events/fetchSuggestions.js +6 -4
- package/plugins/search/src/events/fetchSuggestions.test.js +26 -4
- package/plugins/search/src/events/formHandlers.js +3 -3
- package/plugins/search/src/events/formHandlers.test.js +1 -1
- package/plugins/search/src/events/suggestionHandlers.js +2 -2
- package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
- package/plugins/search/src/utils/updateMap.js +3 -3
- package/plugins/search/src/utils/updateMap.test.js +3 -3
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/appEvents.js +7 -0
- package/providers/maplibre/src/appEvents.test.js +18 -4
- package/providers/maplibre/src/maplibreProvider.js +52 -0
- package/providers/maplibre/src/maplibreProvider.test.js +105 -1
- package/providers/maplibre/src/utils/highlightFeatures.js +36 -7
- package/providers/maplibre/src/utils/highlightFeatures.test.js +153 -96
- package/providers/maplibre/src/utils/hoverCursor.js +61 -0
- package/providers/maplibre/src/utils/hoverCursor.test.js +130 -0
- package/providers/maplibre/src/utils/patternImages.js +70 -0
- package/providers/maplibre/src/utils/patternImages.test.js +180 -0
- package/providers/maplibre/src/utils/queryFeatures.js +38 -16
- package/providers/maplibre/src/utils/queryFeatures.test.js +20 -3
- package/providers/maplibre/src/utils/rasteriseToImageData.js +30 -0
- package/providers/maplibre/src/utils/rasteriseToImageData.test.js +69 -0
- package/providers/maplibre/src/utils/symbolImages.js +147 -0
- package/providers/maplibre/src/utils/symbolImages.test.js +248 -0
- package/src/App/components/Markers/Markers.jsx +122 -27
- package/src/App/components/Markers/Markers.module.scss +0 -10
- package/src/App/components/Markers/Markers.test.jsx +246 -0
- package/src/App/hooks/useInterfaceAPI.test.js +156 -0
- package/src/App/hooks/useMarkersAPI.js +2 -5
- package/src/App/hooks/useMarkersAPI.test.js +4 -4
- package/src/App/layout/Layout.jsx +2 -2
- package/src/App/layout/Layout.test.jsx +4 -2
- package/src/App/store/ServiceProvider.jsx +7 -5
- package/src/App/store/mapActionsMap.js +4 -6
- package/src/App/store/mapActionsMap.test.js +3 -2
- package/src/App/store/mapReducer.js +2 -1
- package/src/config/appConfig.js +0 -6
- package/src/config/appConfig.test.js +1 -2
- package/src/config/defaults.js +0 -2
- package/src/config/mapTheme.js +56 -0
- package/src/config/patternConfig.js +16 -0
- package/src/config/symbolConfig.js +80 -0
- package/src/scss/settings/_colors.scss +0 -9
- package/src/services/patternRegistry.js +40 -0
- package/src/services/patternRegistry.test.js +48 -0
- package/src/services/symbolRegistry.js +113 -0
- package/src/services/symbolRegistry.test.js +262 -0
- package/src/types.js +93 -11
- package/src/utils/patternUtils.js +94 -0
- package/src/utils/patternUtils.test.js +160 -0
- package/src/utils/symbolUtils.js +85 -0
- package/src/utils/symbolUtils.test.js +156 -0
- package/plugins/beta/datasets/src/adapters/maplibre/patternRegistry.js +0 -48
- package/plugins/beta/datasets/src/styles/patterns.js +0 -157
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { EVENTS } from '../../../src/config/events.js'
|
|
2
3
|
import { useInteractionHandlers } from './hooks/useInteractionHandlers.js'
|
|
3
4
|
import { useHighlightSync } from './hooks/useHighlightSync.js'
|
|
5
|
+
import { useHoverCursor } from './hooks/useHoverCursor.js'
|
|
4
6
|
import { attachEvents } from './events.js'
|
|
5
7
|
|
|
6
8
|
export const InteractInit = ({
|
|
@@ -12,8 +14,8 @@ export const InteractInit = ({
|
|
|
12
14
|
pluginState
|
|
13
15
|
}) => {
|
|
14
16
|
const { interfaceType } = appState
|
|
15
|
-
const { dispatch, enabled, selectedFeatures, selectionBounds } = pluginState
|
|
16
|
-
const {
|
|
17
|
+
const { dispatch, enabled, selectedFeatures, selectionBounds, interactionModes, layers } = pluginState
|
|
18
|
+
const { eventBus, closeApp } = services
|
|
17
19
|
const { crossHair, mapStyle } = mapState
|
|
18
20
|
|
|
19
21
|
const isTouchOrKeyboard = ['touch', 'keyboard'].includes(interfaceType)
|
|
@@ -56,10 +58,17 @@ export const InteractInit = ({
|
|
|
56
58
|
selectedFeatures,
|
|
57
59
|
selectionBounds,
|
|
58
60
|
dispatch,
|
|
59
|
-
events,
|
|
61
|
+
events: EVENTS,
|
|
60
62
|
eventBus
|
|
61
63
|
})
|
|
62
64
|
|
|
65
|
+
// Notify other components (e.g. Markers) whether interact is active
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
eventBus.emit('interact:active', { active: enabled, interactionModes })
|
|
68
|
+
}, [enabled, interactionModes])
|
|
69
|
+
|
|
70
|
+
useHoverCursor(mapProvider, enabled, interactionModes, layers)
|
|
71
|
+
|
|
63
72
|
// Toggle target marker visibility
|
|
64
73
|
useEffect(() => {
|
|
65
74
|
if (enabled && isTouchOrKeyboard) {
|
|
@@ -79,7 +88,7 @@ export const InteractInit = ({
|
|
|
79
88
|
mapState,
|
|
80
89
|
getPluginState: () => pluginStateRef.current,
|
|
81
90
|
buttonConfig,
|
|
82
|
-
events,
|
|
91
|
+
events: EVENTS,
|
|
83
92
|
eventBus,
|
|
84
93
|
handleInteraction: (e) => handleInteractionRef.current(e),
|
|
85
94
|
clickReadyRef,
|
|
@@ -87,7 +96,7 @@ export const InteractInit = ({
|
|
|
87
96
|
})
|
|
88
97
|
|
|
89
98
|
return cleanupEvents
|
|
90
|
-
}, [pluginState.enabled, buttonConfig,
|
|
99
|
+
}, [pluginState.enabled, buttonConfig, eventBus, closeApp])
|
|
91
100
|
|
|
92
101
|
return null
|
|
93
102
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { act, render } from '@testing-library/react'
|
|
2
|
+
import { EVENTS } from '../../../src/config/events.js'
|
|
2
3
|
import { InteractInit } from './InteractInit.jsx'
|
|
3
4
|
import { useInteractionHandlers } from './hooks/useInteractionHandlers.js'
|
|
4
5
|
import { useHighlightSync } from './hooks/useHighlightSync.js'
|
|
6
|
+
import { useHoverCursor } from './hooks/useHoverCursor.js'
|
|
5
7
|
import { attachEvents } from './events.js'
|
|
6
8
|
|
|
7
9
|
jest.mock('./hooks/useInteractionHandlers.js')
|
|
8
10
|
jest.mock('./hooks/useHighlightSync.js')
|
|
11
|
+
jest.mock('./hooks/useHoverCursor.js')
|
|
9
12
|
jest.mock('./events.js')
|
|
10
13
|
|
|
11
14
|
describe('InteractInit', () => {
|
|
@@ -19,15 +22,24 @@ describe('InteractInit', () => {
|
|
|
19
22
|
|
|
20
23
|
useInteractionHandlers.mockReturnValue({ handleInteraction: handleInteractionMock })
|
|
21
24
|
useHighlightSync.mockReturnValue(undefined)
|
|
25
|
+
useHoverCursor.mockReturnValue(undefined)
|
|
22
26
|
attachEvents.mockReturnValue(cleanupMock)
|
|
23
27
|
|
|
24
28
|
props = {
|
|
25
|
-
appState: { interfaceType: 'mouse' },
|
|
29
|
+
appState: { interfaceType: 'mouse', layoutRefs: { viewportRef: { current: null } } },
|
|
26
30
|
mapState: { crossHair: { fixAtCenter: jest.fn(), hide: jest.fn() }, mapStyle: {} },
|
|
27
|
-
services: {
|
|
31
|
+
services: { eventBus: { emit: jest.fn() }, closeApp: jest.fn() },
|
|
28
32
|
buttonConfig: {},
|
|
29
|
-
mapProvider: {},
|
|
30
|
-
pluginState: {
|
|
33
|
+
mapProvider: { setHoverCursor: jest.fn() },
|
|
34
|
+
pluginState: {
|
|
35
|
+
dispatch: jest.fn(),
|
|
36
|
+
enabled: true,
|
|
37
|
+
selectedFeatures: [],
|
|
38
|
+
selectedMarkers: [],
|
|
39
|
+
selectionBounds: {},
|
|
40
|
+
interactionModes: ['selectFeature'],
|
|
41
|
+
layers: []
|
|
42
|
+
}
|
|
31
43
|
}
|
|
32
44
|
})
|
|
33
45
|
|
|
@@ -50,7 +62,7 @@ describe('InteractInit', () => {
|
|
|
50
62
|
pluginState: props.pluginState,
|
|
51
63
|
selectedFeatures: props.pluginState.selectedFeatures,
|
|
52
64
|
dispatch: props.pluginState.dispatch,
|
|
53
|
-
events:
|
|
65
|
+
events: EVENTS,
|
|
54
66
|
eventBus: props.services.eventBus
|
|
55
67
|
}))
|
|
56
68
|
})
|
|
@@ -75,7 +87,7 @@ describe('InteractInit', () => {
|
|
|
75
87
|
handleInteraction: expect.any(Function),
|
|
76
88
|
mapState: props.mapState,
|
|
77
89
|
buttonConfig: props.buttonConfig,
|
|
78
|
-
events:
|
|
90
|
+
events: EVENTS,
|
|
79
91
|
eventBus: props.services.eventBus,
|
|
80
92
|
closeApp: props.services.closeApp
|
|
81
93
|
}))
|
|
@@ -92,6 +104,14 @@ describe('InteractInit', () => {
|
|
|
92
104
|
expect(cleanupMock).toHaveBeenCalled()
|
|
93
105
|
})
|
|
94
106
|
|
|
107
|
+
it('emits interact:active with active state and interactionModes on enable', () => {
|
|
108
|
+
render(<InteractInit {...props} />)
|
|
109
|
+
expect(props.services.eventBus.emit).toHaveBeenCalledWith('interact:active', {
|
|
110
|
+
active: true,
|
|
111
|
+
interactionModes: props.pluginState.interactionModes
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
95
115
|
it('enables click handling after a macrotask', () => {
|
|
96
116
|
jest.useFakeTimers()
|
|
97
117
|
render(<InteractInit {...props} />)
|
|
@@ -9,8 +9,8 @@ describe('enable', () => {
|
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
it('dispatches ENABLE with merged payload correctly', () => {
|
|
12
|
-
const pluginConfig = {
|
|
13
|
-
const options = {
|
|
12
|
+
const pluginConfig = { marker: { symbol: 'pin', backgroundColor: 'blue' } }
|
|
13
|
+
const options = { interactionModes: ['selectFeature'], marker: { symbol: 'circle', backgroundColor: 'green' }, layers: [{ layerId: 'test' }] }
|
|
14
14
|
|
|
15
15
|
enable({ pluginState: { dispatch }, pluginConfig }, options)
|
|
16
16
|
|
|
@@ -18,16 +18,16 @@ describe('enable', () => {
|
|
|
18
18
|
expect(dispatch).toHaveBeenCalledWith({
|
|
19
19
|
type: 'ENABLE',
|
|
20
20
|
payload: expect.objectContaining({
|
|
21
|
-
|
|
22
|
-
multiSelect: DEFAULTS.multiSelect,
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
interactionModes: ['selectFeature'],
|
|
22
|
+
multiSelect: DEFAULTS.multiSelect,
|
|
23
|
+
marker: { symbol: 'circle', backgroundColor: 'green' },
|
|
24
|
+
layers: [{ layerId: 'test' }]
|
|
25
25
|
})
|
|
26
26
|
})
|
|
27
27
|
})
|
|
28
28
|
|
|
29
29
|
it('handles empty or undefined options', () => {
|
|
30
|
-
const pluginConfig = {
|
|
30
|
+
const pluginConfig = { marker: { symbol: 'pin', backgroundColor: 'blue' } }
|
|
31
31
|
|
|
32
32
|
enable({ pluginState: { dispatch }, pluginConfig }, {})
|
|
33
33
|
expect(dispatch).toHaveBeenCalledTimes(1)
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
export const DEFAULTS = {
|
|
2
|
-
tolerance: 10,
|
|
3
|
-
|
|
2
|
+
tolerance: 10,
|
|
3
|
+
interactionModes: ['selectMarker'],
|
|
4
4
|
multiSelect: false,
|
|
5
5
|
contiguous: false,
|
|
6
6
|
deselectOnClickOutside: false,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
selectedFill: 'rgba(255, 0, 0, 0.1)',
|
|
10
|
-
selectedStrokeWidth: 2
|
|
7
|
+
marker: {},
|
|
8
|
+
selectedStrokeWidth: 3
|
|
11
9
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
const buildDonePayload = (coords, selectedFeatures, selectedMarkers, selectionBounds) => ({
|
|
2
|
+
...(coords && { coords }),
|
|
3
|
+
...(!coords && selectedFeatures && { selectedFeatures }),
|
|
4
|
+
...(!coords && selectedMarkers?.length && { selectedMarkers }),
|
|
5
|
+
...(!coords && selectionBounds && { selectionBounds })
|
|
6
|
+
})
|
|
7
|
+
|
|
1
8
|
// Helper for feature toggling logic
|
|
2
9
|
const createFeatureHandler = (mapState, getPluginState) => (args, addToExisting) => {
|
|
3
10
|
const pluginState = getPluginState()
|
|
@@ -48,17 +55,13 @@ export function attachEvents ({
|
|
|
48
55
|
const pluginState = getPluginState()
|
|
49
56
|
const marker = mapState.markers.getMarker('location')
|
|
50
57
|
const { coords } = marker || {}
|
|
51
|
-
const { selectionBounds, selectedFeatures } = pluginState
|
|
58
|
+
const { selectionBounds, selectedFeatures, selectedMarkers } = pluginState
|
|
52
59
|
|
|
53
60
|
if (getAppState().disabledButtons.has('selectDone')) {
|
|
54
61
|
return
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
eventBus.emit('interact:done',
|
|
58
|
-
...(coords && { coords }),
|
|
59
|
-
...(!coords && selectedFeatures && { selectedFeatures }),
|
|
60
|
-
...(!coords && selectionBounds && { selectionBounds })
|
|
61
|
-
})
|
|
64
|
+
eventBus.emit('interact:done', buildDonePayload(coords, selectedFeatures, selectedMarkers, selectionBounds))
|
|
62
65
|
|
|
63
66
|
if (pluginState.closeOnAction ?? true) {
|
|
64
67
|
closeApp()
|
|
@@ -172,14 +172,11 @@ describe('attachEvents', () => {
|
|
|
172
172
|
Object.values(params.buttonConfig).forEach(btn => expect(btn.onClick).toBeNull())
|
|
173
173
|
})
|
|
174
174
|
|
|
175
|
-
it('selectDone
|
|
175
|
+
it('selectDone emits selectedFeatures and selectionBounds when no marker/coords', () => {
|
|
176
176
|
const params = createParams()
|
|
177
177
|
cleanup = attachEvents(params)
|
|
178
178
|
|
|
179
|
-
// Ensure marker returns null (no coords)
|
|
180
179
|
params.mapState.markers.getMarker.mockReturnValue(null)
|
|
181
|
-
|
|
182
|
-
// Set up features and bounds
|
|
183
180
|
params.pluginState.selectedFeatures = [{ id: 'f1' }]
|
|
184
181
|
params.pluginState.selectionBounds = { sw: [0, 0], ne: [1, 1] }
|
|
185
182
|
|
|
@@ -191,6 +188,33 @@ describe('attachEvents', () => {
|
|
|
191
188
|
})
|
|
192
189
|
})
|
|
193
190
|
|
|
191
|
+
it('selectDone includes selectedMarkers in payload when present', () => {
|
|
192
|
+
const params = createParams()
|
|
193
|
+
cleanup = attachEvents(params)
|
|
194
|
+
|
|
195
|
+
params.mapState.markers.getMarker.mockReturnValue(null)
|
|
196
|
+
params.pluginState.selectedMarkers = ['m1', 'm2']
|
|
197
|
+
|
|
198
|
+
params.buttonConfig.selectDone.onClick()
|
|
199
|
+
|
|
200
|
+
expect(params.eventBus.emit).toHaveBeenCalledWith('interact:done',
|
|
201
|
+
expect.objectContaining({ selectedMarkers: ['m1', 'm2'] })
|
|
202
|
+
)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('selectDone omits selectedMarkers from payload when empty', () => {
|
|
206
|
+
const params = createParams()
|
|
207
|
+
cleanup = attachEvents(params)
|
|
208
|
+
|
|
209
|
+
params.mapState.markers.getMarker.mockReturnValue(null)
|
|
210
|
+
params.pluginState.selectedMarkers = []
|
|
211
|
+
|
|
212
|
+
params.buttonConfig.selectDone.onClick()
|
|
213
|
+
|
|
214
|
+
const payload = params.eventBus.emit.mock.calls.find(c => c[0] === 'interact:done')[1]
|
|
215
|
+
expect(payload).not.toHaveProperty('selectedMarkers')
|
|
216
|
+
})
|
|
217
|
+
|
|
194
218
|
it('respects default closeOnAction when value is undefined (fallback to true)', () => {
|
|
195
219
|
const params = createParams()
|
|
196
220
|
// Explicitly set to undefined to trigger the ?? fallback
|
|
@@ -10,15 +10,15 @@ export const useHighlightSync = ({
|
|
|
10
10
|
events,
|
|
11
11
|
eventBus
|
|
12
12
|
}) => {
|
|
13
|
-
const {
|
|
13
|
+
const { layers } = pluginState
|
|
14
14
|
|
|
15
15
|
// Memoize stylesMap so it only recalculates when style or layers change
|
|
16
16
|
const stylesMap = useMemo(() => {
|
|
17
17
|
if (!mapStyle) {
|
|
18
18
|
return null
|
|
19
19
|
}
|
|
20
|
-
return buildStylesMap(
|
|
21
|
-
}, [
|
|
20
|
+
return buildStylesMap(layers, mapStyle)
|
|
21
|
+
}, [layers, mapStyle])
|
|
22
22
|
|
|
23
23
|
// Force re-application of all selected features
|
|
24
24
|
const updateHighlightedFeatures = () => {
|
|
@@ -23,7 +23,7 @@ describe('useHighlightSync', () => {
|
|
|
23
23
|
},
|
|
24
24
|
mapStyle: { id: 'default-style' },
|
|
25
25
|
pluginState: {
|
|
26
|
-
|
|
26
|
+
layers: [{ layerId: 'layer1' }]
|
|
27
27
|
},
|
|
28
28
|
selectedFeatures: [],
|
|
29
29
|
dispatch: jest.fn(),
|
|
@@ -93,21 +93,21 @@ describe('useHighlightSync', () => {
|
|
|
93
93
|
)
|
|
94
94
|
})
|
|
95
95
|
|
|
96
|
-
it('rebuilds styles when
|
|
96
|
+
it('rebuilds styles when layers change', () => {
|
|
97
97
|
mockDeps.selectedFeatures = [{ featureId: 'F1' }]
|
|
98
98
|
|
|
99
99
|
const { rerender } = renderHook(
|
|
100
|
-
({
|
|
100
|
+
({ layers }) =>
|
|
101
101
|
useHighlightSync({
|
|
102
102
|
...mockDeps,
|
|
103
|
-
pluginState: {
|
|
103
|
+
pluginState: { layers }
|
|
104
104
|
}),
|
|
105
|
-
{ initialProps: {
|
|
105
|
+
{ initialProps: { layers: [{ layerId: 'layer1' }] } }
|
|
106
106
|
)
|
|
107
107
|
|
|
108
108
|
buildStylesMap.mockClear()
|
|
109
109
|
|
|
110
|
-
rerender({
|
|
110
|
+
rerender({ layers: [{ layerId: 'layer1' }, { layerId: 'layer2' }] })
|
|
111
111
|
|
|
112
112
|
expect(buildStylesMap).toHaveBeenCalled()
|
|
113
113
|
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
export const useHoverCursor = (mapProvider, enabled, interactionModes, layers) => {
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
const canSelect = enabled && interactionModes?.includes('selectFeature')
|
|
6
|
+
const layerIds = canSelect ? layers.map(l => l.layerId) : []
|
|
7
|
+
mapProvider.setHoverCursor?.(layerIds)
|
|
8
|
+
return () => mapProvider.setHoverCursor?.([])
|
|
9
|
+
}, [enabled, interactionModes, layers])
|
|
10
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react'
|
|
2
|
+
import { useHoverCursor } from './useHoverCursor.js'
|
|
3
|
+
|
|
4
|
+
const makeProvider = () => ({ setHoverCursor: jest.fn() })
|
|
5
|
+
|
|
6
|
+
describe('useHoverCursor', () => {
|
|
7
|
+
const dataLayers = [{ layerId: 'layer-a' }, { layerId: 'layer-b' }]
|
|
8
|
+
|
|
9
|
+
it('calls setHoverCursor with layer IDs when enabled with selectFeature mode', () => {
|
|
10
|
+
const mapProvider = makeProvider()
|
|
11
|
+
renderHook(() => useHoverCursor(mapProvider, true, ['selectFeature'], dataLayers))
|
|
12
|
+
expect(mapProvider.setHoverCursor).toHaveBeenCalledWith(['layer-a', 'layer-b'])
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('calls setHoverCursor with layer IDs when selectFeature is combined with other interactionModes', () => {
|
|
16
|
+
const mapProvider = makeProvider()
|
|
17
|
+
renderHook(() => useHoverCursor(mapProvider, true, ['selectMarker', 'selectFeature'], dataLayers))
|
|
18
|
+
expect(mapProvider.setHoverCursor).toHaveBeenCalledWith(['layer-a', 'layer-b'])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('calls setHoverCursor with empty array when disabled', () => {
|
|
22
|
+
const mapProvider = makeProvider()
|
|
23
|
+
renderHook(() => useHoverCursor(mapProvider, false, ['selectFeature'], dataLayers))
|
|
24
|
+
expect(mapProvider.setHoverCursor).toHaveBeenCalledWith([])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('calls setHoverCursor with empty array when selectFeature is not in interactionModes', () => {
|
|
28
|
+
const mapProvider = makeProvider()
|
|
29
|
+
renderHook(() => useHoverCursor(mapProvider, true, ['selectMarker', 'placeMarker'], dataLayers))
|
|
30
|
+
expect(mapProvider.setHoverCursor).toHaveBeenCalledWith([])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('clears cursor on unmount', () => {
|
|
34
|
+
const mapProvider = makeProvider()
|
|
35
|
+
const { unmount } = renderHook(() => useHoverCursor(mapProvider, true, ['selectFeature'], dataLayers))
|
|
36
|
+
mapProvider.setHoverCursor.mockClear()
|
|
37
|
+
unmount()
|
|
38
|
+
expect(mapProvider.setHoverCursor).toHaveBeenCalledWith([])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('does not throw when setHoverCursor is absent', () => {
|
|
42
|
+
expect(() => renderHook(() => useHoverCursor({}, true, ['selectFeature'], dataLayers))).not.toThrow()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -1,8 +1,42 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from 'react'
|
|
2
2
|
import { isContiguousWithAny, canSplitFeatures, areAllContiguous } from '../utils/spatial.js'
|
|
3
3
|
import { getFeaturesAtPoint, findMatchingFeature, buildLayerConfigMap } from '../utils/featureQueries.js'
|
|
4
|
+
import { scaleFactor } from '../../../../src/config/appConfig.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the id of the first DOM marker whose visual bounds contain the given point.
|
|
8
|
+
*
|
|
9
|
+
* MAP_CLICK point is container-relative; getBoundingClientRect is viewport-relative.
|
|
10
|
+
* We convert by subtracting the parent element's top-left (markers share a parent with
|
|
11
|
+
* the map container, so parentElement.getBoundingClientRect() gives the offset).
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} markers - markers object from mapState (has .items and .markerRefs)
|
|
14
|
+
* @param {{ x: number, y: number }} point - container-relative pixel coordinates
|
|
15
|
+
* @param {number} scale - scaleFactor for the current mapSize (e.g. 1.5 for medium)
|
|
16
|
+
* @returns {string|null}
|
|
17
|
+
*/
|
|
18
|
+
const findMarkerAtPoint = (markers, point, scale) => {
|
|
19
|
+
for (const marker of markers.items) {
|
|
20
|
+
const el = markers.markerRefs?.get(marker.id)
|
|
21
|
+
if (!el) {
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
const parent = el.parentElement
|
|
25
|
+
const parentRect = parent ? parent.getBoundingClientRect() : { left: 0, top: 0 }
|
|
26
|
+
const { left, top, right, bottom } = el.getBoundingClientRect()
|
|
27
|
+
const scaledX = point.x * scale
|
|
28
|
+
const scaledY = point.y * scale
|
|
29
|
+
if (
|
|
30
|
+
scaledX >= left - parentRect.left && scaledX <= right - parentRect.left &&
|
|
31
|
+
scaledY >= top - parentRect.top && scaledY <= bottom - parentRect.top
|
|
32
|
+
) {
|
|
33
|
+
return marker.id
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
4
38
|
|
|
5
|
-
const useSelectionChangeEmitter = (eventBus, selectedFeatures, selectionBounds) => {
|
|
39
|
+
const useSelectionChangeEmitter = (eventBus, selectedFeatures, selectedMarkers, selectionBounds) => {
|
|
6
40
|
const lastEmittedSelectionChange = useRef(null)
|
|
7
41
|
|
|
8
42
|
useEffect(() => {
|
|
@@ -14,105 +48,113 @@ const useSelectionChangeEmitter = (eventBus, selectedFeatures, selectionBounds)
|
|
|
14
48
|
|
|
15
49
|
// Skip if selection was already empty and remains empty
|
|
16
50
|
const prev = lastEmittedSelectionChange.current
|
|
17
|
-
const wasEmpty = prev === null || prev.length === 0
|
|
18
|
-
if (wasEmpty && selectedFeatures.length === 0) {
|
|
51
|
+
const wasEmpty = prev === null || (prev.features.length === 0 && prev.markers.length === 0)
|
|
52
|
+
if (wasEmpty && selectedFeatures.length === 0 && selectedMarkers.length === 0) {
|
|
19
53
|
return
|
|
20
54
|
}
|
|
21
55
|
|
|
22
56
|
eventBus.emit('interact:selectionchange', {
|
|
23
57
|
selectedFeatures,
|
|
58
|
+
selectedMarkers,
|
|
24
59
|
selectionBounds,
|
|
25
60
|
canMerge: areAllContiguous(selectedFeatures),
|
|
26
61
|
canSplit: canSplitFeatures(selectedFeatures)
|
|
27
62
|
})
|
|
28
63
|
|
|
29
|
-
lastEmittedSelectionChange.current = selectedFeatures
|
|
30
|
-
}, [selectedFeatures, selectionBounds])
|
|
64
|
+
lastEmittedSelectionChange.current = { features: selectedFeatures, markers: selectedMarkers }
|
|
65
|
+
}, [selectedFeatures, selectedMarkers, selectionBounds])
|
|
31
66
|
}
|
|
32
67
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Core interaction hook. Processes map clicks in fixed priority order:
|
|
70
|
+
* selectMarker → selectFeature → placeMarker (fallback).
|
|
71
|
+
*
|
|
72
|
+
* Which steps are active is controlled by `pluginState.interactionModes`. Steps not
|
|
73
|
+
* present in the array are skipped entirely — e.g. omitting `'selectMarker'` means
|
|
74
|
+
* marker hit-testing is never performed.
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} deps
|
|
77
|
+
* @param {Object} deps.mapState - Map state including markers and mapSize
|
|
78
|
+
* @param {Object} deps.pluginState - Plugin state including interactionModes, layers, etc.
|
|
79
|
+
* @param {Object} deps.services - Services including eventBus
|
|
80
|
+
* @param {Object} deps.mapProvider - Map provider instance for feature queries
|
|
81
|
+
* @returns {{ handleInteraction: Function }}
|
|
82
|
+
*/
|
|
83
|
+
export const useInteractionHandlers = ({ mapState, pluginState, services, mapProvider }) => {
|
|
84
|
+
const { markers, mapSize } = mapState
|
|
85
|
+
const { dispatch, layers, interactionModes, multiSelect, contiguous, marker: markerOptions, tolerance, selectedFeatures, selectedMarkers, selectionBounds, deselectOnClickOutside } = pluginState
|
|
41
86
|
const { eventBus } = services
|
|
42
|
-
const layerConfigMap = buildLayerConfigMap(
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
87
|
+
const layerConfigMap = buildLayerConfigMap(layers)
|
|
88
|
+
const scale = scaleFactor[mapSize] ?? 1
|
|
89
|
+
const processFeatureMatch = useCallback(({ feature, config }) => {
|
|
90
|
+
markers.remove('location')
|
|
91
|
+
const isNewContiguous = contiguous && isContiguousWithAny(feature, selectedFeatures)
|
|
92
|
+
const featureId = feature.properties?.[config.idProperty] ?? feature.id
|
|
93
|
+
if (featureId == null) {
|
|
94
|
+
return
|
|
50
95
|
}
|
|
96
|
+
dispatch({
|
|
97
|
+
type: 'TOGGLE_SELECTED_FEATURES',
|
|
98
|
+
payload: {
|
|
99
|
+
featureId,
|
|
100
|
+
multiSelect,
|
|
101
|
+
layerId: config.layerId,
|
|
102
|
+
idProperty: config.idProperty,
|
|
103
|
+
properties: feature.properties,
|
|
104
|
+
geometry: feature.geometry,
|
|
105
|
+
replaceAll: contiguous && !isNewContiguous
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}, [markers, contiguous, selectedFeatures, dispatch, multiSelect])
|
|
51
109
|
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
// 1. Handle Feature Match
|
|
56
|
-
if (match) {
|
|
57
|
-
processFeatureMatch(match)
|
|
110
|
+
const processFallback = useCallback(({ coords }) => {
|
|
111
|
+
const canPlace = interactionModes.includes('placeMarker')
|
|
112
|
+
if (!canPlace && !deselectOnClickOutside) {
|
|
58
113
|
return
|
|
59
114
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (isMarkerMode) {
|
|
64
|
-
dispatch({ type: 'CLEAR_SELECTED_FEATURES' })
|
|
65
|
-
markers.add('location', coords, { color: markerColor })
|
|
115
|
+
dispatch({ type: 'CLEAR_SELECTED_FEATURES' })
|
|
116
|
+
if (canPlace) {
|
|
117
|
+
markers.add('location', coords, markerOptions)
|
|
66
118
|
eventBus.emit('interact:markerchange', { coords })
|
|
67
|
-
} else if (deselectOnClickOutside) {
|
|
68
|
-
dispatch({ type: 'CLEAR_SELECTED_FEATURES' })
|
|
69
|
-
} else {
|
|
70
|
-
// No action
|
|
71
119
|
}
|
|
120
|
+
}, [interactionModes, dispatch, markers, markerOptions, eventBus, deselectOnClickOutside])
|
|
72
121
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
markers
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (!featureId) {
|
|
122
|
+
const handleInteraction = useCallback(({ point, coords }) => {
|
|
123
|
+
if (interactionModes.includes('selectMarker')) {
|
|
124
|
+
const markerHit = findMarkerAtPoint(markers, point, scale)
|
|
125
|
+
if (markerHit) {
|
|
126
|
+
dispatch({ type: 'TOGGLE_SELECTED_MARKERS', payload: { markerId: markerHit, multiSelect } })
|
|
80
127
|
return
|
|
81
128
|
}
|
|
129
|
+
}
|
|
82
130
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
})
|
|
131
|
+
if (interactionModes.includes('selectFeature') && layers.length > 0) {
|
|
132
|
+
const allFeatures = getFeaturesAtPoint(mapProvider, point, { radius: tolerance })
|
|
133
|
+
if (pluginState?.debug) {
|
|
134
|
+
console.log(`--- Features at ${coords} ---`, allFeatures)
|
|
135
|
+
}
|
|
136
|
+
const match = findMatchingFeature(allFeatures, layerConfigMap)
|
|
137
|
+
if (match) {
|
|
138
|
+
processFeatureMatch(match)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
95
141
|
}
|
|
142
|
+
|
|
143
|
+
processFallback({ coords })
|
|
96
144
|
}, [
|
|
97
145
|
mapProvider,
|
|
98
|
-
|
|
99
|
-
|
|
146
|
+
layers,
|
|
147
|
+
interactionModes,
|
|
100
148
|
multiSelect,
|
|
101
|
-
eventBus,
|
|
102
149
|
dispatch,
|
|
103
150
|
markers,
|
|
104
|
-
contiguous,
|
|
105
|
-
selectedFeatures,
|
|
106
151
|
layerConfigMap,
|
|
107
152
|
pluginState?.debug,
|
|
108
153
|
tolerance,
|
|
109
|
-
|
|
110
|
-
|
|
154
|
+
processFeatureMatch,
|
|
155
|
+
processFallback,
|
|
156
|
+
scale
|
|
111
157
|
])
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
handleInteraction
|
|
117
|
-
}
|
|
158
|
+
useSelectionChangeEmitter(eventBus, selectedFeatures, selectedMarkers, selectionBounds)
|
|
159
|
+
return { handleInteraction }
|
|
118
160
|
}
|