@defra/interactive-map 0.0.22-alpha → 0.0.23-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/button-definition.md +39 -0
- package/docs/api/context.md +4 -4
- package/docs/api/map-style-config.md +11 -2
- package/docs/api/marker-config.md +38 -7
- package/docs/api/panel-definition.md +7 -3
- package/docs/api/symbol-config.md +10 -26
- package/docs/api/symbol-registry.md +28 -13
- package/docs/api.md +24 -2
- package/docs/assets/css/{docusaurus.css → docusaurus.scss} +18 -12
- package/docs/assets/images/toggle-marker-label.jpg +0 -0
- package/docs/examples/index.mdx +12 -0
- package/docs/examples/toggle-marker-label.mdx +89 -0
- package/docs/plugins/interact.md +4 -11
- package/docusaurus.config.cjs +2 -1
- package/package.json +8 -8
- package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +1 -1
- package/plugins/beta/datasets/dist/css/index.css +2 -2
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-maplibre-adapter.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/index.js +1 -1
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +5 -5
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +24 -29
- package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +2 -2
- package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +3 -3
- package/plugins/beta/datasets/src/components/KeySvg.jsx +2 -2
- package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +1 -2
- package/plugins/beta/datasets/src/components/svgProperties.js +2 -2
- package/plugins/beta/datasets/src/panels/Key.module.scss +2 -2
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -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/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/use-location/dist/esm/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/src/manifest.js +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 +23 -55
- package/plugins/interact/src/InteractInit.test.js +58 -80
- package/plugins/interact/src/defaults.js +1 -3
- package/plugins/interact/src/hooks/useAttachEvents.js +46 -0
- package/plugins/interact/src/hooks/useAttachEvents.test.js +65 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +47 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +54 -0
- package/plugins/interact/src/hooks/useHighlightSync.js +30 -8
- package/plugins/interact/src/hooks/useHighlightSync.test.js +127 -80
- package/plugins/interact/src/hooks/useInteractionHandlers.js +18 -17
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +22 -61
- package/plugins/interact/src/hooks/useMapItemList.js +187 -0
- package/plugins/interact/src/hooks/useMapItemList.test.js +584 -0
- package/plugins/interact/src/index.js +0 -2
- package/plugins/interact/src/index.test.js +0 -2
- package/plugins/interact/src/manifest.js +37 -6
- package/plugins/interact/src/reducer.js +8 -4
- package/plugins/interact/src/reducer.test.js +18 -3
- package/plugins/interact/src/utils/buildStylesMap.js +21 -10
- package/plugins/interact/src/utils/buildStylesMap.test.js +29 -29
- package/plugins/interact/src/utils/spatial.js +0 -15
- package/plugins/interact/src/utils/spatial.test.js +1 -13
- package/plugins/search/dist/css/index.css +1 -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/components/CloseButton/CloseButton.module.scss +2 -0
- package/plugins/search/src/components/OpenButton/OpenButton.jsx +1 -1
- package/plugins/search/src/components/OpenButton/OpenButton.module.scss +3 -0
- package/plugins/search/src/search.scss +1 -0
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/esriProvider.js +4 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/esm/index.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/index.js +1 -1
- package/providers/maplibre/src/index.test.js +9 -14
- package/providers/maplibre/src/mapEvents.js +11 -5
- package/providers/maplibre/src/mapEvents.test.js +14 -3
- package/providers/maplibre/src/maplibreProvider.js +18 -11
- package/providers/maplibre/src/maplibreProvider.test.js +43 -36
- package/providers/maplibre/src/utils/highlightFeatures.js +102 -57
- package/providers/maplibre/src/utils/highlightFeatures.test.js +162 -51
- package/providers/maplibre/src/utils/maplibreFixes.js +10 -0
- package/providers/maplibre/src/utils/maplibreFixes.test.js +31 -0
- package/providers/maplibre/src/utils/patternImages.js +10 -12
- package/providers/maplibre/src/utils/patternImages.test.js +145 -167
- package/providers/maplibre/src/utils/rasteriseToImageData.js +1 -4
- package/providers/maplibre/src/utils/rasteriseToImageData.test.js +23 -24
- package/providers/maplibre/src/utils/symbolImages.js +23 -86
- package/providers/maplibre/src/utils/symbolImages.test.js +60 -105
- package/src/App/components/Actions/Actions.module.scss +1 -0
- package/src/App/components/Actions/Actions.test.jsx +17 -4
- package/src/App/components/Hints/Hints.jsx +52 -0
- package/src/App/components/Hints/Hints.module.scss +37 -0
- package/src/App/components/Hints/Hints.test.jsx +104 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +57 -19
- package/src/App/components/KeyboardHelp/KeyboardHelp.module.scss +10 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +140 -31
- package/src/App/components/Markers/LabelMarker.jsx +14 -0
- package/src/App/components/Markers/LabelMarker.test.jsx +36 -0
- package/src/App/components/Markers/Markers.jsx +44 -75
- package/src/App/components/Markers/Markers.module.scss +58 -0
- package/src/App/components/Markers/Markers.test.jsx +90 -113
- package/src/App/components/Markers/SymbolLabelMarker.jsx +43 -0
- package/src/App/components/Markers/SymbolLabelMarker.test.jsx +56 -0
- package/src/App/components/Markers/SymbolMarker.jsx +31 -0
- package/src/App/components/Markers/SymbolMarker.test.jsx +72 -0
- package/src/App/components/Panel/Panel.jsx +1 -1
- package/src/App/components/Panel/Panel.module.scss +1 -0
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +117 -1
- package/src/App/components/PopupMenu/usePopupMenu.js +132 -85
- package/src/App/components/Tabs/Tabs.jsx +68 -0
- package/src/App/components/Tabs/Tabs.module.scss +57 -0
- package/src/App/components/Tabs/Tabs.test.jsx +121 -0
- package/src/App/components/Tooltip/Tooltip.module.scss +1 -1
- package/src/App/components/Viewport/Features.jsx +35 -0
- package/src/App/components/Viewport/Features.module.scss +31 -0
- package/src/App/components/Viewport/Features.test.jsx +124 -0
- package/src/App/components/Viewport/MapStatus.jsx +0 -4
- package/src/App/components/Viewport/Viewport.jsx +35 -29
- package/src/App/components/Viewport/Viewport.module.scss +7 -21
- package/src/App/components/Viewport/Viewport.test.jsx +108 -65
- package/src/App/controls/keyboardActions.js +9 -3
- package/src/App/controls/keyboardActions.test.js +122 -87
- package/src/App/controls/keyboardShortcuts.js +1 -14
- package/src/App/hooks/useFeatureFocus.js +199 -0
- package/src/App/hooks/useFeatureFocus.test.js +635 -0
- package/src/App/hooks/useFeatureItems.js +30 -0
- package/src/App/hooks/useFeatureItems.test.js +97 -0
- package/src/App/hooks/useFocusVisible.js +4 -7
- package/src/App/hooks/useFocusVisible.test.js +6 -6
- package/src/App/hooks/useKeyboardHint.js +9 -34
- package/src/App/hooks/useKeyboardHint.test.js +34 -84
- package/src/App/hooks/useKeyboardShortcuts.js +8 -5
- package/src/App/hooks/useKeyboardShortcuts.test.js +16 -12
- package/src/App/hooks/useLayoutMeasurements.js +10 -4
- package/src/App/hooks/useLayoutMeasurements.test.js +30 -0
- package/src/App/hooks/useMarkerCursor.js +33 -0
- package/src/App/hooks/useMarkerCursor.test.js +87 -0
- package/src/App/hooks/useMarkersAPI.js +32 -9
- package/src/App/hooks/useMarkersAPI.test.js +162 -94
- package/src/App/layout/Layout.jsx +2 -0
- package/src/App/layout/Layout.test.jsx +3 -0
- package/src/App/registry/keyboardShortcutRegistry.js +3 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +6 -8
- package/src/App/renderer/mapButtons.js +5 -1
- package/src/App/renderer/mapButtons.test.js +37 -1
- package/src/App/store/ServiceProvider.jsx +4 -1
- package/src/App/store/ServiceProvider.test.jsx +11 -0
- package/src/App/store/appActionsMap.js +5 -1
- package/src/App/store/appActionsMap.test.js +13 -0
- package/src/App/store/appReducer.js +1 -0
- package/src/App/store/mapActionsMap.js +2 -1
- package/src/InteractiveMap/InteractiveMap.js +12 -0
- package/src/InteractiveMap/InteractiveMap.test.js +2 -0
- package/src/config/appConfig.js +3 -1
- package/src/config/appConfig.test.js +4 -0
- package/src/config/defaults.js +1 -1
- package/src/config/events.js +8 -0
- package/src/config/mapTheme.js +8 -4
- package/src/config/symbolConfig.js +23 -16
- package/src/scss/main.scss +3 -0
- package/src/scss/tools/_border-focus.scss +0 -1
- package/src/services/eventBus.js +15 -0
- package/src/services/eventBus.test.js +11 -0
- package/src/services/hints.js +47 -0
- package/src/services/hints.test.js +110 -0
- package/src/services/patternRegistry.js +69 -1
- package/src/services/patternRegistry.test.js +112 -1
- package/src/services/reverseGeocode.js +4 -0
- package/src/services/reverseGeocode.test.js +11 -1
- package/src/services/symbolRegistry.js +152 -17
- package/src/services/symbolRegistry.test.js +141 -31
- package/src/types.js +71 -14
- package/src/utils/constrainKeyboardFocus.js +5 -5
- package/src/utils/detectInterfaceType.js +27 -5
- package/src/utils/detectInterfaceType.test.js +29 -3
- package/src/utils/patternUtils.js +4 -67
- package/src/utils/patternUtils.test.js +1 -123
- package/src/utils/symbolUtils.js +11 -13
- package/src/utils/symbolUtils.test.js +18 -26
- package/plugins/interact/dist/css/index.css +0 -1
- package/plugins/interact/src/interact.scss +0 -24
|
@@ -6,43 +6,51 @@ jest.mock('../utils/buildStylesMap.js', () => ({
|
|
|
6
6
|
buildStylesMap: jest.fn(() => ({ layer1: { stroke: 'red', fill: 'blue' } }))
|
|
7
7
|
}))
|
|
8
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
|
-
|
|
38
|
-
|
|
9
|
+
const STYLE_CHANGE = 'map:stylechange'
|
|
10
|
+
const DATA_CHANGE = 'map:datachange'
|
|
11
|
+
|
|
12
|
+
let mockDeps
|
|
13
|
+
let capturedStyleChangeHandler
|
|
14
|
+
let capturedDataChangeHandler
|
|
15
|
+
|
|
16
|
+
const render = (overrides = {}) =>
|
|
17
|
+
renderHook(() => useHighlightSync({ ...mockDeps, ...overrides }))
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.clearAllMocks()
|
|
21
|
+
capturedStyleChangeHandler = null
|
|
22
|
+
capturedDataChangeHandler = null
|
|
23
|
+
|
|
24
|
+
mockDeps = {
|
|
25
|
+
mapProvider: {
|
|
26
|
+
updateHighlightedFeatures: jest.fn(() => ({ sw: [0, 0], ne: [1, 1] }))
|
|
27
|
+
},
|
|
28
|
+
mapStyle: { id: 'default-style' },
|
|
29
|
+
pluginState: {
|
|
30
|
+
layers: [{ layerId: 'layer1' }]
|
|
31
|
+
},
|
|
32
|
+
selectedFeatures: [],
|
|
33
|
+
dispatch: jest.fn(),
|
|
34
|
+
events: { MAP_STYLE_CHANGE: STYLE_CHANGE, MAP_DATA_CHANGE: DATA_CHANGE },
|
|
35
|
+
eventBus: {
|
|
36
|
+
on: jest.fn((event, handler) => {
|
|
37
|
+
if (event === STYLE_CHANGE) {
|
|
38
|
+
capturedStyleChangeHandler = handler
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
once: jest.fn((event, handler) => {
|
|
42
|
+
if (event === DATA_CHANGE) {
|
|
43
|
+
capturedDataChangeHandler = handler
|
|
44
|
+
}
|
|
45
|
+
}),
|
|
46
|
+
off: jest.fn()
|
|
39
47
|
}
|
|
40
|
-
}
|
|
48
|
+
}
|
|
49
|
+
})
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
/* Highlighting */
|
|
44
|
-
/* ------------------------------------------------------------------ */
|
|
51
|
+
// ─── useHighlightSync — highlighting ─────────────────────────────────────────
|
|
45
52
|
|
|
53
|
+
describe('useHighlightSync — highlighting', () => {
|
|
46
54
|
it('updates map highlights and dispatches bounds', () => {
|
|
47
55
|
mockDeps.selectedFeatures = [{ featureId: 'F1', layerId: 'layer1' }]
|
|
48
56
|
|
|
@@ -50,9 +58,9 @@ describe('useHighlightSync', () => {
|
|
|
50
58
|
|
|
51
59
|
expect(mockDeps.mapProvider.updateHighlightedFeatures).toHaveBeenCalledWith(
|
|
52
60
|
mockDeps.selectedFeatures,
|
|
61
|
+
[],
|
|
53
62
|
expect.any(Object)
|
|
54
63
|
)
|
|
55
|
-
|
|
56
64
|
expect(mockDeps.dispatch).toHaveBeenCalledWith({
|
|
57
65
|
type: 'UPDATE_SELECTED_BOUNDS',
|
|
58
66
|
payload: { sw: [0, 0], ne: [1, 1] }
|
|
@@ -70,11 +78,11 @@ describe('useHighlightSync', () => {
|
|
|
70
78
|
payload: null
|
|
71
79
|
})
|
|
72
80
|
})
|
|
81
|
+
})
|
|
73
82
|
|
|
74
|
-
|
|
75
|
-
/* Styles memoization */
|
|
76
|
-
/* ------------------------------------------------------------------ */
|
|
83
|
+
// ─── useHighlightSync — styles memoization ───────────────────────────────────
|
|
77
84
|
|
|
85
|
+
describe('useHighlightSync — styles memoization', () => {
|
|
78
86
|
it('rebuilds styles when mapStyle changes', () => {
|
|
79
87
|
mockDeps.selectedFeatures = [{ featureId: 'F1' }]
|
|
80
88
|
|
|
@@ -84,46 +92,37 @@ describe('useHighlightSync', () => {
|
|
|
84
92
|
)
|
|
85
93
|
|
|
86
94
|
buildStylesMap.mockClear()
|
|
87
|
-
|
|
88
95
|
rerender({ mapStyle: { id: 'satellite' } })
|
|
89
96
|
|
|
90
|
-
expect(buildStylesMap).toHaveBeenCalledWith(
|
|
91
|
-
expect.anything(),
|
|
92
|
-
{ id: 'satellite' }
|
|
93
|
-
)
|
|
97
|
+
expect(buildStylesMap).toHaveBeenCalledWith(expect.anything(), { id: 'satellite' })
|
|
94
98
|
})
|
|
95
99
|
|
|
96
100
|
it('rebuilds styles when layers change', () => {
|
|
97
101
|
mockDeps.selectedFeatures = [{ featureId: 'F1' }]
|
|
98
102
|
|
|
99
103
|
const { rerender } = renderHook(
|
|
100
|
-
({ layers }) =>
|
|
101
|
-
useHighlightSync({
|
|
102
|
-
...mockDeps,
|
|
103
|
-
pluginState: { layers }
|
|
104
|
-
}),
|
|
104
|
+
({ layers }) => useHighlightSync({ ...mockDeps, pluginState: { layers } }),
|
|
105
105
|
{ initialProps: { layers: [{ layerId: 'layer1' }] } }
|
|
106
106
|
)
|
|
107
107
|
|
|
108
108
|
buildStylesMap.mockClear()
|
|
109
|
-
|
|
110
109
|
rerender({ layers: [{ layerId: 'layer1' }, { layerId: 'layer2' }] })
|
|
111
110
|
|
|
112
111
|
expect(buildStylesMap).toHaveBeenCalled()
|
|
113
112
|
})
|
|
113
|
+
})
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
/* Map data change events */
|
|
117
|
-
/* ------------------------------------------------------------------ */
|
|
115
|
+
// ─── useHighlightSync — style-change event ───────────────────────────────────
|
|
118
116
|
|
|
119
|
-
|
|
117
|
+
describe('useHighlightSync — style-change event', () => {
|
|
118
|
+
it('refreshes highlights after MAP_STYLE_CHANGE then MAP_DATA_CHANGE', () => {
|
|
120
119
|
mockDeps.selectedFeatures = [{ featureId: 'F1', layerId: 'layer1' }]
|
|
121
120
|
|
|
122
121
|
render()
|
|
123
|
-
|
|
124
122
|
mockDeps.mapProvider.updateHighlightedFeatures.mockClear()
|
|
125
123
|
|
|
126
|
-
act(() =>
|
|
124
|
+
act(() => capturedStyleChangeHandler())
|
|
125
|
+
act(() => capturedDataChangeHandler())
|
|
127
126
|
|
|
128
127
|
expect(mockDeps.mapProvider.updateHighlightedFeatures).toHaveBeenCalled()
|
|
129
128
|
})
|
|
@@ -132,19 +131,15 @@ describe('useHighlightSync', () => {
|
|
|
132
131
|
mockDeps.selectedFeatures = [{ featureId: 'F1', layerId: 'layer1' }]
|
|
133
132
|
|
|
134
133
|
const { unmount } = render()
|
|
135
|
-
|
|
136
134
|
unmount()
|
|
137
135
|
|
|
138
|
-
expect(mockDeps.eventBus.off).toHaveBeenCalledWith(
|
|
139
|
-
'map:datachange',
|
|
140
|
-
expect.any(Function)
|
|
141
|
-
)
|
|
136
|
+
expect(mockDeps.eventBus.off).toHaveBeenCalledWith(STYLE_CHANGE, expect.any(Function))
|
|
142
137
|
})
|
|
138
|
+
})
|
|
143
139
|
|
|
144
|
-
|
|
145
|
-
/* Guards */
|
|
146
|
-
/* ------------------------------------------------------------------ */
|
|
140
|
+
// ─── useHighlightSync — guards ───────────────────────────────────────────────
|
|
147
141
|
|
|
142
|
+
describe('useHighlightSync — guards', () => {
|
|
148
143
|
it('does nothing when mapProvider is null', () => {
|
|
149
144
|
mockDeps.mapProvider = null
|
|
150
145
|
mockDeps.selectedFeatures = [{ featureId: 'F1' }]
|
|
@@ -154,14 +149,6 @@ describe('useHighlightSync', () => {
|
|
|
154
149
|
expect(mockDeps.dispatch).not.toHaveBeenCalled()
|
|
155
150
|
})
|
|
156
151
|
|
|
157
|
-
it('does nothing when selectedFeatures is null', () => {
|
|
158
|
-
mockDeps.selectedFeatures = null
|
|
159
|
-
|
|
160
|
-
render()
|
|
161
|
-
|
|
162
|
-
expect(mockDeps.mapProvider.updateHighlightedFeatures).not.toHaveBeenCalled()
|
|
163
|
-
})
|
|
164
|
-
|
|
165
152
|
it('does nothing when mapStyle is null', () => {
|
|
166
153
|
mockDeps.mapStyle = null
|
|
167
154
|
mockDeps.selectedFeatures = [{ featureId: 'F1' }]
|
|
@@ -171,41 +158,101 @@ describe('useHighlightSync', () => {
|
|
|
171
158
|
expect(mockDeps.mapProvider.updateHighlightedFeatures).not.toHaveBeenCalled()
|
|
172
159
|
expect(buildStylesMap).not.toHaveBeenCalled()
|
|
173
160
|
})
|
|
161
|
+
})
|
|
174
162
|
|
|
175
|
-
|
|
176
|
-
/* Selection updates */
|
|
177
|
-
/* ------------------------------------------------------------------ */
|
|
163
|
+
// ─── useHighlightSync — selection updates ────────────────────────────────────
|
|
178
164
|
|
|
165
|
+
describe('useHighlightSync — selection updates', () => {
|
|
179
166
|
it('updates highlights when selection changes', () => {
|
|
180
167
|
const { rerender } = renderHook(
|
|
181
|
-
({ selectedFeatures }) =>
|
|
182
|
-
useHighlightSync({ ...mockDeps, selectedFeatures }),
|
|
168
|
+
({ selectedFeatures }) => useHighlightSync({ ...mockDeps, selectedFeatures }),
|
|
183
169
|
{ initialProps: { selectedFeatures: [{ featureId: 'F1' }] } }
|
|
184
170
|
)
|
|
185
171
|
|
|
186
172
|
mockDeps.mapProvider.updateHighlightedFeatures.mockClear()
|
|
187
|
-
|
|
188
173
|
rerender({ selectedFeatures: [{ featureId: 'F1' }, { featureId: 'F2' }] })
|
|
189
174
|
|
|
190
175
|
expect(mockDeps.mapProvider.updateHighlightedFeatures).toHaveBeenCalledWith(
|
|
191
176
|
[{ featureId: 'F1' }, { featureId: 'F2' }],
|
|
177
|
+
[],
|
|
192
178
|
expect.anything()
|
|
193
179
|
)
|
|
194
180
|
})
|
|
195
181
|
|
|
196
182
|
it('clears highlights when selection becomes empty', () => {
|
|
197
183
|
const { rerender } = renderHook(
|
|
198
|
-
({ selectedFeatures }) =>
|
|
199
|
-
useHighlightSync({ ...mockDeps, selectedFeatures }),
|
|
184
|
+
({ selectedFeatures }) => useHighlightSync({ ...mockDeps, selectedFeatures }),
|
|
200
185
|
{ initialProps: { selectedFeatures: [{ featureId: 'F1' }] } }
|
|
201
186
|
)
|
|
202
187
|
|
|
203
188
|
mockDeps.mapProvider.updateHighlightedFeatures.mockClear()
|
|
204
|
-
|
|
205
189
|
rerender({ selectedFeatures: [] })
|
|
206
190
|
|
|
207
191
|
expect(mockDeps.mapProvider.updateHighlightedFeatures).toHaveBeenCalledWith(
|
|
208
192
|
[],
|
|
193
|
+
[],
|
|
194
|
+
expect.anything()
|
|
195
|
+
)
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// ─── useHighlightSync — listboxActiveItem ────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
describe('useHighlightSync — listboxActiveItem', () => {
|
|
202
|
+
it('passes listboxActiveItem as activeFeatures (second arg)', () => {
|
|
203
|
+
mockDeps.pluginState = {
|
|
204
|
+
layers: [{ layerId: 'layer1' }],
|
|
205
|
+
listboxActiveItem: { featureId: 'A1', layerId: 'layer1', idProperty: 'id', geometry: { type: 'Point' } }
|
|
206
|
+
}
|
|
207
|
+
mockDeps.selectedFeatures = []
|
|
208
|
+
|
|
209
|
+
render()
|
|
210
|
+
|
|
211
|
+
expect(mockDeps.mapProvider.updateHighlightedFeatures).toHaveBeenCalledWith(
|
|
212
|
+
[],
|
|
213
|
+
[{ featureId: 'A1', layerId: 'layer1', idProperty: 'id', geometry: { type: 'Point' } }],
|
|
214
|
+
expect.anything()
|
|
215
|
+
)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('passes selectedFeatures and listboxActiveItem as separate arguments', () => {
|
|
219
|
+
mockDeps.pluginState = {
|
|
220
|
+
layers: [{ layerId: 'layer1' }],
|
|
221
|
+
listboxActiveItem: { featureId: 'A1', layerId: 'layer1', idProperty: 'id', geometry: { type: 'Point' } }
|
|
222
|
+
}
|
|
223
|
+
mockDeps.selectedFeatures = [{ featureId: 'F1', layerId: 'layer1' }]
|
|
224
|
+
|
|
225
|
+
render()
|
|
226
|
+
|
|
227
|
+
expect(mockDeps.mapProvider.updateHighlightedFeatures).toHaveBeenCalledWith(
|
|
228
|
+
[{ featureId: 'F1', layerId: 'layer1' }],
|
|
229
|
+
[{ featureId: 'A1', layerId: 'layer1', idProperty: 'id', geometry: { type: 'Point' } }],
|
|
230
|
+
expect.anything()
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('re-highlights when listboxActiveItem changes', () => {
|
|
235
|
+
const { rerender } = renderHook(
|
|
236
|
+
({ pluginState }) => useHighlightSync({ ...mockDeps, pluginState }),
|
|
237
|
+
{
|
|
238
|
+
initialProps: {
|
|
239
|
+
pluginState: { layers: [{ layerId: 'layer1' }], listboxActiveItem: null }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
mockDeps.mapProvider.updateHighlightedFeatures.mockClear()
|
|
245
|
+
|
|
246
|
+
rerender({
|
|
247
|
+
pluginState: {
|
|
248
|
+
layers: [{ layerId: 'layer1' }],
|
|
249
|
+
listboxActiveItem: { featureId: 'A1', layerId: 'layer1', idProperty: 'id', geometry: { type: 'Point' } }
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
expect(mockDeps.mapProvider.updateHighlightedFeatures).toHaveBeenCalledWith(
|
|
254
|
+
[],
|
|
255
|
+
[{ featureId: 'A1', layerId: 'layer1', idProperty: 'id', geometry: { type: 'Point' } }],
|
|
209
256
|
expect.anything()
|
|
210
257
|
)
|
|
211
258
|
})
|
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { areAllContiguous } from '../utils/spatial.js'
|
|
3
3
|
import { getFeaturesAtPoint, findMatchingFeature, buildLayerConfigMap } from '../utils/featureQueries.js'
|
|
4
4
|
import { scaleFactor } from '../../../../src/config/appConfig.js'
|
|
5
|
+
import { isStandaloneLabel } from '../../../../src/utils/symbolUtils.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Returns the id of the first DOM marker whose visual bounds contain the given point.
|
|
8
9
|
*
|
|
9
10
|
* MAP_CLICK point is container-relative; getBoundingClientRect is viewport-relative.
|
|
10
|
-
* We convert by subtracting the
|
|
11
|
-
* the map
|
|
11
|
+
* We convert by subtracting the markers container's top-left (.im-c-viewport__markers has
|
|
12
|
+
* inset:0 over the same area as the map canvas, giving the correct offset).
|
|
12
13
|
*
|
|
13
14
|
* @param {Object} markers - markers object from mapState (has .items and .markerRefs)
|
|
14
15
|
* @param {{ x: number, y: number }} point - container-relative pixel coordinates
|
|
15
16
|
* @param {number} scale - scaleFactor for the current mapSize (e.g. 1.5 for medium)
|
|
16
17
|
* @returns {string|null}
|
|
17
18
|
*/
|
|
19
|
+
|
|
18
20
|
const findMarkerAtPoint = (markers, point, scale) => {
|
|
19
21
|
for (const marker of markers.items) {
|
|
20
22
|
const el = markers.markerRefs?.get(marker.id)
|
|
21
|
-
if (!el) {
|
|
23
|
+
if (!el || isStandaloneLabel(marker)) {
|
|
22
24
|
continue
|
|
23
25
|
}
|
|
24
|
-
const
|
|
25
|
-
const parentRect =
|
|
26
|
+
const container = el.closest('.im-c-viewport__markers') || el.parentElement
|
|
27
|
+
const parentRect = container ? container.getBoundingClientRect() : { left: 0, top: 0 }
|
|
26
28
|
const { left, top, right, bottom } = el.getBoundingClientRect()
|
|
27
29
|
const scaledX = point.x * scale
|
|
28
30
|
const scaledY = point.y * scale
|
|
@@ -57,8 +59,7 @@ const useSelectionChangeEmitter = (eventBus, selectedFeatures, selectedMarkers,
|
|
|
57
59
|
selectedFeatures,
|
|
58
60
|
selectedMarkers,
|
|
59
61
|
selectionBounds,
|
|
60
|
-
|
|
61
|
-
canSplit: canSplitFeatures(selectedFeatures)
|
|
62
|
+
contiguous: areAllContiguous(selectedFeatures)
|
|
62
63
|
})
|
|
63
64
|
|
|
64
65
|
lastEmittedSelectionChange.current = { features: selectedFeatures, markers: selectedMarkers }
|
|
@@ -82,13 +83,12 @@ const useSelectionChangeEmitter = (eventBus, selectedFeatures, selectedMarkers,
|
|
|
82
83
|
*/
|
|
83
84
|
export const useInteractionHandlers = ({ mapState, pluginState, services, mapProvider }) => {
|
|
84
85
|
const { markers, mapSize } = mapState
|
|
85
|
-
const { dispatch, layers, interactionModes, multiSelect,
|
|
86
|
+
const { dispatch, layers, interactionModes, multiSelect, marker: markerOptions, tolerance, selectedFeatures, selectedMarkers, selectionBounds, deselectOnClickOutside } = pluginState
|
|
86
87
|
const { eventBus } = services
|
|
87
88
|
const layerConfigMap = buildLayerConfigMap(layers)
|
|
88
89
|
const scale = scaleFactor[mapSize] ?? 1
|
|
89
90
|
const processFeatureMatch = useCallback(({ feature, config }) => {
|
|
90
91
|
markers.remove('location')
|
|
91
|
-
const isNewContiguous = contiguous && isContiguousWithAny(feature, selectedFeatures)
|
|
92
92
|
const featureId = feature.properties?.[config.idProperty] ?? feature.id
|
|
93
93
|
if (featureId == null) {
|
|
94
94
|
return
|
|
@@ -101,11 +101,10 @@ export const useInteractionHandlers = ({ mapState, pluginState, services, mapPro
|
|
|
101
101
|
layerId: config.layerId,
|
|
102
102
|
idProperty: config.idProperty,
|
|
103
103
|
properties: feature.properties,
|
|
104
|
-
geometry: feature.geometry
|
|
105
|
-
replaceAll: contiguous && !isNewContiguous
|
|
104
|
+
geometry: feature.geometry
|
|
106
105
|
}
|
|
107
106
|
})
|
|
108
|
-
}, [markers,
|
|
107
|
+
}, [markers, dispatch, multiSelect])
|
|
109
108
|
|
|
110
109
|
const processFallback = useCallback(({ coords }) => {
|
|
111
110
|
const canPlace = interactionModes.includes('placeMarker')
|
|
@@ -120,6 +119,11 @@ export const useInteractionHandlers = ({ mapState, pluginState, services, mapPro
|
|
|
120
119
|
}, [interactionModes, dispatch, markers, markerOptions, eventBus, deselectOnClickOutside])
|
|
121
120
|
|
|
122
121
|
const handleInteraction = useCallback(({ point, coords }) => {
|
|
122
|
+
const debugFeatures = pluginState?.debug ? getFeaturesAtPoint(mapProvider, point, { radius: tolerance }) : null
|
|
123
|
+
if (debugFeatures) {
|
|
124
|
+
console.log(`--- Features at ${coords} ---`, debugFeatures) // NOSONAR
|
|
125
|
+
}
|
|
126
|
+
|
|
123
127
|
if (interactionModes.includes('selectMarker')) {
|
|
124
128
|
const markerHit = findMarkerAtPoint(markers, point, scale)
|
|
125
129
|
if (markerHit) {
|
|
@@ -129,10 +133,7 @@ export const useInteractionHandlers = ({ mapState, pluginState, services, mapPro
|
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
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 allFeatures = debugFeatures ?? getFeaturesAtPoint(mapProvider, point, { radius: tolerance })
|
|
136
137
|
const match = findMatchingFeature(allFeatures, layerConfigMap)
|
|
137
138
|
if (match) {
|
|
138
139
|
processFeatureMatch(match)
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { renderHook, act } from '@testing-library/react'
|
|
2
2
|
import { useInteractionHandlers } from './useInteractionHandlers.js'
|
|
3
3
|
import * as featureQueries from '../utils/featureQueries.js'
|
|
4
|
-
import { isContiguousWithAny } from '../utils/spatial.js'
|
|
5
|
-
|
|
6
4
|
/* ------------------------------------------------------------------ */
|
|
7
5
|
/* Mocks */
|
|
8
6
|
/* ------------------------------------------------------------------ */
|
|
9
7
|
|
|
10
8
|
jest.mock('../utils/spatial.js', () => ({
|
|
11
|
-
isContiguousWithAny: jest.fn(),
|
|
12
|
-
canSplitFeatures: jest.fn(() => false),
|
|
13
9
|
areAllContiguous: jest.fn(() => false)
|
|
14
10
|
}))
|
|
15
11
|
jest.mock('../utils/featureQueries.js', () => ({
|
|
@@ -32,6 +28,7 @@ const click = result =>
|
|
|
32
28
|
|
|
33
29
|
const makeMarkerEl = (rect) => ({
|
|
34
30
|
getBoundingClientRect: () => rect,
|
|
31
|
+
closest: () => ({ getBoundingClientRect: () => ({ left: 0, top: 0 }) }),
|
|
35
32
|
parentElement: {
|
|
36
33
|
getBoundingClientRect: () => ({ left: 0, top: 0 })
|
|
37
34
|
}
|
|
@@ -47,7 +44,6 @@ const setup = (pluginOverrides = {}, markerItems = [], markerRefs = new Map()) =
|
|
|
47
44
|
layers: [{ layerId: 'parcels', idProperty: 'parcelId' }],
|
|
48
45
|
interactionModes: ['selectMarker', 'selectFeature'],
|
|
49
46
|
multiSelect: false,
|
|
50
|
-
contiguous: false,
|
|
51
47
|
marker: { symbol: 'pin', backgroundColor: 'red' },
|
|
52
48
|
selectedFeatures: [],
|
|
53
49
|
selectedMarkers: [],
|
|
@@ -127,10 +123,11 @@ describe('DOM marker hit detection', () => {
|
|
|
127
123
|
)
|
|
128
124
|
})
|
|
129
125
|
|
|
130
|
-
it('uses zero offset when marker element has no parentElement', () => {
|
|
131
|
-
// parentElement
|
|
126
|
+
it('uses zero offset when marker element has no features container or parentElement', () => {
|
|
127
|
+
// closest and parentElement both null — fallback parentRect { left: 0, top: 0 } should be used
|
|
132
128
|
const markerEl = {
|
|
133
129
|
getBoundingClientRect: () => ({ left: 5, top: 15, right: 15, bottom: 25 }),
|
|
130
|
+
closest: () => null,
|
|
134
131
|
parentElement: null
|
|
135
132
|
}
|
|
136
133
|
const markerRefs = new Map([['marker-1', markerEl]])
|
|
@@ -255,58 +252,6 @@ it('passes multiSelect flag through to dispatch', () => {
|
|
|
255
252
|
)
|
|
256
253
|
})
|
|
257
254
|
|
|
258
|
-
/* ------------------------------------------------------------------ */
|
|
259
|
-
/* Contiguous selection (FULL COVERAGE) */
|
|
260
|
-
/* ------------------------------------------------------------------ */
|
|
261
|
-
|
|
262
|
-
describe('contiguous selection', () => {
|
|
263
|
-
it('does NOT replace selection when feature is contiguous', () => {
|
|
264
|
-
isContiguousWithAny.mockReturnValue(true) // contiguous
|
|
265
|
-
|
|
266
|
-
const { result, deps } = setup({
|
|
267
|
-
contiguous: true,
|
|
268
|
-
selectedFeatures: [{ geometry: { type: 'Polygon' } }]
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
click(result)
|
|
272
|
-
|
|
273
|
-
expect(deps.pluginState.dispatch).toHaveBeenCalledWith(
|
|
274
|
-
expect.objectContaining({
|
|
275
|
-
payload: expect.objectContaining({
|
|
276
|
-
replaceAll: false
|
|
277
|
-
})
|
|
278
|
-
})
|
|
279
|
-
)
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
it('replaces selection when feature is NOT contiguous', () => {
|
|
283
|
-
isContiguousWithAny.mockReturnValue(false) // disjoint
|
|
284
|
-
|
|
285
|
-
const { result, deps } = setup({
|
|
286
|
-
contiguous: true,
|
|
287
|
-
selectedFeatures: [{ geometry: { type: 'Polygon' } }]
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
click(result)
|
|
291
|
-
|
|
292
|
-
expect(deps.pluginState.dispatch).toHaveBeenCalledWith(
|
|
293
|
-
expect.objectContaining({
|
|
294
|
-
payload: expect.objectContaining({
|
|
295
|
-
replaceAll: true
|
|
296
|
-
})
|
|
297
|
-
})
|
|
298
|
-
)
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
it('does not compute contiguity when contiguous is false', () => {
|
|
302
|
-
const { result } = setup({ contiguous: false })
|
|
303
|
-
|
|
304
|
-
click(result)
|
|
305
|
-
|
|
306
|
-
expect(isContiguousWithAny).not.toHaveBeenCalled()
|
|
307
|
-
})
|
|
308
|
-
})
|
|
309
|
-
|
|
310
255
|
/* ------------------------------------------------------------------ */
|
|
311
256
|
/* deselectOnClickOutside */
|
|
312
257
|
/* ------------------------------------------------------------------ */
|
|
@@ -382,6 +327,23 @@ it('does not check markers when selectMarker is not in interactionModes', () =>
|
|
|
382
327
|
/* Selection change event */
|
|
383
328
|
/* ------------------------------------------------------------------ */
|
|
384
329
|
|
|
330
|
+
it('does not emit selectionchange when features are selected but bounds not yet calculated', () => {
|
|
331
|
+
const deps = {
|
|
332
|
+
mapState: { markers: { add: jest.fn(), remove: jest.fn(), items: [], markerRefs: new Map() } },
|
|
333
|
+
pluginState: {
|
|
334
|
+
selectedFeatures: [{ featureId: 'F1' }],
|
|
335
|
+
selectedMarkers: [],
|
|
336
|
+
selectionBounds: null
|
|
337
|
+
},
|
|
338
|
+
services: { eventBus: { emit: jest.fn() } },
|
|
339
|
+
mapProvider: {}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
renderHook(() => useInteractionHandlers(deps))
|
|
343
|
+
|
|
344
|
+
expect(deps.services.eventBus.emit).not.toHaveBeenCalled()
|
|
345
|
+
})
|
|
346
|
+
|
|
385
347
|
it('emits selectionchange once when bounds exist', () => {
|
|
386
348
|
const deps = {
|
|
387
349
|
mapState: { markers: { add: jest.fn(), remove: jest.fn(), items: [], markerRefs: new Map() } },
|
|
@@ -402,8 +364,7 @@ it('emits selectionchange once when bounds exist', () => {
|
|
|
402
364
|
selectedFeatures: deps.pluginState.selectedFeatures,
|
|
403
365
|
selectedMarkers: [],
|
|
404
366
|
selectionBounds: deps.pluginState.selectionBounds,
|
|
405
|
-
|
|
406
|
-
canSplit: false
|
|
367
|
+
contiguous: false
|
|
407
368
|
})
|
|
408
369
|
)
|
|
409
370
|
})
|