@defra/interactive-map 0.0.43-alpha → 0.0.44-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.md +16 -8
- package/docs/plugins/draw.md +1 -1
- package/package.json +1 -1
- 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/draw-ol/dist/esm/im-draw-ol-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/datasets/dist/css/index.css +1 -1
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/src/components/LayersMenu/Layers.module.scss +1 -0
- package/plugins/datasets/src/initialise/DatasetsInit.jsx +2 -2
- package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +6 -5
- package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/esm/index.js +1 -1
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/umd/index.js +1 -1
- package/plugins/draw/src/DrawInit.jsx +31 -26
- package/plugins/draw/src/DrawInit.test.jsx +55 -12
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +113 -114
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +82 -9
- package/plugins/draw/src/adapters/maplibre/drawEvents.js +3 -1
- package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +18 -9
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +9 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +11 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +8 -2
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +35 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +29 -19
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +17 -2
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +35 -29
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +24 -6
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +22 -20
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +3 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +31 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +8 -1
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +38 -30
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +20 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +5 -5
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +3 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -7
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +4 -4
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +14 -14
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +12 -32
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +10 -10
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +48 -22
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +74 -6
- package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +2 -2
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +6 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +9 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +15 -2
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +27 -1
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +1 -1
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +31 -17
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +55 -1
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +13 -7
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +7 -15
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +36 -4
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +41 -1
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +35 -25
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +40 -2
- package/plugins/draw/src/adapters/openlayers/edit/nudge.js +2 -2
- package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +2 -2
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +32 -57
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +14 -13
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +1 -1
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +17 -1
- package/plugins/draw/src/events.js +4 -4
- package/plugins/draw/src/events.test.js +1 -1
- package/plugins/draw/src/hooks/useSpatialList.js +180 -0
- package/plugins/draw/src/hooks/useSpatialList.test.js +339 -0
- package/plugins/draw/src/utils/spatial.js +1 -31
- package/plugins/draw/src/utils/spatial.test.js +1 -24
- 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 +2 -2
- package/plugins/interact/src/InteractInit.test.js +3 -3
- package/plugins/interact/src/events.js +5 -0
- package/plugins/interact/src/hooks/useAttachEvents.js +20 -0
- package/plugins/interact/src/hooks/useAttachEvents.test.js +44 -1
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +4 -4
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +7 -7
- package/plugins/interact/src/hooks/{useMapItemList.js → useSpatialList.js} +87 -52
- package/plugins/interact/src/hooks/{useMapItemList.test.js → useSpatialList.test.js} +152 -71
- package/plugins/interact/src/manifest.js +12 -1
- package/plugins/interact/src/manifest.test.js +20 -0
- package/plugins/interact/src/utils/spatial.js +1 -1
- package/providers/beta/openlayers/src/utils/queryFeatures.js +1 -1
- 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/utils/labels.js +2 -2
- package/providers/maplibre/src/utils/labels.test.js +5 -5
- package/providers/maplibre/src/utils/spatial.js +0 -57
- package/providers/maplibre/src/utils/spatial.test.js +0 -27
- package/src/App/components/CrossHair/CrossHair.jsx +10 -4
- package/src/App/components/CrossHair/CrossHair.module.scss +16 -12
- package/src/App/components/CrossHair/CrossHair.test.jsx +9 -4
- package/src/App/components/Hints/Hints.jsx +3 -3
- package/src/App/components/Hints/Hints.test.jsx +3 -3
- package/src/App/components/MapButton/MapButton.module.scss +1 -2
- package/src/App/components/{MoveControls/MoveControls.jsx → MapControls/MapControls.jsx} +10 -10
- package/src/App/components/{MoveControls/MoveControls.module.scss → MapControls/MapControls.module.scss} +25 -25
- package/src/App/components/{MoveControls/MoveControls.test.jsx → MapControls/MapControls.test.jsx} +48 -48
- package/src/App/components/Markers/Markers.jsx +2 -2
- package/src/App/components/Markers/Markers.test.jsx +3 -3
- package/src/App/components/Markers/SymbolMarker.test.jsx +2 -2
- package/src/App/components/Viewport/MapStatus.jsx +1 -1
- package/src/App/components/Viewport/SpatialList.jsx +47 -0
- package/src/App/components/Viewport/{Features.module.scss → SpatialList.module.scss} +3 -3
- package/src/App/components/Viewport/{Features.test.jsx → SpatialList.test.jsx} +60 -43
- package/src/App/components/Viewport/Viewport.jsx +20 -10
- package/src/App/components/Viewport/Viewport.test.jsx +18 -2
- package/src/App/hooks/useCrossHairAPI.js +5 -5
- package/src/App/hooks/useCrossHairAPI.test.js +5 -5
- package/src/App/hooks/useHintsAPI.js +5 -5
- package/src/App/hooks/useSpatialListFocus.js +296 -0
- package/src/App/hooks/{useFeatureFocus.test.js → useSpatialListFocus.test.js} +318 -163
- package/src/App/hooks/useSpatialListItems.js +51 -0
- package/src/App/hooks/useSpatialListItems.test.js +217 -0
- package/src/App/initialiseApp.js +6 -3
- package/src/App/initialiseApp.test.js +11 -0
- package/src/App/registry/spatialListRegistry.js +104 -0
- package/src/App/registry/spatialListRegistry.test.js +218 -0
- package/src/App/store/AppProvider.jsx +3 -2
- package/src/config/appConfig.js +14 -14
- package/src/config/appConfig.test.js +19 -19
- package/src/config/defaults.js +2 -2
- package/src/config/events.js +8 -8
- package/src/scss/main.scss +2 -2
- package/src/test-utils.js +8 -0
- package/src/types.js +11 -9
- package/src/utils/findNearestItemInDirection.js +29 -0
- package/src/utils/findNearestItemInDirection.test.js +67 -0
- package/src/utils/globalAltShortcuts.js +12 -0
- package/src/utils/globalAltShortcuts.test.js +27 -0
- package/src/utils/spatialNavigate.js +49 -0
- package/src/utils/spatialNavigate.test.js +42 -0
- package/src/App/components/Viewport/Features.jsx +0 -49
- package/src/App/hooks/useFeatureFocus.js +0 -257
- package/src/App/hooks/useFeatureItems.js +0 -39
- package/src/App/hooks/useFeatureItems.test.js +0 -155
|
@@ -56,7 +56,7 @@ describe('createDrawPointMode', () => {
|
|
|
56
56
|
const { interaction, manager, emitted } = setup()
|
|
57
57
|
interaction.dispatchEvent({ type: 'drawend', feature: pointFeature([1, 2], 'ol-generated-id') })
|
|
58
58
|
|
|
59
|
-
const created = emitted().filter((
|
|
59
|
+
const created = emitted().filter((event) => event.type === ADAPTER_EVENTS.CREATE)
|
|
60
60
|
expect(created).toHaveLength(1)
|
|
61
61
|
expect(created[0].payload.id).toBe('point-1') // re-id'd, not the OL-generated one
|
|
62
62
|
expect(created[0].payload.geometry.coordinates).toEqual([1, 2])
|
|
@@ -82,7 +82,7 @@ describe('createDrawPointMode', () => {
|
|
|
82
82
|
test('defaults properties to {} when none are supplied', () => {
|
|
83
83
|
const { interaction, manager, emitted } = setup({ properties: undefined })
|
|
84
84
|
interaction.dispatchEvent({ type: 'drawend', feature: pointFeature([1, 2], 'ol-generated-id') })
|
|
85
|
-
const created = emitted().filter((
|
|
85
|
+
const created = emitted().filter((event) => event.type === ADAPTER_EVENTS.CREATE)
|
|
86
86
|
// OL's GeoJSON writer reports no properties as null (not {}) — the meaningful
|
|
87
87
|
// assertion is what setProperties() was actually called with, on the stored feature.
|
|
88
88
|
expect(created[0].payload.properties).toBeNull()
|
|
@@ -96,7 +96,7 @@ describe('createDrawPointMode', () => {
|
|
|
96
96
|
const condition = interaction.condition_
|
|
97
97
|
const allowed = condition({ coordinate: [1, 2], originalEvent: {} })
|
|
98
98
|
expect(allowed).toBe(false)
|
|
99
|
-
const blocked = emitted().filter((
|
|
99
|
+
const blocked = emitted().filter((event) => event.type === ADAPTER_EVENTS.PLACEMENT_BLOCKED)
|
|
100
100
|
expect(blocked).toHaveLength(1)
|
|
101
101
|
expect(blocked[0].payload).toMatchObject({ reason: 'not allowed here', phase: 'place', mode: 'draw_point' })
|
|
102
102
|
})
|
|
@@ -116,7 +116,7 @@ describe('createDrawPointMode', () => {
|
|
|
116
116
|
test('drawabort emits CANCEL', () => {
|
|
117
117
|
const { interaction, emitted } = setup()
|
|
118
118
|
interaction.dispatchEvent({ type: 'drawabort' })
|
|
119
|
-
expect(emitted().some((
|
|
119
|
+
expect(emitted().some((event) => event.type === ADAPTER_EVENTS.CANCEL)).toBe(true)
|
|
120
120
|
})
|
|
121
121
|
|
|
122
122
|
describe('touch/keyboard placement (via the wired placeVertex callback)', () => {
|
|
@@ -142,13 +142,13 @@ describe('createDrawPointMode', () => {
|
|
|
142
142
|
expect(finish).toHaveBeenCalled()
|
|
143
143
|
})
|
|
144
144
|
|
|
145
|
-
test('
|
|
146
|
-
const snap = { apply: jest.fn(), hideIndicator: jest.fn() }
|
|
145
|
+
test('still snaps with interfaceType mouse — placeVertex is only ever reached via the crosshair path (Enter, the add-vertex button, or the crosshair\'s own click), never a real mouse click', () => {
|
|
146
|
+
const snap = { apply: jest.fn(() => [9, 9]), hideIndicator: jest.fn() }
|
|
147
147
|
const { wireArgs, interaction } = setup({ snap, interfaceType: 'mouse' })
|
|
148
148
|
const { append } = spyOnCommit(interaction)
|
|
149
149
|
wireArgs.placeVertex()
|
|
150
|
-
expect(snap.apply).
|
|
151
|
-
expect(append).toHaveBeenCalledWith([[
|
|
150
|
+
expect(snap.apply).toHaveBeenCalledWith([5, 5])
|
|
151
|
+
expect(append).toHaveBeenCalledWith([[9, 9]])
|
|
152
152
|
})
|
|
153
153
|
|
|
154
154
|
test('a vetoed crosshair placement never commits', () => {
|
|
@@ -158,7 +158,7 @@ describe('createDrawPointMode', () => {
|
|
|
158
158
|
wireArgs.placeVertex()
|
|
159
159
|
expect(append).not.toHaveBeenCalled()
|
|
160
160
|
expect(finish).not.toHaveBeenCalled()
|
|
161
|
-
expect(emitted().filter((
|
|
161
|
+
expect(emitted().filter((event) => event.type === ADAPTER_EVENTS.PLACEMENT_BLOCKED)).toHaveLength(1)
|
|
162
162
|
})
|
|
163
163
|
|
|
164
164
|
test('updateRubberbanding shows the snap indicator at the crosshair for touch/keyboard, not mouse', () => {
|
|
@@ -169,11 +169,12 @@ describe('createDrawPointMode', () => {
|
|
|
169
169
|
expect(snap.apply).toHaveBeenCalledWith([7, 7])
|
|
170
170
|
})
|
|
171
171
|
|
|
172
|
-
test('updateRubberbanding
|
|
172
|
+
test('updateRubberbanding still refreshes the indicator regardless of interfaceType — the caller decides when it\'s worth calling this at all', () => {
|
|
173
173
|
const snap = { apply: jest.fn(), hideIndicator: jest.fn() }
|
|
174
|
-
const
|
|
174
|
+
const mapProvider = { getCenter: () => [7, 7] }
|
|
175
|
+
const { wireArgs } = setup({ snap, mapProvider, interfaceType: 'mouse' })
|
|
175
176
|
wireArgs.updateRubberbanding()
|
|
176
|
-
expect(snap.apply).
|
|
177
|
+
expect(snap.apply).toHaveBeenCalledWith([7, 7])
|
|
177
178
|
})
|
|
178
179
|
|
|
179
180
|
// Regression: without this, switching to touch/keyboard left the indicator hidden until
|
|
@@ -239,7 +240,7 @@ describe('createDrawPointMode', () => {
|
|
|
239
240
|
const { mode, map, interaction, emitted } = setup()
|
|
240
241
|
const removeSpy = jest.spyOn(map, 'removeInteraction')
|
|
241
242
|
mode.destroy()
|
|
242
|
-
expect(emitted().some((
|
|
243
|
+
expect(emitted().some((event) => event.type === ADAPTER_EVENTS.INTERFACE_TYPE_CHANGE)).toBe(true)
|
|
243
244
|
expect(removeSpy).toHaveBeenCalledWith(interaction)
|
|
244
245
|
})
|
|
245
246
|
|
|
@@ -250,7 +250,7 @@ export const createEditPointMode = ({ map, manager, options }) => {
|
|
|
250
250
|
// Matches MaplibreDrawAdapter.deleteVertex's existing no-op precedent.
|
|
251
251
|
deleteVertex () {},
|
|
252
252
|
|
|
253
|
-
//
|
|
253
|
+
// MapControls' D-pad, routed here via mapProvider.activeMoveTarget (events.js) — claimed
|
|
254
254
|
// once at construction (pointSelectionState's VERTEX_SELECTION) and held for the session.
|
|
255
255
|
nudgeSelectedVertex: keyboardHandler.nudgeByDelta,
|
|
256
256
|
|
|
@@ -208,7 +208,7 @@ describe('mouse hit-testing respects the icon\'s own rendered pixels, not a fixe
|
|
|
208
208
|
})
|
|
209
209
|
})
|
|
210
210
|
|
|
211
|
-
test('nudgeSelectedVertex (
|
|
211
|
+
test('nudgeSelectedVertex (MapControls D-pad) moves the point and is undoable', () => {
|
|
212
212
|
const { manager, mode, coord } = setup()
|
|
213
213
|
mode.nudgeSelectedVertex(1, 0, true)
|
|
214
214
|
expect(coord()).not.toEqual([5, 5])
|
|
@@ -249,6 +249,22 @@ test('keyboard: Delete is a no-op — there is no delete-vertex action for a poi
|
|
|
249
249
|
expect(coord()).toEqual([5, 5])
|
|
250
250
|
})
|
|
251
251
|
|
|
252
|
+
// A point is "always selected" (pointSelectionState.js's selectedVertexIndex never leaves 0),
|
|
253
|
+
// so the shared keyboardHandler.js's Alt+<key> shadow guard (gated on selectedVertexIndex >= 0)
|
|
254
|
+
// stays unconditionally true here — unlike edit_vertex, where it only applies once a vertex is
|
|
255
|
+
// actually selected.
|
|
256
|
+
test('keyboard: Alt+Enter/Alt+Arrow keyup are shadowed from the app-wide label shortcuts for the whole session', () => {
|
|
257
|
+
setup()
|
|
258
|
+
const enterEvent = new KeyboardEvent('keyup', { key: 'Enter', altKey: true, cancelable: true, bubbles: true })
|
|
259
|
+
const arrowEvent = new KeyboardEvent('keyup', { key: 'ArrowRight', altKey: true, cancelable: true, bubbles: true })
|
|
260
|
+
const enterSpy = jest.spyOn(enterEvent, 'stopPropagation')
|
|
261
|
+
const arrowSpy = jest.spyOn(arrowEvent, 'stopPropagation')
|
|
262
|
+
window.dispatchEvent(enterEvent)
|
|
263
|
+
window.dispatchEvent(arrowEvent)
|
|
264
|
+
expect(enterSpy).toHaveBeenCalled()
|
|
265
|
+
expect(arrowSpy).toHaveBeenCalled()
|
|
266
|
+
})
|
|
267
|
+
|
|
252
268
|
test('keyboard: Cmd/Ctrl+Z undoes via the same op-popping logic as mode.undo()', () => {
|
|
253
269
|
const { manager, coord } = setup()
|
|
254
270
|
key('keydown', { key: 'ArrowRight' })
|
|
@@ -36,8 +36,8 @@ const createGeometryInvalidEmitter = (eventBus, hints) => (payload) => {
|
|
|
36
36
|
if (payload.reason && !isIncompleteShape(payload.reason)) { hints.show(payload.reason) }
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// Claims
|
|
40
|
-
// mapProvider.activeMoveTarget contract (
|
|
39
|
+
// Claims MapControls' D-pad for the currently selected vertex — see the generic
|
|
40
|
+
// mapProvider.activeMoveTarget contract (MapControls.jsx). Any plugin could use
|
|
41
41
|
// this slot; the draw plugin is just its first consumer.
|
|
42
42
|
const buildVertexMoveTarget = (draw) => ({
|
|
43
43
|
move: (dx, dy, isLargeStep) => draw.nudgeSelectedVertex(dx, dy, isLargeStep),
|
|
@@ -45,7 +45,7 @@ const buildVertexMoveTarget = (draw) => ({
|
|
|
45
45
|
})
|
|
46
46
|
|
|
47
47
|
// Vertex-selection handlers: sync pluginState.selectedVertexIndex, and claim/release
|
|
48
|
-
//
|
|
48
|
+
// MapControls' D-pad via buildVertexMoveTarget above.
|
|
49
49
|
const createVertexSelectionHandlers = ({ draw, pluginState, mapProvider, eventBus }) => ({
|
|
50
50
|
onVertexSelection: (e) => {
|
|
51
51
|
pluginState.dispatch({ type: 'SET_SELECTED_VERTEX_INDEX', payload: e })
|
|
@@ -233,7 +233,7 @@ export function attachEvents ({ appState, appConfig, mapState, pluginState, mapP
|
|
|
233
233
|
const resetState = () => {
|
|
234
234
|
pluginState.dispatch({ type: 'SET_MODE', payload: null })
|
|
235
235
|
pluginState.dispatch({ type: 'SET_FEATURE', payload: { feature: null, tempFeature: null } })
|
|
236
|
-
// Release
|
|
236
|
+
// Release MapControls' D-pad back to panning the map whenever a draw/edit
|
|
237
237
|
// session ends — a stale claim here would silently hijack it for good.
|
|
238
238
|
mapProvider.activeMoveTarget = null
|
|
239
239
|
}
|
|
@@ -179,7 +179,7 @@ describe('draw event handlers', () => {
|
|
|
179
179
|
expect(() => drawHandler(draw, 'cancel')()).not.toThrow()
|
|
180
180
|
})
|
|
181
181
|
|
|
182
|
-
test('vertexselection dispatches, emits, and claims mapProvider.activeMoveTarget for
|
|
182
|
+
test('vertexselection dispatches, emits, and claims mapProvider.activeMoveTarget for MapControls', () => {
|
|
183
183
|
const { draw, dispatch, eventBus, mapProvider } = setup()
|
|
184
184
|
drawHandler(draw, 'vertexselection')({ index: 2 })
|
|
185
185
|
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED_VERTEX_INDEX', payload: { index: 2 } })
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { EVENTS } from '../../../../src/config/events.js'
|
|
3
|
+
import { scaleFactor } from '../../../../src/config/appConfig.js'
|
|
4
|
+
import { ADAPTER_EVENTS } from '../adapterEvents.js'
|
|
5
|
+
|
|
6
|
+
const EDIT_VERTEX_MODE = 'edit_vertex'
|
|
7
|
+
const DRAW_PROVIDER_ID = 'draw'
|
|
8
|
+
|
|
9
|
+
// Mirrors interact's own projectToScreen (plugins/interact/src/hooks/useSpatialList.js) —
|
|
10
|
+
// mapProvider.mapToScreen is the provider-agnostic projection every mapProvider implements,
|
|
11
|
+
// not any adapter-internal pixel conversion.
|
|
12
|
+
const projectToScreen = (mapProvider, mapSize, coord) => {
|
|
13
|
+
const { x, y } = mapProvider.mapToScreen(coord)
|
|
14
|
+
return { x: x * scaleFactor[mapSize], y: y * scaleFactor[mapSize] }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ids stay flat vertices-then-midpoints indices — matches the mode's own selectedVertexIndex
|
|
18
|
+
// convention on both adapters (see the adapters' getVertexItems() doc comments) and is what
|
|
19
|
+
// selectVertex/insertVertexAtMidpoint expect, so it's unrelated to display order. The array
|
|
20
|
+
// order below is interleaved for display only: Point 1, Insert 1-2, Point 2, ... The "between
|
|
21
|
+
// N and M" label (and this interleave) only reads correctly for a single ring/part (the
|
|
22
|
+
// common case); a shape with holes or multiple parts would need ring-aware numbering to stay
|
|
23
|
+
// accurate, not done here.
|
|
24
|
+
const buildVertexItems = (mapProvider, mapSize) => {
|
|
25
|
+
const { vertices, midpoints } = mapProvider.draw?.getVertexItems?.() ?? { vertices: [], midpoints: [] }
|
|
26
|
+
const toVertexItem = (coord, i) => ({
|
|
27
|
+
id: String(i),
|
|
28
|
+
label: `Point ${i + 1}`,
|
|
29
|
+
isMidpoint: false,
|
|
30
|
+
...projectToScreen(mapProvider, mapSize, coord)
|
|
31
|
+
})
|
|
32
|
+
const toMidpointItem = (coord, i) => ({
|
|
33
|
+
id: String(vertices.length + i),
|
|
34
|
+
// Kept as short as possible — a voice user hears/reads this in full each time, and
|
|
35
|
+
// "Insert point between N and M" was long enough to lose.
|
|
36
|
+
label: `Insert ${i + 1}-${((i + 1) % vertices.length) + 1}`,
|
|
37
|
+
isMidpoint: true,
|
|
38
|
+
...projectToScreen(mapProvider, mapSize, coord)
|
|
39
|
+
})
|
|
40
|
+
const items = []
|
|
41
|
+
vertices.forEach((coord, i) => {
|
|
42
|
+
items.push(toVertexItem(coord, i))
|
|
43
|
+
if (i < midpoints.length) {
|
|
44
|
+
items.push(toMidpointItem(midpoints[i], i))
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
return items
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Rebuilds the vertex/midpoint item list whenever edit_vertex is entered/left, on every
|
|
52
|
+
* geometry change, and on every map move (pan/zoom moves each item's screen position even
|
|
53
|
+
* though the underlying geographic coordinate hasn't changed — same reason interact's own
|
|
54
|
+
* hook rebuilds on MAP_MOVE_END). Keeps the result in itemsRef for useConfirmHandler to
|
|
55
|
+
* resolve against later, and registers as an exclusive spatialListRegistry provider —
|
|
56
|
+
* claiming ownership while edit_vertex is active (interact's own items are excluded
|
|
57
|
+
* meanwhile, restored automatically once the claim is released) rather than contributing
|
|
58
|
+
* additively like interact does.
|
|
59
|
+
*/
|
|
60
|
+
function useVertexItemSync ({ pluginState, mapProvider, mapSize, spatialListRegistry, itemsRef, eventBus }) {
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const rebuild = () => {
|
|
63
|
+
itemsRef.current = pluginState.mode === EDIT_VERTEX_MODE ? buildVertexItems(mapProvider, mapSize) : []
|
|
64
|
+
spatialListRegistry.notifyItemsChanged(DRAW_PROVIDER_ID)
|
|
65
|
+
}
|
|
66
|
+
rebuild()
|
|
67
|
+
eventBus.on(EVENTS.MAP_MOVE_END, rebuild)
|
|
68
|
+
const { draw } = mapProvider
|
|
69
|
+
if (!draw) {
|
|
70
|
+
return () => eventBus.off(EVENTS.MAP_MOVE_END, rebuild)
|
|
71
|
+
}
|
|
72
|
+
draw.on(ADAPTER_EVENTS.UPDATE, rebuild)
|
|
73
|
+
return () => {
|
|
74
|
+
eventBus.off(EVENTS.MAP_MOVE_END, rebuild)
|
|
75
|
+
draw.off(ADAPTER_EVENTS.UPDATE, rebuild)
|
|
76
|
+
}
|
|
77
|
+
}, [pluginState.mode, mapProvider, mapSize, spatialListRegistry, itemsRef, eventBus])
|
|
78
|
+
|
|
79
|
+
// Registers once, for the lifetime of the hook — safe outside edit_vertex too, since an
|
|
80
|
+
// exclusive-capable provider only ever contributes while it holds the claim (see
|
|
81
|
+
// spatialListRegistry.js). Declared after the effect above for the same initial-recompute
|
|
82
|
+
// ordering reason interact's own hook documents.
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
spatialListRegistry.registerItemProvider(DRAW_PROVIDER_ID, {
|
|
85
|
+
// focusable: false — Tab-only keyboard users already have a full path via the map's own
|
|
86
|
+
// arrow-key nudge/Alt+Arrow navigate, so this list exists purely for Voice Control and a
|
|
87
|
+
// screen reader's own virtual-cursor discovery + activation. See SpatialList.jsx's own
|
|
88
|
+
// comment on why forcing every item's tabIndex to -1 is deliberate here.
|
|
89
|
+
getItems: () => ({ items: itemsRef.current, multiselectable: false, label: 'Shape points', focusable: false }),
|
|
90
|
+
exclusive: true
|
|
91
|
+
})
|
|
92
|
+
return () => { spatialListRegistry.unregisterItemProvider(DRAW_PROVIDER_ID) }
|
|
93
|
+
}, [spatialListRegistry, itemsRef])
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (pluginState.mode !== EDIT_VERTEX_MODE) {
|
|
97
|
+
return undefined
|
|
98
|
+
}
|
|
99
|
+
spatialListRegistry.claimExclusive(DRAW_PROVIDER_ID)
|
|
100
|
+
return () => spatialListRegistry.releaseExclusive(DRAW_PROVIDER_ID)
|
|
101
|
+
}, [pluginState.mode, spatialListRegistry])
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Listens for MAP_SET_ACTIVE_ITEM and previews (non-destructively) whichever vertex or
|
|
106
|
+
* midpoint the listbox cursor is now on — every arrow-key move through the list, mirroring
|
|
107
|
+
* how mouse/touch/keyboard selection already behaves live elsewhere in edit_vertex. Unlike
|
|
108
|
+
* interact's real map features (which need an explicit Enter/Space confirm before anything
|
|
109
|
+
* happens), there's no separate "preview" step for a real vertex anywhere else in this mode —
|
|
110
|
+
* arrowing onto one already is the live selection. A midpoint is also just previewed here;
|
|
111
|
+
* confirming the actual insert is useConfirmHandler's job below.
|
|
112
|
+
*/
|
|
113
|
+
function useActiveVertexHandler ({ eventBus, mapProvider, pluginState, activeIdRef }) {
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const handle = ({ id }) => {
|
|
116
|
+
activeIdRef.current = id
|
|
117
|
+
if (id === null || pluginState.mode !== EDIT_VERTEX_MODE) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
mapProvider.draw?.selectVertex?.(Number(id))
|
|
121
|
+
}
|
|
122
|
+
eventBus.on(EVENTS.MAP_SET_ACTIVE_ITEM, handle)
|
|
123
|
+
return () => { eventBus.off(EVENTS.MAP_SET_ACTIVE_ITEM, handle) }
|
|
124
|
+
}, [eventBus, mapProvider, pluginState.mode, activeIdRef])
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Handles MAP_SELECT_ITEM (Enter/Space, or a click/Voice-Control click — and, since none of
|
|
129
|
+
* these items are ever a real Tab stop, a screen reader's own virtual-cursor activation gesture
|
|
130
|
+
* too, which likewise fires as a click — all three arrive here identically). For a midpoint,
|
|
131
|
+
* this is the destructive step: commits a new vertex there (a real vertex is already the live
|
|
132
|
+
* selection the moment it's previewed, so confirming one doesn't change anything on the map).
|
|
133
|
+
* Either way, confirming announces what happened and hands keyboard focus back to the viewport
|
|
134
|
+
* — landing the user exactly where the pre-existing, untouched arrow-key nudge/Alt+Arrow flow
|
|
135
|
+
* already works, same as if they'd selected that point with a mouse click. This is deliberately
|
|
136
|
+
* unconditional (not just for midpoints) so every input path ends up in the same place.
|
|
137
|
+
*/
|
|
138
|
+
function useConfirmHandler ({ eventBus, mapProvider, pluginState, activeIdRef, itemsRef, viewportRef, announce }) {
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
const handleConfirm = () => {
|
|
141
|
+
if (pluginState.mode !== EDIT_VERTEX_MODE || activeIdRef.current === null) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
const item = itemsRef.current.find(i => i.id === activeIdRef.current)
|
|
145
|
+
if (!item) {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
if (item.isMidpoint) {
|
|
149
|
+
mapProvider.draw?.insertVertexAtMidpoint?.(Number(activeIdRef.current))
|
|
150
|
+
announce('New point inserted', 'action')
|
|
151
|
+
} else {
|
|
152
|
+
announce(`${item.label} selected`, 'action')
|
|
153
|
+
}
|
|
154
|
+
viewportRef.current?.focus()
|
|
155
|
+
}
|
|
156
|
+
eventBus.on(EVENTS.MAP_SELECT_ITEM, handleConfirm)
|
|
157
|
+
return () => { eventBus.off(EVENTS.MAP_SELECT_ITEM, handleConfirm) }
|
|
158
|
+
}, [eventBus, mapProvider, pluginState.mode, activeIdRef, itemsRef, viewportRef, announce])
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Orchestrates the accessible (Voice Control / screen-reader-discoverable) vertex/midpoint
|
|
163
|
+
* targets for draw's edit_vertex mode. Mirrors plugins/interact/src/hooks/useSpatialList.js's
|
|
164
|
+
* structure (item-sync + active-item concerns), but claims the registry's exclusive slot
|
|
165
|
+
* instead of contributing additively, declares focusable: false (no real Tab stop — see
|
|
166
|
+
* useVertexItemSync's own comment on why), and splits "preview" (any listbox move, non-
|
|
167
|
+
* destructive) from "confirm" (inserts at a midpoint, then always hands focus back to the
|
|
168
|
+
* viewport) rather than interact's own "preview, then explicit confirm for everything" split.
|
|
169
|
+
*
|
|
170
|
+
* @param {{ mapState: object, pluginState: object, services: object, mapProvider: object, spatialListRegistry: object, viewportRef: React.RefObject }} params
|
|
171
|
+
*/
|
|
172
|
+
export function useSpatialList ({ mapState, pluginState, services, mapProvider, spatialListRegistry, viewportRef }) {
|
|
173
|
+
const { mapSize } = mapState
|
|
174
|
+
const { eventBus, announce } = services
|
|
175
|
+
const itemsRef = useRef([])
|
|
176
|
+
const activeIdRef = useRef(null)
|
|
177
|
+
useVertexItemSync({ pluginState, mapProvider, mapSize, spatialListRegistry, itemsRef, eventBus })
|
|
178
|
+
useActiveVertexHandler({ eventBus, mapProvider, pluginState, activeIdRef })
|
|
179
|
+
useConfirmHandler({ eventBus, mapProvider, pluginState, activeIdRef, itemsRef, viewportRef, announce })
|
|
180
|
+
}
|