@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
|
@@ -30,19 +30,27 @@ const click = result =>
|
|
|
30
30
|
})
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
-
const
|
|
33
|
+
const makeMarkerEl = (rect) => ({
|
|
34
|
+
getBoundingClientRect: () => rect,
|
|
35
|
+
parentElement: {
|
|
36
|
+
getBoundingClientRect: () => ({ left: 0, top: 0 })
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const setup = (pluginOverrides = {}, markerItems = [], markerRefs = new Map()) => {
|
|
34
41
|
const deps = {
|
|
35
42
|
mapState: {
|
|
36
|
-
markers: { add: jest.fn(), remove: jest.fn() }
|
|
43
|
+
markers: { add: jest.fn(), remove: jest.fn(), items: markerItems, markerRefs }
|
|
37
44
|
},
|
|
38
45
|
pluginState: {
|
|
39
46
|
dispatch: jest.fn(),
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
layers: [{ layerId: 'parcels', idProperty: 'parcelId' }],
|
|
48
|
+
interactionModes: ['selectMarker', 'selectFeature'],
|
|
42
49
|
multiSelect: false,
|
|
43
50
|
contiguous: false,
|
|
44
|
-
|
|
51
|
+
marker: { symbol: 'pin', backgroundColor: 'red' },
|
|
45
52
|
selectedFeatures: [],
|
|
53
|
+
selectedMarkers: [],
|
|
46
54
|
selectionBounds: null,
|
|
47
55
|
...pluginOverrides
|
|
48
56
|
},
|
|
@@ -73,12 +81,94 @@ beforeEach(() => {
|
|
|
73
81
|
})
|
|
74
82
|
|
|
75
83
|
/* ------------------------------------------------------------------ */
|
|
76
|
-
/*
|
|
84
|
+
/* DOM marker hit detection */
|
|
85
|
+
/* ------------------------------------------------------------------ */
|
|
86
|
+
|
|
87
|
+
describe('DOM marker hit detection', () => {
|
|
88
|
+
it('dispatches TOGGLE_SELECTED_MARKERS when click is within a marker bounds', () => {
|
|
89
|
+
const markerEl = makeMarkerEl({ left: 5, top: 15, right: 15, bottom: 25 })
|
|
90
|
+
const markerRefs = new Map([['marker-1', markerEl]])
|
|
91
|
+
const markerItems = [{ id: 'marker-1', coords: [1, 2] }]
|
|
92
|
+
|
|
93
|
+
const { result, deps } = setup({}, markerItems, markerRefs)
|
|
94
|
+
click(result)
|
|
95
|
+
|
|
96
|
+
expect(deps.pluginState.dispatch).toHaveBeenCalledWith({
|
|
97
|
+
type: 'TOGGLE_SELECTED_MARKERS',
|
|
98
|
+
payload: { markerId: 'marker-1', multiSelect: false }
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('markers take precedence over features when both hit', () => {
|
|
103
|
+
const markerEl = makeMarkerEl({ left: 5, top: 15, right: 15, bottom: 25 })
|
|
104
|
+
const markerRefs = new Map([['marker-1', markerEl]])
|
|
105
|
+
const markerItems = [{ id: 'marker-1', coords: [1, 2] }]
|
|
106
|
+
|
|
107
|
+
const { result, deps } = setup({}, markerItems, markerRefs)
|
|
108
|
+
click(result)
|
|
109
|
+
|
|
110
|
+
expect(deps.pluginState.dispatch).not.toHaveBeenCalledWith(
|
|
111
|
+
expect.objectContaining({ type: 'TOGGLE_SELECTED_FEATURES' })
|
|
112
|
+
)
|
|
113
|
+
expect(featureQueries.getFeaturesAtPoint).not.toHaveBeenCalled()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('skips markers with no ref and continues to next', () => {
|
|
117
|
+
// marker in items but not in markerRefs — should not throw, should fall through to features
|
|
118
|
+
const markerItems = [{ id: 'marker-no-ref', coords: [1, 2] }]
|
|
119
|
+
const markerRefs = new Map() // no entry for marker-no-ref
|
|
120
|
+
|
|
121
|
+
const { result, deps } = setup({}, markerItems, markerRefs)
|
|
122
|
+
click(result)
|
|
123
|
+
|
|
124
|
+
expect(featureQueries.getFeaturesAtPoint).toHaveBeenCalled()
|
|
125
|
+
expect(deps.pluginState.dispatch).toHaveBeenCalledWith(
|
|
126
|
+
expect.objectContaining({ type: 'TOGGLE_SELECTED_FEATURES' })
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('uses zero offset when marker element has no parentElement', () => {
|
|
131
|
+
// parentElement is null — fallback parentRect { left: 0, top: 0 } should be used
|
|
132
|
+
const markerEl = {
|
|
133
|
+
getBoundingClientRect: () => ({ left: 5, top: 15, right: 15, bottom: 25 }),
|
|
134
|
+
parentElement: null
|
|
135
|
+
}
|
|
136
|
+
const markerRefs = new Map([['marker-1', markerEl]])
|
|
137
|
+
const markerItems = [{ id: 'marker-1', coords: [1, 2] }]
|
|
138
|
+
|
|
139
|
+
const { result, deps } = setup({}, markerItems, markerRefs)
|
|
140
|
+
click(result)
|
|
141
|
+
|
|
142
|
+
// click point { x: 10, y: 20 } is within [5,15,15,25] with zero parent offset
|
|
143
|
+
expect(deps.pluginState.dispatch).toHaveBeenCalledWith({
|
|
144
|
+
type: 'TOGGLE_SELECTED_MARKERS',
|
|
145
|
+
payload: { markerId: 'marker-1', multiSelect: false }
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('falls through to feature selection when click misses all markers', () => {
|
|
150
|
+
// marker bounds don't include the click point { x: 10, y: 20 }
|
|
151
|
+
const markerEl = makeMarkerEl({ left: 50, top: 50, right: 80, bottom: 80 })
|
|
152
|
+
const markerRefs = new Map([['marker-1', markerEl]])
|
|
153
|
+
const markerItems = [{ id: 'marker-1', coords: [1, 2] }]
|
|
154
|
+
|
|
155
|
+
const { result, deps } = setup({}, markerItems, markerRefs)
|
|
156
|
+
click(result)
|
|
157
|
+
|
|
158
|
+
expect(featureQueries.getFeaturesAtPoint).toHaveBeenCalled()
|
|
159
|
+
expect(deps.pluginState.dispatch).toHaveBeenCalledWith(
|
|
160
|
+
expect.objectContaining({ type: 'TOGGLE_SELECTED_FEATURES' })
|
|
161
|
+
)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
/* ------------------------------------------------------------------ */
|
|
166
|
+
/* placeMarker mode */
|
|
77
167
|
/* ------------------------------------------------------------------ */
|
|
78
168
|
|
|
79
|
-
describe('
|
|
169
|
+
describe('placeMarker mode', () => {
|
|
80
170
|
it('places marker, clears selection, and emits event', () => {
|
|
81
|
-
const { result, deps } = setup({
|
|
171
|
+
const { result, deps } = setup({ interactionModes: ['placeMarker'] })
|
|
82
172
|
|
|
83
173
|
click(result)
|
|
84
174
|
|
|
@@ -88,7 +178,7 @@ describe('marker mode', () => {
|
|
|
88
178
|
expect(deps.mapState.markers.add).toHaveBeenCalledWith(
|
|
89
179
|
'location',
|
|
90
180
|
[1, 2],
|
|
91
|
-
{
|
|
181
|
+
{ symbol: 'pin', backgroundColor: 'red' }
|
|
92
182
|
)
|
|
93
183
|
expect(deps.services.eventBus.emit).toHaveBeenCalledWith(
|
|
94
184
|
'interact:markerchange',
|
|
@@ -98,32 +188,30 @@ describe('marker mode', () => {
|
|
|
98
188
|
})
|
|
99
189
|
|
|
100
190
|
/* ------------------------------------------------------------------ */
|
|
101
|
-
/*
|
|
191
|
+
/* selectFeature mode */
|
|
102
192
|
/* ------------------------------------------------------------------ */
|
|
103
193
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const { result, deps } = setup({ interactionMode: mode })
|
|
194
|
+
it('selectFeature mode dispatches selection for matching feature', () => {
|
|
195
|
+
const { result, deps } = setup({ interactionModes: ['selectFeature'] })
|
|
107
196
|
|
|
108
|
-
|
|
197
|
+
click(result)
|
|
109
198
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
})
|
|
199
|
+
expect(deps.pluginState.dispatch).toHaveBeenCalledWith(
|
|
200
|
+
expect.objectContaining({
|
|
201
|
+
type: 'TOGGLE_SELECTED_FEATURES',
|
|
202
|
+
payload: expect.objectContaining({
|
|
203
|
+
featureId: 'P1',
|
|
204
|
+
layerId: 'parcels'
|
|
117
205
|
})
|
|
118
|
-
)
|
|
119
|
-
|
|
206
|
+
})
|
|
207
|
+
)
|
|
120
208
|
})
|
|
121
209
|
|
|
122
|
-
it('falls back to
|
|
210
|
+
it('falls back to placeMarker when selectFeature finds no match', () => {
|
|
123
211
|
featureQueries.getFeaturesAtPoint.mockReturnValue([])
|
|
124
212
|
featureQueries.findMatchingFeature.mockReturnValue(null)
|
|
125
213
|
|
|
126
|
-
const { result, deps } = setup({
|
|
214
|
+
const { result, deps } = setup({ interactionModes: ['selectFeature', 'placeMarker'] })
|
|
127
215
|
|
|
128
216
|
click(result)
|
|
129
217
|
|
|
@@ -248,32 +336,58 @@ describe('deselectOnClickOutside', () => {
|
|
|
248
336
|
})
|
|
249
337
|
|
|
250
338
|
/* ------------------------------------------------------------------ */
|
|
251
|
-
/*
|
|
339
|
+
/* placeMarker / selectFeature guards */
|
|
252
340
|
/* ------------------------------------------------------------------ */
|
|
253
341
|
|
|
254
|
-
it('
|
|
342
|
+
it('places marker with placeMarker mode even when no layers exist', () => {
|
|
255
343
|
featureQueries.getFeaturesAtPoint.mockReturnValue([])
|
|
256
344
|
featureQueries.findMatchingFeature.mockReturnValue(null)
|
|
257
345
|
|
|
258
346
|
const { result, deps } = setup({
|
|
259
|
-
|
|
260
|
-
|
|
347
|
+
interactionModes: ['selectFeature', 'placeMarker'],
|
|
348
|
+
layers: []
|
|
261
349
|
})
|
|
262
350
|
|
|
263
351
|
click(result)
|
|
264
352
|
|
|
353
|
+
expect(deps.mapState.markers.add).toHaveBeenCalled()
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it('does not place marker when placeMarker is not in interactionModes', () => {
|
|
357
|
+
featureQueries.getFeaturesAtPoint.mockReturnValue([])
|
|
358
|
+
featureQueries.findMatchingFeature.mockReturnValue(null)
|
|
359
|
+
|
|
360
|
+
const { result, deps } = setup({ interactionModes: ['selectFeature'] })
|
|
361
|
+
|
|
362
|
+
click(result)
|
|
363
|
+
|
|
265
364
|
expect(deps.mapState.markers.add).not.toHaveBeenCalled()
|
|
266
365
|
})
|
|
267
366
|
|
|
367
|
+
it('does not check markers when selectMarker is not in interactionModes', () => {
|
|
368
|
+
const markerEl = makeMarkerEl({ left: 5, top: 15, right: 15, bottom: 25 })
|
|
369
|
+
const markerRefs = new Map([['marker-1', markerEl]])
|
|
370
|
+
const markerItems = [{ id: 'marker-1', coords: [1, 2] }]
|
|
371
|
+
|
|
372
|
+
const { result, deps } = setup({ interactionModes: ['selectFeature'] }, markerItems, markerRefs)
|
|
373
|
+
click(result)
|
|
374
|
+
|
|
375
|
+
expect(deps.pluginState.dispatch).not.toHaveBeenCalledWith(
|
|
376
|
+
expect.objectContaining({ type: 'TOGGLE_SELECTED_MARKERS' })
|
|
377
|
+
)
|
|
378
|
+
expect(featureQueries.getFeaturesAtPoint).toHaveBeenCalled()
|
|
379
|
+
})
|
|
380
|
+
|
|
268
381
|
/* ------------------------------------------------------------------ */
|
|
269
382
|
/* Selection change event */
|
|
270
383
|
/* ------------------------------------------------------------------ */
|
|
271
384
|
|
|
272
385
|
it('emits selectionchange once when bounds exist', () => {
|
|
273
386
|
const deps = {
|
|
274
|
-
mapState: { markers: { add: jest.fn(), remove: jest.fn() } },
|
|
387
|
+
mapState: { markers: { add: jest.fn(), remove: jest.fn(), items: [], markerRefs: new Map() } },
|
|
275
388
|
pluginState: {
|
|
276
389
|
selectedFeatures: [{ featureId: 'F1' }],
|
|
390
|
+
selectedMarkers: [],
|
|
277
391
|
selectionBounds: { sw: [0, 0], ne: [1, 1] }
|
|
278
392
|
},
|
|
279
393
|
services: { eventBus: { emit: jest.fn() } },
|
|
@@ -286,6 +400,7 @@ it('emits selectionchange once when bounds exist', () => {
|
|
|
286
400
|
'interact:selectionchange',
|
|
287
401
|
expect.objectContaining({
|
|
288
402
|
selectedFeatures: deps.pluginState.selectedFeatures,
|
|
403
|
+
selectedMarkers: [],
|
|
289
404
|
selectionBounds: deps.pluginState.selectionBounds,
|
|
290
405
|
canMerge: false,
|
|
291
406
|
canSplit: false
|
|
@@ -299,8 +414,8 @@ it('skips emission when selection remains empty after being cleared', () => {
|
|
|
299
414
|
// 1. First render with a feature (prev is null, emission happens)
|
|
300
415
|
const { rerender } = renderHook(
|
|
301
416
|
({ features }) => useInteractionHandlers({
|
|
302
|
-
mapState: { markers: {} },
|
|
303
|
-
pluginState: { selectedFeatures: features, selectionBounds: { b: 1 } },
|
|
417
|
+
mapState: { markers: { items: [], markerRefs: new Map() } },
|
|
418
|
+
pluginState: { selectedFeatures: features, selectedMarkers: [], selectionBounds: { b: 1 } },
|
|
304
419
|
services: { eventBus },
|
|
305
420
|
mapProvider: {}
|
|
306
421
|
}),
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// /plugins/interact/manifest.js
|
|
2
2
|
import { InteractInit } from './InteractInit.jsx'
|
|
3
|
+
import { isSelectMarkerOnly } from './utils/interactionModes.js'
|
|
3
4
|
import { initialState, actions } from './reducer.js'
|
|
4
5
|
import { enable } from './api/enable.js'
|
|
5
6
|
import { disable } from './api/disable.js'
|
|
6
7
|
import { clear } from './api/clear.js'
|
|
7
8
|
import { selectFeature } from './api/selectFeature.js'
|
|
8
9
|
import { unselectFeature } from './api/unselectFeature.js'
|
|
10
|
+
import { selectMarker } from './api/selectMarker.js'
|
|
11
|
+
import { unselectMarker } from './api/unselectMarker.js'
|
|
9
12
|
|
|
10
13
|
export const manifest = {
|
|
11
14
|
InitComponent: InteractInit,
|
|
@@ -36,7 +39,10 @@ export const manifest = {
|
|
|
36
39
|
id: 'selectAtTarget',
|
|
37
40
|
label: 'Select',
|
|
38
41
|
variant: 'primary',
|
|
39
|
-
|
|
42
|
+
// Hidden for touch when selectMarker is the only mode — markers have a sufficient tap target
|
|
43
|
+
// and the Select button is only needed alongside the crosshair. Mirrors the crosshair logic in InteractInit.
|
|
44
|
+
hiddenWhen: ({ appState, pluginState }) =>
|
|
45
|
+
!pluginState.enabled || appState.interfaceType !== 'touch' || isSelectMarkerOnly(pluginState.interactionModes),
|
|
40
46
|
mobile: {
|
|
41
47
|
slot: 'actions'
|
|
42
48
|
},
|
|
@@ -83,6 +89,8 @@ export const manifest = {
|
|
|
83
89
|
disable,
|
|
84
90
|
clear,
|
|
85
91
|
selectFeature,
|
|
86
|
-
unselectFeature
|
|
92
|
+
unselectFeature,
|
|
93
|
+
selectMarker,
|
|
94
|
+
unselectMarker
|
|
87
95
|
}
|
|
88
96
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
const initialState = {
|
|
2
2
|
enabled: false,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
layers: [],
|
|
4
|
+
marker: null,
|
|
5
|
+
interactionModes: null,
|
|
6
6
|
multiSelect: false,
|
|
7
7
|
contiguous: false,
|
|
8
8
|
deselectOnClickOutside: false,
|
|
9
9
|
selectedFeatures: [],
|
|
10
|
+
selectedMarkers: [],
|
|
10
11
|
selectionBounds: null,
|
|
11
12
|
closeOnAction: true // Done or Cancel
|
|
12
13
|
}
|
|
@@ -24,6 +25,7 @@ const disable = (state) => {
|
|
|
24
25
|
...state,
|
|
25
26
|
enabled: false,
|
|
26
27
|
selectedFeatures: [],
|
|
28
|
+
selectedMarkers: [],
|
|
27
29
|
selectionBounds: null
|
|
28
30
|
}
|
|
29
31
|
}
|
|
@@ -67,7 +69,7 @@ const toggleSelectedFeatures = (state, payload) => {
|
|
|
67
69
|
nextSelected = isSameSingle ? [] : [featureObj]
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
return { ...state, selectedFeatures: nextSelected, selectionBounds: null }
|
|
72
|
+
return { ...state, selectedFeatures: nextSelected, selectedMarkers: multiSelect && !replaceAll ? state.selectedMarkers : [], selectionBounds: null }
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
// Update bounds (called from useEffect after map provider calculates them)
|
|
@@ -81,20 +83,72 @@ const updateSelectedBounds = (state, payload) => {
|
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
const toggleSelectedMarkers = (state, { markerId, multiSelect }) => {
|
|
87
|
+
const current = state.selectedMarkers
|
|
88
|
+
const exists = current.includes(markerId)
|
|
89
|
+
if (multiSelect) {
|
|
90
|
+
const next = exists ? current.filter(id => id !== markerId) : [...current, markerId]
|
|
91
|
+
return { ...state, selectedMarkers: next }
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
...state,
|
|
95
|
+
selectedFeatures: [],
|
|
96
|
+
selectionBounds: null,
|
|
97
|
+
selectedMarkers: exists && current.length === 1 ? [] : [markerId]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
84
101
|
const clearSelectedFeatures = (state) => {
|
|
85
102
|
return {
|
|
86
103
|
...state,
|
|
87
104
|
selectedFeatures: [],
|
|
105
|
+
selectedMarkers: [],
|
|
88
106
|
selectionBounds: null
|
|
89
107
|
}
|
|
90
108
|
}
|
|
91
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Explicitly select a marker. Has no effect if already selected.
|
|
112
|
+
* In single-select mode, clears selectedFeatures and replaces the selection.
|
|
113
|
+
* In multi-select mode, adds to the existing selection.
|
|
114
|
+
*/
|
|
115
|
+
const selectMarker = (state, { markerId, multiSelect }) => {
|
|
116
|
+
const current = state.selectedMarkers
|
|
117
|
+
if (current.includes(markerId)) {
|
|
118
|
+
return state
|
|
119
|
+
}
|
|
120
|
+
const nextMarkers = multiSelect ? [...current, markerId] : [markerId]
|
|
121
|
+
return {
|
|
122
|
+
...state,
|
|
123
|
+
selectedFeatures: multiSelect ? state.selectedFeatures : [],
|
|
124
|
+
selectionBounds: null,
|
|
125
|
+
selectedMarkers: nextMarkers
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Explicitly unselect a marker. Has no effect if not selected.
|
|
131
|
+
*/
|
|
132
|
+
const unselectMarker = (state, { markerId }) => {
|
|
133
|
+
const current = state.selectedMarkers
|
|
134
|
+
if (!current.includes(markerId)) {
|
|
135
|
+
return state
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
...state,
|
|
139
|
+
selectedMarkers: current.filter(id => id !== markerId)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
92
143
|
const actions = {
|
|
93
144
|
ENABLE: enable,
|
|
94
145
|
DISABLE: disable,
|
|
95
146
|
TOGGLE_SELECTED_FEATURES: toggleSelectedFeatures,
|
|
147
|
+
TOGGLE_SELECTED_MARKERS: toggleSelectedMarkers,
|
|
96
148
|
UPDATE_SELECTED_BOUNDS: updateSelectedBounds,
|
|
97
|
-
CLEAR_SELECTED_FEATURES: clearSelectedFeatures
|
|
149
|
+
CLEAR_SELECTED_FEATURES: clearSelectedFeatures,
|
|
150
|
+
SELECT_MARKER: selectMarker,
|
|
151
|
+
UNSELECT_MARKER: unselectMarker
|
|
98
152
|
}
|
|
99
153
|
|
|
100
154
|
export {
|
|
@@ -4,13 +4,14 @@ describe('initialState', () => {
|
|
|
4
4
|
it('has correct defaults', () => {
|
|
5
5
|
expect(initialState).toEqual({
|
|
6
6
|
enabled: false,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
layers: [],
|
|
8
|
+
marker: null,
|
|
9
|
+
interactionModes: null,
|
|
10
10
|
multiSelect: false,
|
|
11
11
|
contiguous: false,
|
|
12
12
|
deselectOnClickOutside: false,
|
|
13
13
|
selectedFeatures: [],
|
|
14
|
+
selectedMarkers: [],
|
|
14
15
|
selectionBounds: null,
|
|
15
16
|
closeOnAction: true
|
|
16
17
|
})
|
|
@@ -20,23 +21,26 @@ describe('initialState', () => {
|
|
|
20
21
|
describe('ENABLE/DISABLE actions', () => {
|
|
21
22
|
it('ENABLE sets enabled and merges payload', () => {
|
|
22
23
|
const state = { ...initialState, enabled: false }
|
|
23
|
-
const
|
|
24
|
+
const marker = { symbol: 'pin', backgroundColor: 'red' }
|
|
25
|
+
const payload = { layers: [1], marker }
|
|
24
26
|
const result = actions.ENABLE(state, payload)
|
|
25
27
|
|
|
26
28
|
expect(result.enabled).toBe(true)
|
|
27
|
-
expect(result.
|
|
28
|
-
expect(result.
|
|
29
|
+
expect(result.layers).toEqual([1])
|
|
30
|
+
expect(result.marker).toEqual(marker)
|
|
29
31
|
expect(result).not.toBe(state)
|
|
30
32
|
})
|
|
31
33
|
|
|
32
|
-
it('DISABLE sets enabled to false, clears selection, and preserves other state', () => {
|
|
33
|
-
const
|
|
34
|
+
it('DISABLE sets enabled to false, clears selection and markers, and preserves other state', () => {
|
|
35
|
+
const marker = { symbol: 'pin', backgroundColor: 'red' }
|
|
36
|
+
const state = { ...initialState, enabled: true, layers: [1], marker, selectedFeatures: [{ featureId: 'f1' }], selectedMarkers: ['m1'], selectionBounds: [0, 0, 1, 1] }
|
|
34
37
|
const result = actions.DISABLE(state)
|
|
35
38
|
|
|
36
39
|
expect(result.enabled).toBe(false)
|
|
37
|
-
expect(result.
|
|
38
|
-
expect(result.
|
|
40
|
+
expect(result.layers).toEqual([1])
|
|
41
|
+
expect(result.marker).toEqual(marker)
|
|
39
42
|
expect(result.selectedFeatures).toEqual([])
|
|
43
|
+
expect(result.selectedMarkers).toEqual([])
|
|
40
44
|
expect(result.selectionBounds).toBeNull()
|
|
41
45
|
expect(result).not.toBe(state)
|
|
42
46
|
})
|
|
@@ -96,6 +100,19 @@ describe('TOGGLE_SELECTED_FEATURES action', () => {
|
|
|
96
100
|
expect(state.selectedFeatures[0].featureId).toBe('f3')
|
|
97
101
|
})
|
|
98
102
|
|
|
103
|
+
it('clears selectedMarkers in single-select; preserves them in multi-select', () => {
|
|
104
|
+
const state = { ...initialState, selectedMarkers: ['m1'] }
|
|
105
|
+
const feature = createFeature('f1')
|
|
106
|
+
|
|
107
|
+
// single-select clears markers
|
|
108
|
+
const single = actions.TOGGLE_SELECTED_FEATURES(state, feature)
|
|
109
|
+
expect(single.selectedMarkers).toEqual([])
|
|
110
|
+
|
|
111
|
+
// multi-select preserves markers
|
|
112
|
+
const multi = actions.TOGGLE_SELECTED_FEATURES(state, { ...feature, multiSelect: true })
|
|
113
|
+
expect(multi.selectedMarkers).toEqual(['m1'])
|
|
114
|
+
})
|
|
115
|
+
|
|
99
116
|
it('handles null or empty selectedFeatures gracefully', () => {
|
|
100
117
|
let state = { ...initialState, selectedFeatures: null }
|
|
101
118
|
state = actions.TOGGLE_SELECTED_FEATURES(state, createFeature('f1'))
|
|
@@ -127,28 +144,99 @@ describe('UPDATE_SELECTED_BOUNDS action', () => {
|
|
|
127
144
|
})
|
|
128
145
|
})
|
|
129
146
|
|
|
147
|
+
describe('TOGGLE_SELECTED_MARKERS action', () => {
|
|
148
|
+
it('selects a marker in single-select mode and clears features', () => {
|
|
149
|
+
const state = { ...initialState, selectedFeatures: [{ featureId: 'f1' }], selectionBounds: { sw: [0, 0], ne: [1, 1] } }
|
|
150
|
+
const result = actions.TOGGLE_SELECTED_MARKERS(state, { markerId: 'm1', multiSelect: false })
|
|
151
|
+
expect(result.selectedMarkers).toEqual(['m1'])
|
|
152
|
+
expect(result.selectedFeatures).toEqual([])
|
|
153
|
+
expect(result.selectionBounds).toBeNull()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('toggles off the only selected marker in single-select mode', () => {
|
|
157
|
+
const state = { ...initialState, selectedMarkers: ['m1'] }
|
|
158
|
+
const result = actions.TOGGLE_SELECTED_MARKERS(state, { markerId: 'm1', multiSelect: false })
|
|
159
|
+
expect(result.selectedMarkers).toEqual([])
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('adds a marker in multi-select mode without clearing features', () => {
|
|
163
|
+
const state = { ...initialState, selectedFeatures: [{ featureId: 'f1' }], selectedMarkers: ['m1'] }
|
|
164
|
+
const result = actions.TOGGLE_SELECTED_MARKERS(state, { markerId: 'm2', multiSelect: true })
|
|
165
|
+
expect(result.selectedMarkers).toEqual(['m1', 'm2'])
|
|
166
|
+
expect(result.selectedFeatures).toEqual([{ featureId: 'f1' }])
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('removes a marker in multi-select mode', () => {
|
|
170
|
+
const state = { ...initialState, selectedMarkers: ['m1', 'm2'] }
|
|
171
|
+
const result = actions.TOGGLE_SELECTED_MARKERS(state, { markerId: 'm1', multiSelect: true })
|
|
172
|
+
expect(result.selectedMarkers).toEqual(['m2'])
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
130
176
|
describe('CLEAR_SELECTED_FEATURES action', () => {
|
|
131
|
-
it('resets
|
|
177
|
+
it('resets features, markers and bounds', () => {
|
|
132
178
|
const state = {
|
|
133
179
|
...initialState,
|
|
134
180
|
selectedFeatures: [1],
|
|
181
|
+
selectedMarkers: ['m1'],
|
|
135
182
|
selectionBounds: { sw: [0, 0], ne: [1, 1] }
|
|
136
183
|
}
|
|
137
184
|
const result = actions.CLEAR_SELECTED_FEATURES(state)
|
|
138
185
|
expect(result.selectedFeatures).toEqual([])
|
|
186
|
+
expect(result.selectedMarkers).toEqual([])
|
|
139
187
|
expect(result.selectionBounds).toBeNull()
|
|
140
188
|
expect(result).not.toBe(state)
|
|
141
189
|
})
|
|
142
190
|
})
|
|
143
191
|
|
|
192
|
+
describe('SELECT_MARKER action', () => {
|
|
193
|
+
it('adds a marker in single-select mode, clearing selectedFeatures', () => {
|
|
194
|
+
const state = { ...initialState, selectedFeatures: [{ featureId: 'f1' }], selectedMarkers: [] }
|
|
195
|
+
const result = actions.SELECT_MARKER(state, { markerId: 'm1', multiSelect: false })
|
|
196
|
+
expect(result.selectedMarkers).toEqual(['m1'])
|
|
197
|
+
expect(result.selectedFeatures).toEqual([])
|
|
198
|
+
expect(result.selectionBounds).toBeNull()
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('adds a marker in multi-select mode without clearing selectedFeatures', () => {
|
|
202
|
+
const state = { ...initialState, selectedFeatures: [{ featureId: 'f1' }], selectedMarkers: ['m1'] }
|
|
203
|
+
const result = actions.SELECT_MARKER(state, { markerId: 'm2', multiSelect: true })
|
|
204
|
+
expect(result.selectedMarkers).toEqual(['m1', 'm2'])
|
|
205
|
+
expect(result.selectedFeatures).toEqual([{ featureId: 'f1' }])
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('is idempotent — does not change state if marker already selected', () => {
|
|
209
|
+
const state = { ...initialState, selectedMarkers: ['m1'] }
|
|
210
|
+
const result = actions.SELECT_MARKER(state, { markerId: 'm1', multiSelect: false })
|
|
211
|
+
expect(result).toBe(state)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('UNSELECT_MARKER action', () => {
|
|
216
|
+
it('removes a selected marker', () => {
|
|
217
|
+
const state = { ...initialState, selectedMarkers: ['m1', 'm2'] }
|
|
218
|
+
const result = actions.UNSELECT_MARKER(state, { markerId: 'm1' })
|
|
219
|
+
expect(result.selectedMarkers).toEqual(['m2'])
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('is idempotent — does not change state if marker not selected', () => {
|
|
223
|
+
const state = { ...initialState, selectedMarkers: ['m2'] }
|
|
224
|
+
const result = actions.UNSELECT_MARKER(state, { markerId: 'm1' })
|
|
225
|
+
expect(result).toBe(state)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
144
229
|
describe('actions object', () => {
|
|
145
230
|
it('exports all action handlers as functions', () => {
|
|
146
231
|
expect(Object.keys(actions)).toEqual([
|
|
147
232
|
'ENABLE',
|
|
148
233
|
'DISABLE',
|
|
149
234
|
'TOGGLE_SELECTED_FEATURES',
|
|
235
|
+
'TOGGLE_SELECTED_MARKERS',
|
|
150
236
|
'UPDATE_SELECTED_BOUNDS',
|
|
151
|
-
'CLEAR_SELECTED_FEATURES'
|
|
237
|
+
'CLEAR_SELECTED_FEATURES',
|
|
238
|
+
'SELECT_MARKER',
|
|
239
|
+
'UNSELECT_MARKER'
|
|
152
240
|
])
|
|
153
241
|
Object.values(actions).forEach(fn => expect(typeof fn).toBe('function'))
|
|
154
242
|
})
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import { DEFAULTS } from '../defaults.js'
|
|
2
1
|
import { getValueForStyle } from '../../../../src/utils/getValueForStyle.js'
|
|
3
2
|
|
|
3
|
+
const DEFAULT_STROKE_WIDTH = 3
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a map of layerId → resolved highlight style for the given data layers.
|
|
7
|
+
*
|
|
8
|
+
* Stroke colour resolution order:
|
|
9
|
+
* layer.selectedStroke → mapStyle.selectedColor → mapColorScheme scheme default
|
|
10
|
+
*
|
|
11
|
+
* @param {Object[]} dataLayers
|
|
12
|
+
* @param {Object} mapStyle - Current map style config
|
|
13
|
+
* @returns {Object} layerId → { stroke, fill, strokeWidth }
|
|
14
|
+
*/
|
|
4
15
|
export const buildStylesMap = (dataLayers, mapStyle) => {
|
|
5
16
|
const stylesMap = {}
|
|
6
17
|
|
|
@@ -8,10 +19,12 @@ export const buildStylesMap = (dataLayers, mapStyle) => {
|
|
|
8
19
|
return stylesMap
|
|
9
20
|
}
|
|
10
21
|
|
|
22
|
+
const schemeSelectedColor = mapStyle.mapColorScheme === 'dark' ? '#ffffff' : '#0b0c0c'
|
|
23
|
+
|
|
11
24
|
dataLayers.forEach(layer => {
|
|
12
|
-
const stroke = layer.selectedStroke ||
|
|
13
|
-
const fill = layer.selectedFill ||
|
|
14
|
-
const strokeWidth = layer.selectedStrokeWidth ||
|
|
25
|
+
const stroke = layer.selectedStroke || mapStyle.selectedColor || schemeSelectedColor
|
|
26
|
+
const fill = layer.selectedFill || 'transparent'
|
|
27
|
+
const strokeWidth = layer.selectedStrokeWidth || DEFAULT_STROKE_WIDTH
|
|
15
28
|
|
|
16
29
|
stylesMap[layer.layerId] = {
|
|
17
30
|
stroke: getValueForStyle(stroke, mapStyle.id),
|
|
@@ -34,11 +34,25 @@ describe('buildStylesMap', () => {
|
|
|
34
34
|
})
|
|
35
35
|
|
|
36
36
|
// Default fallback values
|
|
37
|
-
expect(result.custom2.stroke).toBe(
|
|
38
|
-
expect(result.custom2.fill).toBe(
|
|
37
|
+
expect(result.custom2.stroke).toBe('#0b0c0c')
|
|
38
|
+
expect(result.custom2.fill).toBe('transparent')
|
|
39
39
|
expect(result.custom2.strokeWidth).toBe(DEFAULTS.selectedStrokeWidth)
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
+
it('uses mapStyle.selectedColor as default stroke when no layer override', () => {
|
|
43
|
+
const dataLayers = [{ layerId: 'layer1' }]
|
|
44
|
+
const mapStyle = { id: 'test', selectedColor: '#336699' }
|
|
45
|
+
const result = buildStylesMap(dataLayers, mapStyle)
|
|
46
|
+
expect(result.layer1.stroke).toBe('#336699')
|
|
47
|
+
expect(result.layer1.fill).toBe('transparent')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('uses scheme default when mapStyle has no selectedColor', () => {
|
|
51
|
+
const dataLayers = [{ layerId: 'layer1' }]
|
|
52
|
+
expect(buildStylesMap(dataLayers, { id: 'light' }).layer1.stroke).toBe('#0b0c0c')
|
|
53
|
+
expect(buildStylesMap(dataLayers, { id: 'dark', mapColorScheme: 'dark' }).layer1.stroke).toBe('#ffffff')
|
|
54
|
+
})
|
|
55
|
+
|
|
42
56
|
it('calls getValueForStyle for stroke and fill with mapStyle.id', () => {
|
|
43
57
|
const dataLayers = [
|
|
44
58
|
{ layerId: 'layer1', selectedStroke: 'strokeVal', selectedFill: 'fillVal' }
|
|
@@ -15,12 +15,17 @@ export const getFeaturesAtPoint = (mapProvider, point, options) => {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const isPointGeometry = (feature) => {
|
|
19
|
+
const type = feature.geometry?.type
|
|
20
|
+
return type === 'Point' || type === 'MultiPoint'
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
export const findMatchingFeature = (features, layerConfigMap) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
+
const matched = features.filter(f => layerConfigMap[f.layer?.id])
|
|
25
|
+
const pointMatch = matched.find(isPointGeometry)
|
|
26
|
+
if (pointMatch) {
|
|
27
|
+
return { feature: pointMatch, config: layerConfigMap[pointMatch.layer.id] }
|
|
24
28
|
}
|
|
25
|
-
|
|
29
|
+
const first = matched[0]
|
|
30
|
+
return first ? { feature: first, config: layerConfigMap[first.layer.id] } : null
|
|
26
31
|
}
|