@defra/interactive-map 0.0.17-alpha → 0.0.19-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/assets/css/docusaurus.css +58 -34
- 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/panel-definition.md +16 -0
- package/docs/api/symbol-config.md +160 -0
- package/docs/api/symbol-registry.md +115 -0
- package/docs/api.md +50 -23
- package/docs/assets/basic-map.jpg +0 -0
- package/docs/assets/button-first.jpg +0 -0
- package/docs/assets/maker-panel.jpg +0 -0
- package/docs/examples/add-marker-with-panel.mdx +59 -0
- package/docs/examples/basic-map.mdx +24 -0
- package/docs/examples/button-map.mdx +24 -0
- package/docs/examples/index.mdx +49 -0
- package/docs/index.mdx +1 -1
- package/docs/plugins/datasets.md +105 -9
- package/docs/plugins/interact.md +100 -44
- package/docs/plugins/search.md +15 -3
- package/docs/plugins.md +1 -1
- package/docusaurus.config.cjs +9 -1
- 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 +3 -0
- 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/beta/scale-bar/dist/css/index.css +1 -1
- package/plugins/beta/scale-bar/src/scaleBar.scss +1 -0
- 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 +19 -8
- package/plugins/interact/src/InteractInit.test.js +26 -6
- package/plugins/interact/src/api/clear.js +1 -1
- package/plugins/interact/src/api/enable.test.js +7 -7
- package/plugins/interact/src/api/selectMarker.js +14 -0
- package/plugins/interact/src/api/selectMarker.test.js +25 -0
- package/plugins/interact/src/api/unselectMarker.js +14 -0
- package/plugins/interact/src/api/unselectMarker.test.js +14 -0
- package/plugins/interact/src/defaults.js +4 -6
- package/plugins/interact/src/events.js +27 -36
- package/plugins/interact/src/events.test.js +119 -90
- 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/manifest.js +10 -2
- package/plugins/interact/src/reducer.js +59 -5
- package/plugins/interact/src/reducer.test.js +100 -12
- 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/interact/src/utils/interactionModes.js +12 -0
- 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/components/Panel/Panel.jsx +6 -6
- package/src/App/components/Panel/Panel.test.jsx +37 -0
- package/src/App/components/Viewport/Viewport.jsx +5 -15
- package/src/App/components/Viewport/Viewport.module.scss +2 -0
- package/src/App/components/Viewport/Viewport.test.jsx +16 -33
- package/src/App/hooks/useInterfaceAPI.js +7 -7
- package/src/App/hooks/useInterfaceAPI.test.js +162 -0
- package/src/App/hooks/useLayoutMeasurements.js +64 -72
- package/src/App/hooks/useMarkersAPI.js +2 -5
- package/src/App/hooks/useMarkersAPI.test.js +4 -4
- package/src/App/layout/Layout.jsx +3 -3
- package/src/App/layout/Layout.test.jsx +4 -2
- package/src/App/layout/layout.module.scss +1 -8
- package/src/App/renderer/HtmlElementHost.jsx +10 -5
- package/src/App/renderer/mapPanels.js +2 -1
- package/src/App/store/ServiceProvider.jsx +7 -5
- package/src/App/store/appActionsMap.js +4 -4
- package/src/App/store/appActionsMap.test.js +10 -0
- 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/InteractiveMap/InteractiveMap.js +59 -11
- package/src/InteractiveMap/InteractiveMap.test.js +126 -4
- package/src/InteractiveMap/domStateManager.js +18 -6
- package/src/InteractiveMap/domStateManager.test.js +21 -0
- package/src/InteractiveMap/historyManager.js +28 -16
- package/src/InteractiveMap/historyManager.test.js +17 -0
- package/src/config/appConfig.js +2 -7
- package/src/config/appConfig.test.js +4 -15
- package/src/config/defaults.js +2 -3
- package/src/config/events.js +20 -21
- 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/closeApp.js +1 -10
- package/src/services/closeApp.test.js +3 -43
- 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 +99 -12
- package/src/utils/mapStateSync.js +48 -10
- package/src/utils/mapStateSync.test.js +29 -9
- 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/docs/examples.mdx +0 -70
- package/plugins/beta/datasets/src/adapters/maplibre/patternRegistry.js +0 -48
- package/plugins/beta/datasets/src/styles/patterns.js +0 -157
|
@@ -1,40 +1,38 @@
|
|
|
1
1
|
import { attachEvents } from './events.js'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
it('keyboard Enter triggers only on viewport', () => {
|
|
3
|
+
const MOCK_POINT = { x: 1, y: 2 }
|
|
4
|
+
const MOCK_COORDS = [1, 2]
|
|
5
|
+
const INTERACT_DONE = 'interact:done'
|
|
6
|
+
|
|
7
|
+
const createParams = () => {
|
|
8
|
+
const appState = { layoutRefs: { viewportRef: { current: document.body } }, disabledButtons: new Set() }
|
|
9
|
+
const pluginState = { dispatch: jest.fn(), selectionBounds: null, selectedFeatures: [], selectedMarkers: [], closeOnAction: true, multiSelect: false }
|
|
10
|
+
const clickReadyRef = { current: false }
|
|
11
|
+
return {
|
|
12
|
+
appState,
|
|
13
|
+
pluginState,
|
|
14
|
+
clickReadyRef,
|
|
15
|
+
getAppState: () => appState,
|
|
16
|
+
getPluginState: () => pluginState,
|
|
17
|
+
mapState: {
|
|
18
|
+
markers: { remove: jest.fn(), getMarker: jest.fn(() => null) },
|
|
19
|
+
crossHair: { getDetail: jest.fn(() => ({ point: { x: 0, y: 0 }, coords: [0, 0] })) }
|
|
20
|
+
},
|
|
21
|
+
buttonConfig: { selectDone: {}, selectAtTarget: {}, selectCancel: {} },
|
|
22
|
+
events: { MAP_CLICK: 'map:click' },
|
|
23
|
+
eventBus: { on: jest.fn(), off: jest.fn(), emit: jest.fn() },
|
|
24
|
+
handleInteraction: jest.fn(),
|
|
25
|
+
closeApp: jest.fn()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('attachEvents — keyboard', () => {
|
|
30
|
+
let cleanup = null
|
|
31
|
+
|
|
32
|
+
beforeEach(() => { jest.useFakeTimers() })
|
|
33
|
+
afterEach(() => { cleanup?.(); jest.useRealTimers() })
|
|
34
|
+
|
|
35
|
+
it('Enter on viewport triggers interaction', () => {
|
|
38
36
|
const params = createParams()
|
|
39
37
|
cleanup = attachEvents(params)
|
|
40
38
|
|
|
@@ -54,7 +52,6 @@ describe('attachEvents', () => {
|
|
|
54
52
|
cleanup = attachEvents(params)
|
|
55
53
|
const input = document.createElement('input')
|
|
56
54
|
|
|
57
|
-
// Enter outside viewport
|
|
58
55
|
let kd = new KeyboardEvent('keydown', { key: 'Enter' })
|
|
59
56
|
Object.defineProperty(kd, 'target', { value: input })
|
|
60
57
|
document.dispatchEvent(kd)
|
|
@@ -62,7 +59,6 @@ describe('attachEvents', () => {
|
|
|
62
59
|
Object.defineProperty(ku, 'target', { value: input })
|
|
63
60
|
document.dispatchEvent(ku)
|
|
64
61
|
|
|
65
|
-
// other key
|
|
66
62
|
kd = new KeyboardEvent('keydown', { key: 'Space' })
|
|
67
63
|
Object.defineProperty(kd, 'target', { value: document.body })
|
|
68
64
|
document.dispatchEvent(kd)
|
|
@@ -72,6 +68,13 @@ describe('attachEvents', () => {
|
|
|
72
68
|
|
|
73
69
|
expect(params.handleInteraction).not.toHaveBeenCalled()
|
|
74
70
|
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('attachEvents — click handling', () => {
|
|
74
|
+
let cleanup = null
|
|
75
|
+
|
|
76
|
+
beforeEach(() => { jest.useFakeTimers() })
|
|
77
|
+
afterEach(() => { cleanup?.(); jest.useRealTimers() })
|
|
75
78
|
|
|
76
79
|
it('map click triggers interaction when clickReadyRef is true', () => {
|
|
77
80
|
const params = createParams()
|
|
@@ -79,18 +82,17 @@ describe('attachEvents', () => {
|
|
|
79
82
|
cleanup = attachEvents(params)
|
|
80
83
|
|
|
81
84
|
const handler = params.eventBus.on.mock.calls.find(c => c[0] === 'map:click')[1]
|
|
82
|
-
handler({ point:
|
|
85
|
+
handler({ point: MOCK_POINT, coords: MOCK_COORDS })
|
|
83
86
|
|
|
84
|
-
expect(params.handleInteraction).toHaveBeenCalledWith({ point:
|
|
87
|
+
expect(params.handleInteraction).toHaveBeenCalledWith({ point: MOCK_POINT, coords: MOCK_COORDS })
|
|
85
88
|
})
|
|
86
89
|
|
|
87
90
|
it('map click is suppressed when clickReadyRef is false', () => {
|
|
88
91
|
const params = createParams()
|
|
89
|
-
params.clickReadyRef.current = false
|
|
90
92
|
cleanup = attachEvents(params)
|
|
91
93
|
|
|
92
94
|
const handler = params.eventBus.on.mock.calls.find(c => c[0] === 'map:click')[1]
|
|
93
|
-
handler({ point:
|
|
95
|
+
handler({ point: MOCK_POINT, coords: MOCK_COORDS })
|
|
94
96
|
|
|
95
97
|
expect(params.handleInteraction).not.toHaveBeenCalled()
|
|
96
98
|
})
|
|
@@ -99,111 +101,138 @@ describe('attachEvents', () => {
|
|
|
99
101
|
const params = createParams()
|
|
100
102
|
cleanup = attachEvents(params)
|
|
101
103
|
|
|
102
|
-
const crossDetail = { point:
|
|
104
|
+
const crossDetail = { point: MOCK_POINT, coords: MOCK_COORDS }
|
|
103
105
|
params.mapState.crossHair.getDetail.mockReturnValue(crossDetail)
|
|
104
106
|
|
|
105
107
|
params.buttonConfig.selectAtTarget.onClick()
|
|
106
108
|
expect(params.handleInteraction).toHaveBeenCalledWith(crossDetail)
|
|
107
109
|
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('attachEvents — button actions', () => {
|
|
113
|
+
let cleanup = null
|
|
114
|
+
|
|
115
|
+
beforeEach(() => { jest.useFakeTimers() })
|
|
116
|
+
afterEach(() => { cleanup?.(); jest.useRealTimers() })
|
|
108
117
|
|
|
109
118
|
it('selectDone emits correct payload and respects closeOnAction', () => {
|
|
110
119
|
const params = createParams()
|
|
111
120
|
cleanup = attachEvents(params)
|
|
112
121
|
|
|
113
|
-
|
|
114
|
-
params.mapState.markers.getMarker.mockReturnValue({ coords: [1, 2] })
|
|
122
|
+
params.mapState.markers.getMarker.mockReturnValue({ coords: MOCK_COORDS })
|
|
115
123
|
params.buttonConfig.selectDone.onClick()
|
|
116
124
|
expect(params.closeApp).toHaveBeenCalled()
|
|
117
125
|
|
|
118
|
-
// cover closeOnAction = false
|
|
119
126
|
params.closeApp.mockClear()
|
|
120
127
|
params.pluginState.closeOnAction = false
|
|
121
|
-
params.mapState.markers.getMarker.mockReturnValue({ coords: [3, 4] })
|
|
122
128
|
params.buttonConfig.selectDone.onClick()
|
|
123
129
|
expect(params.closeApp).not.toHaveBeenCalled()
|
|
124
130
|
})
|
|
125
131
|
|
|
126
|
-
it('
|
|
132
|
+
it('selectDone emits selectedFeatures and selectionBounds when no marker', () => {
|
|
127
133
|
const params = createParams()
|
|
128
134
|
cleanup = attachEvents(params)
|
|
129
135
|
|
|
130
|
-
|
|
131
|
-
params.
|
|
132
|
-
|
|
136
|
+
params.pluginState.selectedFeatures = [{ id: 'f1' }]
|
|
137
|
+
params.pluginState.selectionBounds = { sw: [0, 0], ne: [1, 1] }
|
|
138
|
+
params.buttonConfig.selectDone.onClick()
|
|
133
139
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
params2.pluginState.closeOnAction = false
|
|
139
|
-
params2.buttonConfig.selectCancel.onClick()
|
|
140
|
-
expect(params2.closeApp).not.toHaveBeenCalled()
|
|
140
|
+
expect(params.eventBus.emit).toHaveBeenCalledWith(INTERACT_DONE, {
|
|
141
|
+
selectedFeatures: [{ id: 'f1' }],
|
|
142
|
+
selectionBounds: { sw: [0, 0], ne: [1, 1] }
|
|
143
|
+
})
|
|
141
144
|
})
|
|
142
145
|
|
|
143
|
-
it('
|
|
146
|
+
it('selectDone includes selectedMarkers in payload when present', () => {
|
|
144
147
|
const params = createParams()
|
|
145
148
|
cleanup = attachEvents(params)
|
|
146
149
|
|
|
147
|
-
params.
|
|
150
|
+
params.pluginState.selectedMarkers = ['m1', 'm2']
|
|
148
151
|
params.buttonConfig.selectDone.onClick()
|
|
149
152
|
|
|
150
|
-
expect(params.eventBus.emit).
|
|
151
|
-
|
|
153
|
+
expect(params.eventBus.emit).toHaveBeenCalledWith(INTERACT_DONE,
|
|
154
|
+
expect.objectContaining({ selectedMarkers: ['m1', 'm2'] })
|
|
155
|
+
)
|
|
152
156
|
})
|
|
153
157
|
|
|
154
|
-
it('
|
|
158
|
+
it('selectDone omits selectedMarkers from payload when empty', () => {
|
|
155
159
|
const params = createParams()
|
|
156
160
|
cleanup = attachEvents(params)
|
|
157
161
|
|
|
158
|
-
|
|
159
|
-
const unselectHandler = params.eventBus.on.mock.calls.find(c => c[0] === 'interact:unselectFeature')[1]
|
|
160
|
-
|
|
161
|
-
selectHandler({ featureId: 'F1' })
|
|
162
|
-
unselectHandler({ featureId: 'F2' })
|
|
162
|
+
params.buttonConfig.selectDone.onClick()
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
expect(
|
|
164
|
+
const payload = params.eventBus.emit.mock.calls.find(c => c[0] === INTERACT_DONE)[1]
|
|
165
|
+
expect(payload).not.toHaveProperty('selectedMarkers')
|
|
166
166
|
})
|
|
167
167
|
|
|
168
|
-
it('
|
|
168
|
+
it('does not emit or closeApp if selectDone button is disabled', () => {
|
|
169
169
|
const params = createParams()
|
|
170
170
|
cleanup = attachEvents(params)
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
|
|
172
|
+
params.appState.disabledButtons.add('selectDone')
|
|
173
|
+
params.buttonConfig.selectDone.onClick()
|
|
174
|
+
|
|
175
|
+
expect(params.eventBus.emit).not.toHaveBeenCalled()
|
|
176
|
+
expect(params.closeApp).not.toHaveBeenCalled()
|
|
173
177
|
})
|
|
174
178
|
|
|
175
|
-
it('
|
|
179
|
+
it('selectCancel emits cancel and respects closeOnAction', () => {
|
|
176
180
|
const params = createParams()
|
|
177
181
|
cleanup = attachEvents(params)
|
|
178
182
|
|
|
179
|
-
|
|
180
|
-
params.
|
|
181
|
-
|
|
182
|
-
// Set up features and bounds
|
|
183
|
-
params.pluginState.selectedFeatures = [{ id: 'f1' }]
|
|
184
|
-
params.pluginState.selectionBounds = { sw: [0, 0], ne: [1, 1] }
|
|
185
|
-
|
|
186
|
-
params.buttonConfig.selectDone.onClick()
|
|
183
|
+
params.buttonConfig.selectCancel.onClick()
|
|
184
|
+
expect(params.closeApp).toHaveBeenCalled()
|
|
187
185
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
186
|
+
cleanup()
|
|
187
|
+
const params2 = createParams()
|
|
188
|
+
cleanup = attachEvents(params2)
|
|
189
|
+
params2.pluginState.closeOnAction = false
|
|
190
|
+
params2.buttonConfig.selectCancel.onClick()
|
|
191
|
+
expect(params2.closeApp).not.toHaveBeenCalled()
|
|
192
192
|
})
|
|
193
193
|
|
|
194
|
-
it('respects default closeOnAction when value is
|
|
194
|
+
it('respects default closeOnAction when value is nullish', () => {
|
|
195
195
|
const params = createParams()
|
|
196
|
-
|
|
197
|
-
params.pluginState.closeOnAction = undefined
|
|
196
|
+
params.pluginState.closeOnAction = null
|
|
198
197
|
cleanup = attachEvents(params)
|
|
199
198
|
|
|
200
|
-
// Test for selectDone
|
|
201
199
|
params.buttonConfig.selectDone.onClick()
|
|
202
200
|
expect(params.closeApp).toHaveBeenCalledTimes(1)
|
|
203
201
|
|
|
204
|
-
// Test for selectCancel
|
|
205
202
|
params.closeApp.mockClear()
|
|
206
203
|
params.buttonConfig.selectCancel.onClick()
|
|
207
204
|
expect(params.closeApp).toHaveBeenCalledTimes(1)
|
|
208
205
|
})
|
|
209
206
|
})
|
|
207
|
+
|
|
208
|
+
describe('attachEvents — programmatic selection', () => {
|
|
209
|
+
let cleanup = null
|
|
210
|
+
|
|
211
|
+
beforeEach(() => { jest.useFakeTimers() })
|
|
212
|
+
afterEach(() => { cleanup?.(); jest.useRealTimers() })
|
|
213
|
+
|
|
214
|
+
it('selectFeature and unselectFeature dispatch and remove location marker', () => {
|
|
215
|
+
const params = createParams()
|
|
216
|
+
cleanup = attachEvents(params)
|
|
217
|
+
|
|
218
|
+
const selectHandler = params.eventBus.on.mock.calls.find(c => c[0] === 'interact:selectFeature')[1]
|
|
219
|
+
const unselectHandler = params.eventBus.on.mock.calls.find(c => c[0] === 'interact:unselectFeature')[1]
|
|
220
|
+
|
|
221
|
+
selectHandler({ featureId: 'F1' })
|
|
222
|
+
unselectHandler({ featureId: 'F2' })
|
|
223
|
+
|
|
224
|
+
expect(params.pluginState.dispatch).toHaveBeenCalledTimes(2)
|
|
225
|
+
expect(params.mapState.markers.remove).toHaveBeenCalledTimes(2)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
describe('attachEvents — cleanup', () => {
|
|
230
|
+
it('removes all handlers and nulls button onClick callbacks', () => {
|
|
231
|
+
jest.useFakeTimers()
|
|
232
|
+
const params = createParams()
|
|
233
|
+
const cleanup = attachEvents(params)
|
|
234
|
+
cleanup()
|
|
235
|
+
Object.values(params.buttonConfig).forEach(btn => expect(btn.onClick).toBeNull())
|
|
236
|
+
jest.useRealTimers()
|
|
237
|
+
})
|
|
238
|
+
})
|
|
@@ -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
|
}
|