@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getSnapInstance, clearSnapIndicator } from '../../utils/snapHelpers.js'
|
|
2
2
|
import { ARROW_KEYS, ARROW_OFFSETS, isInteractiveElementFocused, isUndoShortcut, sharedKeyboardHandlers } from '../../utils/keyboardShortcuts.js'
|
|
3
3
|
import { getCoords } from './geometryHelpers.js'
|
|
4
|
+
import { stopIfGlobalAltKey } from '../../../../../../../src/utils/globalAltShortcuts.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Keyboard interaction for the vertex-edit mode: arrow-key vertex movement/insertion,
|
|
@@ -9,7 +10,7 @@ import { getCoords } from './geometryHelpers.js'
|
|
|
9
10
|
export const keyboardHandlers = {
|
|
10
11
|
...sharedKeyboardHandlers,
|
|
11
12
|
|
|
12
|
-
onKeydown (state,
|
|
13
|
+
onKeydown (state, event) {
|
|
13
14
|
if (isInteractiveElementFocused(state)) {
|
|
14
15
|
return
|
|
15
16
|
}
|
|
@@ -17,40 +18,40 @@ export const keyboardHandlers = {
|
|
|
17
18
|
state.interfaceType = 'keyboard'
|
|
18
19
|
this.hideTouchVertexIndicator(state)
|
|
19
20
|
|
|
20
|
-
if (
|
|
21
|
-
this.handleSpace(state,
|
|
21
|
+
if (event.key === ' ') {
|
|
22
|
+
this.handleSpace(state, event)
|
|
22
23
|
return
|
|
23
24
|
}
|
|
24
|
-
if (ARROW_KEYS.has(
|
|
25
|
-
this.handleArrowKey(state,
|
|
25
|
+
if (ARROW_KEYS.has(event.key) && state.selectedVertexIndex >= 0) {
|
|
26
|
+
this.handleArrowKey(state, event)
|
|
26
27
|
return
|
|
27
28
|
}
|
|
28
|
-
if (
|
|
29
|
+
if (event.key === 'Escape') {
|
|
29
30
|
this.changeMode(state, { isPanEnabled: true, selectedVertexIndex: -1, selectedVertexType: null })
|
|
30
31
|
return
|
|
31
32
|
}
|
|
32
|
-
if (isUndoShortcut(
|
|
33
|
-
this.handleUndoShortcut(state,
|
|
33
|
+
if (isUndoShortcut(event)) {
|
|
34
|
+
this.handleUndoShortcut(state, event)
|
|
34
35
|
}
|
|
35
36
|
},
|
|
36
37
|
|
|
37
38
|
// Space always cancels the default; with no active selection it starts keyboard editing.
|
|
38
|
-
handleSpace (state,
|
|
39
|
-
|
|
39
|
+
handleSpace (state, event) {
|
|
40
|
+
event.preventDefault()
|
|
40
41
|
if (state.selectedVertexIndex < 0) {
|
|
41
42
|
this.startKeyboardSelection(state)
|
|
42
43
|
}
|
|
43
44
|
},
|
|
44
45
|
|
|
45
46
|
// Alt+arrow steps to the next vertex/midpoint; a plain arrow nudges the selected vertex.
|
|
46
|
-
handleArrowKey (state,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
this.updateVertex(state,
|
|
47
|
+
handleArrowKey (state, event) {
|
|
48
|
+
event.preventDefault()
|
|
49
|
+
event.stopPropagation()
|
|
50
|
+
if (event.altKey) {
|
|
51
|
+
this.updateVertex(state, event.key)
|
|
51
52
|
return
|
|
52
53
|
}
|
|
53
|
-
this.moveVertexByKey(state,
|
|
54
|
+
this.moveVertexByKey(state, event)
|
|
54
55
|
},
|
|
55
56
|
|
|
56
57
|
// Space with no active selection: select the first vertex for keyboard editing.
|
|
@@ -59,11 +60,11 @@ export const keyboardHandlers = {
|
|
|
59
60
|
if (snap) {
|
|
60
61
|
clearSnapIndicator(snap, this.map)
|
|
61
62
|
}
|
|
62
|
-
if (!state.
|
|
63
|
-
state.
|
|
63
|
+
if (!state.vertices?.length) {
|
|
64
|
+
state.vertices = this.getVertices(state.featureId)
|
|
64
65
|
state.midpoints = this.getMidpoints(state.featureId)
|
|
65
66
|
}
|
|
66
|
-
if (!state.
|
|
67
|
+
if (!state.vertices?.length) {
|
|
67
68
|
return
|
|
68
69
|
}
|
|
69
70
|
state.isPanEnabled = false
|
|
@@ -71,9 +72,9 @@ export const keyboardHandlers = {
|
|
|
71
72
|
},
|
|
72
73
|
|
|
73
74
|
// Arrow key with a selected vertex: insert (midpoint) or nudge the vertex, honouring snap.
|
|
74
|
-
moveVertexByKey (state,
|
|
75
|
+
moveVertexByKey (state, event) {
|
|
75
76
|
if (state.selectedVertexType === 'midpoint') {
|
|
76
|
-
this.insertVertex(state,
|
|
77
|
+
this.insertVertex(state, event)
|
|
77
78
|
return
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -89,25 +90,32 @@ export const keyboardHandlers = {
|
|
|
89
90
|
state._keyboardMoveStartIndex = state.selectedVertexIndex
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
this.moveVertex(state, this._keyboardMoveTarget(state,
|
|
93
|
+
this.moveVertex(state, this._keyboardMoveTarget(state, event, currentCoord))
|
|
93
94
|
},
|
|
94
95
|
|
|
95
96
|
// Resolve the destination coordinate for a keyboard nudge, applying or breaking
|
|
96
97
|
// snap — delegates to the shared resolver (utils/snapMovement.js) also used by
|
|
97
|
-
//
|
|
98
|
-
_keyboardMoveTarget (state,
|
|
99
|
-
const [dx, dy] = ARROW_OFFSETS[
|
|
100
|
-
return this.resolveSnapTarget(state, dx, dy, currentCoord, () => this.getNewCoord(state,
|
|
98
|
+
// MapControls' nudgeVertexByDelta, so both snap identically.
|
|
99
|
+
_keyboardMoveTarget (state, event, currentCoord) {
|
|
100
|
+
const [dx, dy] = ARROW_OFFSETS[event.key]
|
|
101
|
+
return this.resolveSnapTarget(state, dx, dy, currentCoord, () => this.getNewCoord(state, event))
|
|
101
102
|
},
|
|
102
103
|
|
|
103
|
-
onKeyup (state,
|
|
104
|
+
onKeyup (state, event) {
|
|
104
105
|
if (isInteractiveElementFocused(state)) {
|
|
105
106
|
return
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
state.interfaceType = 'keyboard'
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
// Only shadow while a vertex/midpoint is actually selected — that's the same condition
|
|
111
|
+
// gating this mode's own local Alt+Arrow handling below (and there's no local Enter
|
|
112
|
+
// meaning here at all, selected or not), so with nothing selected there's no local
|
|
113
|
+
// conflict to protect and the global map-label shortcuts should keep working.
|
|
114
|
+
if (state.selectedVertexIndex >= 0) {
|
|
115
|
+
stopIfGlobalAltKey(event)
|
|
116
|
+
}
|
|
117
|
+
if (ARROW_KEYS.has(event.key) && state.selectedVertexIndex >= 0) {
|
|
118
|
+
event.stopPropagation()
|
|
111
119
|
|
|
112
120
|
// Push undo for keyboard move sequence
|
|
113
121
|
if (state._keyboardMoveStartPosition && state._keyboardMoveStartIndex != null) {
|
|
@@ -121,7 +129,7 @@ export const keyboardHandlers = {
|
|
|
121
129
|
state._keyboardMoveStartIndex = null
|
|
122
130
|
}
|
|
123
131
|
}
|
|
124
|
-
if (
|
|
132
|
+
if (event.key === 'Delete') {
|
|
125
133
|
this.deleteVertex(state)
|
|
126
134
|
}
|
|
127
135
|
}
|
|
@@ -4,6 +4,23 @@ describe('keyboardHandlers', () => {
|
|
|
4
4
|
const keydown = (ctx, state, key, extra = {}) => ctx.onKeydown(state, { key, preventDefault: jest.fn(), stopPropagation: jest.fn(), ...extra })
|
|
5
5
|
const keyup = (ctx, state, key) => ctx.onKeyup(state, { key, stopPropagation: jest.fn() })
|
|
6
6
|
|
|
7
|
+
// stopIfGlobalAltKey's own branches are covered by src/utils/globalAltShortcuts.test.js —
|
|
8
|
+
// this just checks onKeyup only calls it once a vertex/midpoint is actually selected, same
|
|
9
|
+
// condition its own local Alt+Arrow handling already requires; with nothing selected there's
|
|
10
|
+
// no local meaning to protect, so the app-wide map-label shortcuts should keep working.
|
|
11
|
+
test('shadows Alt+Enter/Alt+Arrow from the app-wide label shortcuts only while something is selected', () => {
|
|
12
|
+
const { ctx, state } = createHarness()
|
|
13
|
+
const altEnter = (extra) => ({ key: 'Enter', altKey: true, stopPropagation: jest.fn(), ...extra })
|
|
14
|
+
|
|
15
|
+
const whileSelected = altEnter()
|
|
16
|
+
ctx.onKeyup({ ...state, selectedVertexIndex: 0 }, whileSelected)
|
|
17
|
+
expect(whileSelected.stopPropagation).toHaveBeenCalled()
|
|
18
|
+
|
|
19
|
+
const whileUnselected = altEnter()
|
|
20
|
+
ctx.onKeyup({ ...state, selectedVertexIndex: -1 }, whileUnselected)
|
|
21
|
+
expect(whileUnselected.stopPropagation).not.toHaveBeenCalled()
|
|
22
|
+
})
|
|
23
|
+
|
|
7
24
|
// isInteractiveElementFocused's own branches are covered by utils/keyboardShortcuts.test.js
|
|
8
25
|
// — this just checks onKeydown/onKeyup actually consult it.
|
|
9
26
|
test('shortcuts are ignored while a form control outside the viewport has focus', () => {
|
|
@@ -28,7 +45,7 @@ describe('keyboardHandlers', () => {
|
|
|
28
45
|
expect(updateSpy).toHaveBeenCalledTimes(1)
|
|
29
46
|
|
|
30
47
|
map._snapInstance = null // no snap indicator to clear
|
|
31
|
-
const s = { ...state, featureId: 'missing',
|
|
48
|
+
const s = { ...state, featureId: 'missing', vertices: [], selectedVertexIndex: -1, isPanEnabled: true }
|
|
32
49
|
keydown(ctx, s, ' ')
|
|
33
50
|
expect(s.isPanEnabled).toBe(true)
|
|
34
51
|
})
|
|
@@ -43,7 +60,7 @@ describe('keyboardHandlers', () => {
|
|
|
43
60
|
keydown(ctx, state, 'ArrowRight')
|
|
44
61
|
expect(state._keyboardMoveStartIndex).toBe(1)
|
|
45
62
|
expect(state._keyboardMoveStartPosition).toEqual([10, 0])
|
|
46
|
-
expect(state.
|
|
63
|
+
expect(state.vertices[1]).not.toEqual([10, 0])
|
|
47
64
|
|
|
48
65
|
// Still held → start position isn't reset to the intermediate (already-moved) position
|
|
49
66
|
keydown(ctx, state, 'ArrowRight')
|
|
@@ -53,7 +70,7 @@ describe('keyboardHandlers', () => {
|
|
|
53
70
|
test('an arrow key on a midpoint inserts a vertex; guards a missing feature or out-of-range vertex', () => {
|
|
54
71
|
const { ctx, state } = createHarness()
|
|
55
72
|
const insertSpy = jest.spyOn(ctx, 'insertVertex').mockImplementation(() => {})
|
|
56
|
-
keydown(ctx, { ...state, selectedVertexIndex: state.
|
|
73
|
+
keydown(ctx, { ...state, selectedVertexIndex: state.vertices.length, selectedVertexType: 'midpoint' }, 'ArrowRight')
|
|
57
74
|
expect(insertSpy).toHaveBeenCalled()
|
|
58
75
|
|
|
59
76
|
keydown(ctx, { ...state, featureId: 'missing', selectedVertexIndex: 0 }, 'ArrowRight')
|
|
@@ -28,7 +28,7 @@ export const pointerHandlers = {
|
|
|
28
28
|
state.selectedVertexIndex = vertexIndex
|
|
29
29
|
state.selectedVertexType = 'vertex'
|
|
30
30
|
state.coordPath = coordPath
|
|
31
|
-
const vertex = state.
|
|
31
|
+
const vertex = state.vertices?.[vertexIndex]
|
|
32
32
|
if (vertex) {
|
|
33
33
|
state._moveStartPosition = [...vertex]
|
|
34
34
|
state._moveStartIndex = vertexIndex
|
|
@@ -47,7 +47,7 @@ export const pointerHandlers = {
|
|
|
47
47
|
state.selectedVertexIndex = this.getVertexIndexFromMidpoint(state, coordPath)
|
|
48
48
|
state.selectedVertexType = 'vertex'
|
|
49
49
|
state.coordPath = null // Clear coordPath for midpoints
|
|
50
|
-
this.map.fire(CUSTOM_DRAW_EVENTS.VERTEX_SELECTION, { index: state.selectedVertexIndex,
|
|
50
|
+
this.map.fire(CUSTOM_DRAW_EVENTS.VERTEX_SELECTION, { index: state.selectedVertexIndex, numVertices: state.vertices.length })
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
|
|
@@ -60,7 +60,7 @@ export const pointerHandlers = {
|
|
|
60
60
|
state.selectedVertexType = 'vertex'
|
|
61
61
|
state._isInsertingVertex = false
|
|
62
62
|
state._insertedVertexIndex = null
|
|
63
|
-
this.map.fire(CUSTOM_DRAW_EVENTS.VERTEX_SELECTION, { index: insertedIndex,
|
|
63
|
+
this.map.fire(CUSTOM_DRAW_EVENTS.VERTEX_SELECTION, { index: insertedIndex, numVertices: state.vertices.length })
|
|
64
64
|
return
|
|
65
65
|
}
|
|
66
66
|
DirectSelect.onClick.call(this, state, e)
|
|
@@ -91,7 +91,7 @@ export const pointerHandlers = {
|
|
|
91
91
|
},
|
|
92
92
|
|
|
93
93
|
// Did the selected vertex actually change position during this interaction?
|
|
94
|
-
// Reads the live feature (not the cached state.
|
|
94
|
+
// Reads the live feature (not the cached state.vertices) for reliability.
|
|
95
95
|
_didVertexMove (state) {
|
|
96
96
|
if (!state._moveStartPosition || state._moveStartIndex == null) {
|
|
97
97
|
return false
|
|
@@ -115,7 +115,7 @@ export const pointerHandlers = {
|
|
|
115
115
|
state._isInsertingVertex = false
|
|
116
116
|
state._insertedVertexIndex = null
|
|
117
117
|
// DirectSelect.onMouseUp fires draw.update but not draw.selectionchange, so broadcast the count here
|
|
118
|
-
this.map.fire(CUSTOM_DRAW_EVENTS.VERTEX_SELECTION, { index: insertedIndex,
|
|
118
|
+
this.map.fire(CUSTOM_DRAW_EVENTS.VERTEX_SELECTION, { index: insertedIndex, numVertices: state.vertices.length })
|
|
119
119
|
},
|
|
120
120
|
|
|
121
121
|
_recordMoveUndo (state) {
|
|
@@ -66,7 +66,7 @@ export const touchHandlers = {
|
|
|
66
66
|
// alignment. Left uncorrected, the next onTouchstart's delta math re-anchors off that
|
|
67
67
|
// drifted position, compounding a little further with every snapped drag (same fix as
|
|
68
68
|
// editPointMode's own onTouchend, for the same reason).
|
|
69
|
-
const vertex = state.
|
|
69
|
+
const vertex = state.vertices[state.selectedVertexIndex]
|
|
70
70
|
if (vertex) {
|
|
71
71
|
this.updateTouchVertexTarget(state, scalePoint(this.map.project(vertex), state.scale))
|
|
72
72
|
}
|
|
@@ -101,7 +101,7 @@ export const touchHandlers = {
|
|
|
101
101
|
onTouchstart (state, e) {
|
|
102
102
|
clearSnapState(getSnapInstance(this.map))
|
|
103
103
|
// Always get fresh vertex data in case coordinates changed during previous edits
|
|
104
|
-
const freshVertices = this.
|
|
104
|
+
const freshVertices = this.getVertices(state.featureId)
|
|
105
105
|
const vertex = freshVertices?.[state.selectedVertexIndex]
|
|
106
106
|
if (!vertex || !isOnSVG(e.target.parentNode)) {
|
|
107
107
|
return
|
|
@@ -96,14 +96,14 @@ describe('touchHandlers', () => {
|
|
|
96
96
|
ctx.onTouchstart(state, { target: svgTarget(state), touches: [{ clientX: 20, clientY: 20 }] })
|
|
97
97
|
|
|
98
98
|
ctx.onTouchmove(state, { target: { parentNode: document.createElement('div') }, touches: [{ clientX: 5, clientY: 5 }] })
|
|
99
|
-
expect(state.
|
|
99
|
+
expect(state.vertices[1]).toEqual([10, 0]) // off-target move ignored
|
|
100
100
|
ctx.onTouchmove(state, { target: svgTarget(state), touches: [{ clientX: 30, clientY: 40 }] })
|
|
101
101
|
expect(state._touchMoved).toBe(true)
|
|
102
102
|
|
|
103
103
|
state.getSnapEnabled = () => true
|
|
104
104
|
map._snapInstance = { status: true, snapStatus: true, snapCoords: [7, 8], snapToClosestPoint: jest.fn() }
|
|
105
105
|
ctx.onTouchmove(state, { target: svgTarget(state), touches: [{ clientX: 50, clientY: 60 }] })
|
|
106
|
-
expect(state.
|
|
106
|
+
expect(state.vertices[1]).toEqual([7, 8])
|
|
107
107
|
})
|
|
108
108
|
|
|
109
109
|
test('onTap clears the snap indicator, then selects a vertex, inserts on a midpoint, or clears with no target', () => {
|
|
@@ -64,7 +64,7 @@ export const undoHandlers = {
|
|
|
64
64
|
this._applyUndoAndSync(state, geojson, featureId)
|
|
65
65
|
|
|
66
66
|
// Update touch vertex target position
|
|
67
|
-
const vertex = state.
|
|
67
|
+
const vertex = state.vertices[state.selectedVertexIndex]
|
|
68
68
|
if (vertex) {
|
|
69
69
|
this.updateTouchVertexTarget(state, scalePoint(this.map.project(vertex), state.scale))
|
|
70
70
|
}
|
|
@@ -122,13 +122,13 @@ export const undoHandlers = {
|
|
|
122
122
|
this._applyUndoAndSync(state, geojson, featureId)
|
|
123
123
|
|
|
124
124
|
// Re-insertion always lands the vertex back at vertexIndex, so it is guaranteed present
|
|
125
|
-
this.updateTouchVertexTarget(state, scalePoint(this.map.project(state.
|
|
125
|
+
this.updateTouchVertexTarget(state, scalePoint(this.map.project(state.vertices[vertexIndex]), state.scale))
|
|
126
126
|
this.changeMode(state, { selectedVertexIndex: vertexIndex, selectedVertexType: 'vertex', coordPath: this.getCoordPath(state, vertexIndex) })
|
|
127
127
|
},
|
|
128
128
|
|
|
129
129
|
_applyUndoAndSync (state, geojson, featureId) {
|
|
130
130
|
this._ctx.api.add(geojson)
|
|
131
|
-
state.
|
|
131
|
+
state.vertices = this.getVertices(featureId)
|
|
132
132
|
state.midpoints = this.getMidpoints(featureId)
|
|
133
133
|
this._ctx.store.render()
|
|
134
134
|
this.fireGeometryChange(state)
|
|
@@ -55,23 +55,23 @@ describe('undoHandlers', () => {
|
|
|
55
55
|
ctx.undoMoveVertex(state, { vertexIndex: 99, previousPosition: [0, 0], featureId: 'feat-1' })
|
|
56
56
|
state.selectedVertexIndex = 0
|
|
57
57
|
ctx.undoMoveVertex(state, { vertexIndex: 0, previousPosition: [3, 4], featureId: 'feat-1' })
|
|
58
|
-
expect(ctx.
|
|
58
|
+
expect(ctx.getVertices('feat-1')[0]).toEqual([3, 4])
|
|
59
59
|
|
|
60
60
|
// Skips the touch target update when the selected vertex is gone
|
|
61
61
|
ctx.undoMoveVertex({ ...state, selectedVertexIndex: 99 }, { vertexIndex: 0, previousPosition: [1, 1], featureId: 'feat-1' })
|
|
62
|
-
expect(ctx.
|
|
62
|
+
expect(ctx.getVertices('feat-1')[0]).toEqual([1, 1])
|
|
63
63
|
})
|
|
64
64
|
|
|
65
65
|
test('undoInsertVertex removes the inserted vertex and clears the selection', () => {
|
|
66
66
|
const { ctx, state, api } = createHarness()
|
|
67
67
|
ctx.undoInsertVertex(state, { vertexIndex: 1, featureId: 'feat-1' })
|
|
68
|
-
expect(ctx.
|
|
68
|
+
expect(ctx.getVertices('feat-1')).toHaveLength(3)
|
|
69
69
|
expect(api.changeMode).toHaveBeenCalledWith('edit_vertex', expect.objectContaining({ selectedVertexIndex: -1 }))
|
|
70
70
|
|
|
71
71
|
// Guards a missing feature and an out-of-range index
|
|
72
72
|
ctx.undoInsertVertex({ ...state, featureId: 'missing' }, { vertexIndex: 0, featureId: 'missing' })
|
|
73
73
|
ctx.undoInsertVertex(state, { vertexIndex: 999, featureId: 'feat-1' })
|
|
74
|
-
expect(ctx.
|
|
74
|
+
expect(ctx.getVertices('feat-1')).toHaveLength(3)
|
|
75
75
|
})
|
|
76
76
|
|
|
77
77
|
test('undoDeleteVertex re-inserts, handling boundary, fallback and missing feature', () => {
|
|
@@ -79,13 +79,13 @@ describe('undoHandlers', () => {
|
|
|
79
79
|
ctx.undoDeleteVertex({ ...state, featureId: 'missing' }, { vertexIndex: 0, position: [1, 1], featureId: 'missing' })
|
|
80
80
|
// Fallback: index far beyond any segment boundary → no result → no-op
|
|
81
81
|
ctx.undoDeleteVertex(state, { vertexIndex: 999, position: [1, 1], featureId: 'feat-1' })
|
|
82
|
-
expect(ctx.
|
|
82
|
+
expect(ctx.getVertices('feat-1')).toHaveLength(4)
|
|
83
83
|
// Success: re-insert at index 1
|
|
84
84
|
ctx.undoDeleteVertex(state, { vertexIndex: 1, position: [1, 1], featureId: 'feat-1' })
|
|
85
|
-
expect(ctx.
|
|
85
|
+
expect(ctx.getVertices('feat-1')).toHaveLength(5)
|
|
86
86
|
// Boundary: index === segment.start + segment.length
|
|
87
87
|
const seg = getRingSegments(ctx.getFeature('feat-1'))[0]
|
|
88
88
|
ctx.undoDeleteVertex(state, { vertexIndex: seg.start + seg.length, position: [2, 2], featureId: 'feat-1' })
|
|
89
|
-
expect(ctx.
|
|
89
|
+
expect(ctx.getVertices('feat-1')).toHaveLength(6)
|
|
90
90
|
})
|
|
91
91
|
})
|
|
@@ -33,7 +33,7 @@ export const vertexOperations = {
|
|
|
33
33
|
},
|
|
34
34
|
|
|
35
35
|
// Moves the selected vertex by an explicit (dx, dy) unit direction — the entry
|
|
36
|
-
// point for
|
|
36
|
+
// point for MapControls' D-pad (see mapProvider.activeMoveTarget in events.js),
|
|
37
37
|
// as opposed to moveVertexByKey's KeyboardEvent-driven path. Each call is treated
|
|
38
38
|
// as one complete, undoable action (no held-key sequencing, since a button click
|
|
39
39
|
// has no "held" state to batch the way arrow keys do) — but still honours snap
|
|
@@ -55,7 +55,7 @@ export const vertexOperations = {
|
|
|
55
55
|
},
|
|
56
56
|
|
|
57
57
|
insertVertex (state, e) {
|
|
58
|
-
const midIdx = state.selectedVertexIndex - state.
|
|
58
|
+
const midIdx = state.selectedVertexIndex - state.vertices.length
|
|
59
59
|
const newCoord = this.getOffset(state.midpoints[midIdx], e)
|
|
60
60
|
const feature = this.getFeature(state.featureId)
|
|
61
61
|
const geojson = feature.toGeoJSON()
|
|
@@ -107,7 +107,7 @@ export const vertexOperations = {
|
|
|
107
107
|
const coords = getModifiableCoords(geojson, result.segment.path)
|
|
108
108
|
coords[result.localIdx] = [coord.lng, coord.lat]
|
|
109
109
|
this._ctx.api.add(geojson)
|
|
110
|
-
state.
|
|
110
|
+
state.vertices = this.getVertices(state.featureId)
|
|
111
111
|
|
|
112
112
|
this.map.fire('draw.geometrychange', state.feature)
|
|
113
113
|
},
|
|
@@ -134,7 +134,7 @@ export const vertexOperations = {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
// Save position for undo before deletion
|
|
137
|
-
const deletedPosition = [...state.
|
|
137
|
+
const deletedPosition = [...state.vertices[state.selectedVertexIndex]]
|
|
138
138
|
const deletedIndex = state.selectedVertexIndex
|
|
139
139
|
|
|
140
140
|
// Remove the coordinate directly rather than via this._ctx.api.trash(), which routes through
|
|
@@ -21,7 +21,7 @@ describe('vertexOperations', () => {
|
|
|
21
21
|
}))
|
|
22
22
|
|
|
23
23
|
api.changeMode.mockClear()
|
|
24
|
-
ctx.updateVertex({ featureId: 'missing',
|
|
24
|
+
ctx.updateVertex({ featureId: 'missing', vertices: [], selectedVertexIndex: -1 }, 'ArrowRight')
|
|
25
25
|
expect(api.changeMode).not.toHaveBeenCalled()
|
|
26
26
|
|
|
27
27
|
// A vertex target carries a coordPath; a midpoint target does not
|
|
@@ -43,9 +43,9 @@ describe('vertexOperations', () => {
|
|
|
43
43
|
const { ctx, state, map } = createHarness()
|
|
44
44
|
state.selectedVertexIndex = 0
|
|
45
45
|
state.selectedVertexType = 'vertex'
|
|
46
|
-
const before = [...state.
|
|
46
|
+
const before = [...state.vertices[0]]
|
|
47
47
|
ctx.nudgeVertexByDelta(state, 1, 0, true)
|
|
48
|
-
expect(state.
|
|
48
|
+
expect(state.vertices[0]).not.toEqual(before)
|
|
49
49
|
expect(map._undoStack.pop()).toMatchObject({ type: 'move_vertex', vertexIndex: 0, previousPosition: before })
|
|
50
50
|
|
|
51
51
|
map._undoStack.clear()
|
|
@@ -60,7 +60,7 @@ describe('vertexOperations', () => {
|
|
|
60
60
|
|
|
61
61
|
// resolveSnapTarget's own snap/break-out-of-snap branches are covered by
|
|
62
62
|
// utils/snapMovement.test.js — this just checks nudgeVertexByDelta wires a snap hit
|
|
63
|
-
// through to the moved vertex, same as keyboard nudging (regression:
|
|
63
|
+
// through to the moved vertex, same as keyboard nudging (regression: MapControls
|
|
64
64
|
// bypassed snap entirely before resolveSnapTarget was shared).
|
|
65
65
|
test('nudgeVertexByDelta honours an active snap', () => {
|
|
66
66
|
const { ctx, state, map } = createHarness()
|
|
@@ -70,12 +70,12 @@ describe('vertexOperations', () => {
|
|
|
70
70
|
map._snapInstance = { status: true, snapStatus: true, snapCoords: [7, 8], snapToClosestPoint: jest.fn() }
|
|
71
71
|
ctx.nudgeVertexByDelta(state, 0, -1, true)
|
|
72
72
|
expect(state._isSnapped).toBe(true)
|
|
73
|
-
expect(state.
|
|
73
|
+
expect(state.vertices[1]).toEqual([7, 8])
|
|
74
74
|
})
|
|
75
75
|
|
|
76
76
|
test('insertVertex splits a midpoint into a new vertex, records undo and selects it', () => {
|
|
77
77
|
const { ctx, state, api } = createHarness()
|
|
78
|
-
ctx.insertVertex({ ...state, selectedVertexIndex: state.
|
|
78
|
+
ctx.insertVertex({ ...state, selectedVertexIndex: state.vertices.length, selectedVertexType: 'midpoint' }, { key: 'ArrowRight', shiftKey: false })
|
|
79
79
|
expect(api.add).toHaveBeenCalled()
|
|
80
80
|
expect(state.map ?? ctx.map._undoStack.length).toBeGreaterThan(0)
|
|
81
81
|
expect(api.changeMode).toHaveBeenCalledWith('edit_vertex', expect.objectContaining({
|
|
@@ -84,14 +84,14 @@ describe('vertexOperations', () => {
|
|
|
84
84
|
|
|
85
85
|
// Also works for an open line segment
|
|
86
86
|
const line = createHarness(LINE())
|
|
87
|
-
line.ctx.insertVertex({ ...line.state, selectedVertexIndex: line.state.
|
|
87
|
+
line.ctx.insertVertex({ ...line.state, selectedVertexIndex: line.state.vertices.length, selectedVertexType: 'midpoint' }, { key: 'ArrowRight', shiftKey: false })
|
|
88
88
|
expect(line.api.add).toHaveBeenCalled()
|
|
89
89
|
})
|
|
90
90
|
|
|
91
91
|
test('insertVertex bails out when the midpoint maps to no segment', () => {
|
|
92
92
|
const { ctx, state, api } = createHarness()
|
|
93
93
|
// midIdx beyond the segment's midpoint count, but with a defined midpoint coord
|
|
94
|
-
const badState = { ...state, selectedVertexIndex: state.
|
|
94
|
+
const badState = { ...state, selectedVertexIndex: state.vertices.length + 4, midpoints: [...state.midpoints, [5, 5]] }
|
|
95
95
|
api.add.mockClear()
|
|
96
96
|
ctx.insertVertex(badState, { key: 'ArrowRight', shiftKey: false })
|
|
97
97
|
expect(api.add).not.toHaveBeenCalled()
|
|
@@ -101,20 +101,20 @@ describe('vertexOperations', () => {
|
|
|
101
101
|
const { ctx, state, map } = createHarness()
|
|
102
102
|
state.selectedVertexIndex = 0
|
|
103
103
|
ctx.moveVertex(state, { lng: 1, lat: 2 })
|
|
104
|
-
expect(state.
|
|
104
|
+
expect(state.vertices[0]).toEqual([1, 2])
|
|
105
105
|
|
|
106
106
|
map._snapInstance = { snapStatus: true, snapCoords: [7, 8] }
|
|
107
107
|
ctx.moveVertex(state, { lng: 1, lat: 2 }, { checkSnap: true })
|
|
108
|
-
expect(state.
|
|
108
|
+
expect(state.vertices[0]).toEqual([7, 8])
|
|
109
109
|
|
|
110
|
-
const before = [...state.
|
|
110
|
+
const before = [...state.vertices]
|
|
111
111
|
ctx.moveVertex({ ...state, selectedVertexIndex: 99 }, { lng: 3, lat: 3 })
|
|
112
|
-
expect(ctx.
|
|
112
|
+
expect(ctx.getVertices('feat-1')).toEqual(before)
|
|
113
113
|
|
|
114
114
|
// checkSnap requested but no active snap → coordinate used as-is
|
|
115
115
|
const h = createHarness()
|
|
116
116
|
h.ctx.moveVertex({ ...h.state, selectedVertexIndex: 0 }, { lng: 1, lat: 2 }, { checkSnap: true })
|
|
117
|
-
expect(h.ctx.
|
|
117
|
+
expect(h.ctx.getVertices('feat-1')[0]).toEqual([1, 2])
|
|
118
118
|
})
|
|
119
119
|
|
|
120
120
|
test('deleteVertex removes a vertex, or no-ops for missing feature / bad index / minimum size', () => {
|
|
@@ -137,6 +137,6 @@ describe('vertexOperations', () => {
|
|
|
137
137
|
const line = createHarness(LINE())
|
|
138
138
|
line.state.selectedVertexIndex = 1
|
|
139
139
|
line.ctx.deleteVertex(line.state)
|
|
140
|
-
expect(line.ctx.
|
|
140
|
+
expect(line.ctx.getVertices('feat-1')).toHaveLength(2)
|
|
141
141
|
})
|
|
142
142
|
})
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getCoords,
|
|
3
|
+
getMidpointCoords,
|
|
3
4
|
getRingSegments,
|
|
4
5
|
getSegmentForIndex
|
|
5
6
|
} from './geometryHelpers.js'
|
|
6
|
-
import { spatialNavigate } from '
|
|
7
|
+
import { spatialNavigate } from '../../../../../../../src/utils/spatialNavigate.js'
|
|
7
8
|
|
|
8
9
|
export const vertexQueries = {
|
|
9
10
|
findVertexIndex (coords, targetCoord, currentIdx) {
|
|
@@ -40,56 +41,35 @@ export const vertexQueries = {
|
|
|
40
41
|
},
|
|
41
42
|
|
|
42
43
|
syncVertices (state) {
|
|
43
|
-
state.
|
|
44
|
+
state.vertices = this.getVertices(state.featureId)
|
|
44
45
|
state.midpoints = this.getMidpoints(state.featureId)
|
|
45
46
|
},
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
getVertices (featureId) {
|
|
48
49
|
return getCoords(this.getFeature(featureId))
|
|
49
50
|
},
|
|
50
51
|
|
|
51
52
|
getMidpoints (featureId) {
|
|
52
|
-
|
|
53
|
-
const coords = getCoords(feature)
|
|
54
|
-
const segments = getRingSegments(feature)
|
|
55
|
-
if (!coords?.length || !segments.length) {
|
|
56
|
-
return []
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const midpoints = []
|
|
60
|
-
// Create midpoints within each segment, respecting boundaries
|
|
61
|
-
for (const seg of segments) {
|
|
62
|
-
// For closed rings, create midpoint between every vertex including last→first
|
|
63
|
-
// For open lines, create midpoints only between consecutive vertices (no wrap-around)
|
|
64
|
-
const count = seg.closed ? seg.length : seg.length - 1
|
|
65
|
-
for (let i = 0; i < count; i++) {
|
|
66
|
-
const idx = seg.start + i
|
|
67
|
-
const nextIdx = seg.start + ((i + 1) % seg.length)
|
|
68
|
-
const [x1, y1] = coords[idx]
|
|
69
|
-
const [x2, y2] = coords[nextIdx]
|
|
70
|
-
midpoints.push([(x1 + x2) / 2, (y1 + y2) / 2])
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return midpoints
|
|
53
|
+
return getMidpointCoords(this.getFeature(featureId))
|
|
74
54
|
},
|
|
75
55
|
|
|
76
56
|
getVertexOrMidpoint (state, direction) {
|
|
77
57
|
// Ensure vertices and midpoints are populated
|
|
78
|
-
if (!state.
|
|
79
|
-
state.
|
|
58
|
+
if (!state.vertices?.length) {
|
|
59
|
+
state.vertices = this.getVertices(state.featureId)
|
|
80
60
|
state.midpoints = this.getMidpoints(state.featureId)
|
|
81
61
|
}
|
|
82
|
-
if (!state.
|
|
62
|
+
if (!state.vertices?.length) {
|
|
83
63
|
return [-1, null]
|
|
84
64
|
}
|
|
85
65
|
const project = (p) => p ? Object.values(this.map.project(p)) : null
|
|
86
|
-
const pixels = [...state.
|
|
66
|
+
const pixels = [...state.vertices.map(project), ...state.midpoints.map(project)].filter(Boolean)
|
|
87
67
|
if (!pixels.length) {
|
|
88
68
|
return [-1, null]
|
|
89
69
|
}
|
|
90
70
|
const start = pixels[state.selectedVertexIndex] || Object.values(this.map.project(this.map.getCenter()))
|
|
91
71
|
const idx = spatialNavigate(start, pixels, direction)
|
|
92
|
-
return [idx, idx < state.
|
|
72
|
+
return [idx, idx < state.vertices.length ? 'vertex' : 'midpoint']
|
|
93
73
|
},
|
|
94
74
|
|
|
95
75
|
getVertexIndexFromMidpoint (state, coordPath) {
|
|
@@ -108,7 +88,7 @@ export const vertexQueries = {
|
|
|
108
88
|
const insertionIdx = parts[parts.length - 1]
|
|
109
89
|
const localMidpointIdx = insertionIdx > 0 ? insertionIdx - 1 : seg.length - 2
|
|
110
90
|
// Midpoints are indexed after all vertices
|
|
111
|
-
return state.
|
|
91
|
+
return state.vertices.length + midpointOffset + localMidpointIdx
|
|
112
92
|
}
|
|
113
93
|
// Count midpoints in this segment (must match getMidpoints calculation)
|
|
114
94
|
const segMidpoints = seg.closed ? seg.length : seg.length - 1
|
|
@@ -116,6 +96,6 @@ export const vertexQueries = {
|
|
|
116
96
|
}
|
|
117
97
|
|
|
118
98
|
// Fallback
|
|
119
|
-
return state.
|
|
99
|
+
return state.vertices.length
|
|
120
100
|
}
|
|
121
101
|
}
|
|
@@ -17,9 +17,9 @@ describe('vertexQueries', () => {
|
|
|
17
17
|
expect(ctx.getCoordPath({ featureId: 'missing' }, 0)).toBe('0')
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
-
test('
|
|
20
|
+
test('getVertices / getMidpoints for closed rings, open lines and missing features', () => {
|
|
21
21
|
const { ctx } = createHarness()
|
|
22
|
-
expect(ctx.
|
|
22
|
+
expect(ctx.getVertices('feat-1')).toHaveLength(4)
|
|
23
23
|
expect(ctx.getMidpoints('feat-1')).toHaveLength(4)
|
|
24
24
|
expect(ctx.getMidpoints('missing')).toEqual([])
|
|
25
25
|
const line = createHarness(LINE())
|
|
@@ -28,29 +28,29 @@ describe('vertexQueries', () => {
|
|
|
28
28
|
|
|
29
29
|
test('syncVertices refreshes the cached vertex and midpoint lists', () => {
|
|
30
30
|
const { ctx, state } = createHarness()
|
|
31
|
-
state.
|
|
31
|
+
state.vertices = []
|
|
32
32
|
state.midpoints = []
|
|
33
33
|
ctx.syncVertices(state)
|
|
34
|
-
expect(state.
|
|
34
|
+
expect(state.vertices).toHaveLength(4)
|
|
35
35
|
expect(state.midpoints).toHaveLength(4)
|
|
36
36
|
})
|
|
37
37
|
|
|
38
38
|
test('getVertexOrMidpoint repopulates empty lists, navigates, and guards empty features', () => {
|
|
39
39
|
const { ctx, state } = createHarness()
|
|
40
|
-
state.
|
|
40
|
+
state.vertices = []
|
|
41
41
|
state.selectedVertexIndex = 0
|
|
42
42
|
const [idx, type] = ctx.getVertexOrMidpoint(state, 'ArrowRight')
|
|
43
43
|
expect(idx).toBeGreaterThanOrEqual(0)
|
|
44
44
|
expect(['vertex', 'midpoint']).toContain(type)
|
|
45
45
|
|
|
46
|
-
expect(ctx.getVertexOrMidpoint({ featureId: 'missing',
|
|
47
|
-
expect(ctx.getVertexOrMidpoint({ ...state,
|
|
46
|
+
expect(ctx.getVertexOrMidpoint({ featureId: 'missing', vertices: [] }, 'ArrowRight')).toEqual([-1, null])
|
|
47
|
+
expect(ctx.getVertexOrMidpoint({ ...state, vertices: [null], midpoints: [], selectedVertexIndex: 0 }, 'ArrowRight')).toEqual([-1, null])
|
|
48
48
|
|
|
49
49
|
// Falls back to the map centre when the current index has no pixel, and can resolve a vertex target
|
|
50
50
|
const fresh = createHarness()
|
|
51
51
|
expect(fresh.ctx.getVertexOrMidpoint({ ...fresh.state, selectedVertexIndex: -1 }, 'ArrowRight')[0]).toBeGreaterThanOrEqual(0)
|
|
52
52
|
const types = new Set()
|
|
53
|
-
for (let s = 0; s < fresh.state.
|
|
53
|
+
for (let s = 0; s < fresh.state.vertices.length + fresh.state.midpoints.length; s++) {
|
|
54
54
|
for (const d of ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown']) {
|
|
55
55
|
types.add(fresh.ctx.getVertexOrMidpoint({ ...fresh.state, selectedVertexIndex: s }, d)[1])
|
|
56
56
|
}
|
|
@@ -66,8 +66,8 @@ describe('vertexQueries', () => {
|
|
|
66
66
|
|
|
67
67
|
// Counts earlier segments for multi-part geometry (closed and open)
|
|
68
68
|
features.set('multipoly', { type: 'MultiPolygon', coordinates: [[[[0, 0], [1, 0], [1, 1], [0, 0]]], [[[5, 5], [6, 5], [6, 6], [5, 5]]]] })
|
|
69
|
-
expect(ctx.getVertexIndexFromMidpoint({ ...state, featureId: 'multipoly',
|
|
69
|
+
expect(ctx.getVertexIndexFromMidpoint({ ...state, featureId: 'multipoly', vertices: new Array(8).fill([0, 0]) }, '1.0.1')).toBeGreaterThan(8)
|
|
70
70
|
features.set('multiline', { type: 'MultiLineString', coordinates: [[[0, 0], [1, 1]], [[5, 5], [6, 6]]] })
|
|
71
|
-
expect(ctx.getVertexIndexFromMidpoint({ ...state, featureId: 'multiline',
|
|
71
|
+
expect(ctx.getVertexIndexFromMidpoint({ ...state, featureId: 'multiline', vertices: new Array(4).fill([0, 0]) }, '1.1')).toBeGreaterThan(4)
|
|
72
72
|
})
|
|
73
73
|
})
|