@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
|
@@ -10,7 +10,7 @@ describe('mouse clicks (polygon)', () => {
|
|
|
10
10
|
expect(state.polygon.coordinates[0]).toHaveLength(3)
|
|
11
11
|
expect(state.polygon.coordinates[0][0]).toEqual([0, 0])
|
|
12
12
|
expect(state.polygon.coordinates[0][1]).toEqual([10, 0])
|
|
13
|
-
expect(firedWith(ctx.map, 'draw.vertexchange').pop()).toEqual({
|
|
13
|
+
expect(firedWith(ctx.map, 'draw.vertexchange').pop()).toEqual({ numVertices: 2 })
|
|
14
14
|
expect(ctx.map._undoStack.length).toBe(2)
|
|
15
15
|
})
|
|
16
16
|
|
|
@@ -132,6 +132,16 @@ describe('add-vertex button and doClick', () => {
|
|
|
132
132
|
expect(ctx.map._undoStack.length).toBe(1)
|
|
133
133
|
})
|
|
134
134
|
|
|
135
|
+
// Regression: lifecycle.js's shared crosshair.activate wiring calls this._placeAtCrossHair
|
|
136
|
+
// for every create mode — drawPointMode.js defines its own, but polygon/line only had
|
|
137
|
+
// doClick under a different name, so "Click Target" (Voice Control) silently no-opped.
|
|
138
|
+
test('_placeAtCrossHair aliases doClick — Voice Control\'s "Click Target" places a vertex too', () => {
|
|
139
|
+
const { ctx, state } = setup(DrawPolygonMode, { interfaceType: 'keyboard' })
|
|
140
|
+
ctx._placeAtCrossHair(state)
|
|
141
|
+
expect(state.polygon.coordinates[0][0]).toEqual([CENTER.lng, CENTER.lat])
|
|
142
|
+
expect(ctx.map._undoStack.length).toBe(1)
|
|
143
|
+
})
|
|
144
|
+
|
|
135
145
|
test('does not finish a line via doClick while the geometry is invalid', () => {
|
|
136
146
|
const { ctx, state } = setup(DrawLineMode)
|
|
137
147
|
clickAt(ctx, state, 0, 0)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { stopIfGlobalAltKey } from '../../../../../../../src/utils/globalAltShortcuts.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Setup / teardown for the shared draw mode: binds the window/container/map event
|
|
3
5
|
* handlers on entry and removes them on exit. Part of createDrawMode.
|
|
@@ -61,6 +63,10 @@ export const createLifecycle = ({ ParentMode, featureProp, excludeFeatureIdFromS
|
|
|
61
63
|
this._listeners = [
|
|
62
64
|
[window, 'keydown', this.keydownHandler],
|
|
63
65
|
[window, 'keyup', this.keyupHandler],
|
|
66
|
+
// Capture phase (unlike keyupHandler above) so it runs before the event ever bubbles to
|
|
67
|
+
// useKeyboardShortcuts.js's app-wide listener — shadows global Alt+<key> shortcuts
|
|
68
|
+
// (src/utils/globalAltShortcuts.js) unconditionally while drawing.
|
|
69
|
+
[window, 'keyup', stopIfGlobalAltKey, { capture: true }],
|
|
64
70
|
[window, 'click', this.vertexButtonClickHandler],
|
|
65
71
|
[container, 'blur', this.blurHandler],
|
|
66
72
|
[container, 'pointermove', this.pointermoveHandler],
|
|
@@ -71,14 +77,14 @@ export const createLifecycle = ({ ParentMode, featureProp, excludeFeatureIdFromS
|
|
|
71
77
|
[map, 'draw.undo', this.undoHandler],
|
|
72
78
|
[map, 'draw.interfacetypechange', this.interfaceTypeChangeHandler]
|
|
73
79
|
]
|
|
74
|
-
this._listeners.forEach(([
|
|
80
|
+
this._listeners.forEach(([target, eventName, handler, opts]) => target.addEventListener ? target.addEventListener(eventName, handler, opts) : target.on(eventName, handler))
|
|
75
81
|
|
|
76
82
|
return state
|
|
77
83
|
},
|
|
78
84
|
|
|
79
85
|
onStop (state) {
|
|
80
86
|
ParentMode.onStop.call(this, state)
|
|
81
|
-
this._listeners.forEach(([
|
|
87
|
+
this._listeners.forEach(([target, eventName, handler, opts]) => target.removeEventListener ? target.removeEventListener(eventName, handler, opts) : target.off(eventName, handler))
|
|
82
88
|
// Don't leave a stale closure over this mode's state on the shared crossHair object once
|
|
83
89
|
// it's gone — the next owner (e.g. interact re-enabling) assigns its own before this one
|
|
84
90
|
// could ever be invoked again, but this avoids relying on that ordering.
|
|
@@ -50,6 +50,41 @@ describe('setup and crosshair', () => {
|
|
|
50
50
|
})
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
+
describe('global Alt+<key> shortcut shadowing', () => {
|
|
54
|
+
test('Alt+Enter keyup stops propagation while drawing — never reaches the app-wide "highlight label at center" shortcut', () => {
|
|
55
|
+
setup(DrawPolygonMode)
|
|
56
|
+
const event = new KeyboardEvent('keyup', { key: 'Enter', altKey: true, cancelable: true, bubbles: true })
|
|
57
|
+
const stopSpy = jest.spyOn(event, 'stopPropagation')
|
|
58
|
+
window.dispatchEvent(event)
|
|
59
|
+
expect(stopSpy).toHaveBeenCalled()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('Alt+Arrow keyup stops propagation too — never reaches the app-wide "highlight next label" shortcut', () => {
|
|
63
|
+
setup(DrawPolygonMode)
|
|
64
|
+
const event = new KeyboardEvent('keyup', { key: 'ArrowRight', altKey: true, cancelable: true, bubbles: true })
|
|
65
|
+
const stopSpy = jest.spyOn(event, 'stopPropagation')
|
|
66
|
+
window.dispatchEvent(event)
|
|
67
|
+
expect(stopSpy).toHaveBeenCalled()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('a plain Enter keyup (no Alt) is left alone', () => {
|
|
71
|
+
setup(DrawPolygonMode)
|
|
72
|
+
const event = new KeyboardEvent('keyup', { key: 'Enter', altKey: false, cancelable: true, bubbles: true })
|
|
73
|
+
const stopSpy = jest.spyOn(event, 'stopPropagation')
|
|
74
|
+
window.dispatchEvent(event)
|
|
75
|
+
expect(stopSpy).not.toHaveBeenCalled()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('the shadow listener is removed on onStop — Alt+Enter bubbles normally again', () => {
|
|
79
|
+
const { ctx, state } = setup(DrawPolygonMode)
|
|
80
|
+
ctx.onStop(state)
|
|
81
|
+
const event = new KeyboardEvent('keyup', { key: 'Enter', altKey: true, cancelable: true, bubbles: true })
|
|
82
|
+
const stopSpy = jest.spyOn(event, 'stopPropagation')
|
|
83
|
+
window.dispatchEvent(event)
|
|
84
|
+
expect(stopSpy).not.toHaveBeenCalled()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
53
88
|
describe('onStop', () => {
|
|
54
89
|
test('mouse session: removes listeners, hides the crosshair and reports the final interface type', () => {
|
|
55
90
|
const { ctx, state, marker, container } = setup(DrawPolygonMode, { interfaceType: 'mouse' })
|
|
@@ -7,17 +7,17 @@ import {
|
|
|
7
7
|
* switching, rubber-band movement (with snapping) and blur. Part of createDrawMode.
|
|
8
8
|
*/
|
|
9
9
|
export const createPointerHandlers = ({ ParentMode, getFeature, getCoords }) => ({
|
|
10
|
-
onTouchStart (state,
|
|
10
|
+
onTouchStart (state, event) {
|
|
11
11
|
this._setInterface(state, 'touch')
|
|
12
|
-
this.onMove(state,
|
|
12
|
+
this.onMove(state, event)
|
|
13
13
|
},
|
|
14
14
|
|
|
15
|
-
onTouchEnd (state,
|
|
15
|
+
onTouchEnd (state, event) {
|
|
16
16
|
this._setInterface(state, 'touch')
|
|
17
|
-
this.onMove(state,
|
|
17
|
+
this.onMove(state, event)
|
|
18
18
|
},
|
|
19
19
|
|
|
20
|
-
// The global interface type (e.g. switching to touch and panning via
|
|
20
|
+
// The global interface type (e.g. switching to touch and panning via MapControls)
|
|
21
21
|
// can change mid-session without any touch/pointer/key event ever landing on the
|
|
22
22
|
// map container, so this can't rely on onTouchStart/onPointerdown alone — refresh
|
|
23
23
|
// the rubber band immediately rather than waiting for the next incidental 'move'.
|
|
@@ -25,37 +25,47 @@ export const createPointerHandlers = ({ ParentMode, getFeature, getCoords }) =>
|
|
|
25
25
|
// draw_polygon/draw_line starts), so this must only show the crosshair for
|
|
26
26
|
// touch/keyboard — same as onSetup/onMove — not unconditionally like touch's own
|
|
27
27
|
// onTouchStart/onTouchEnd, or a mouse-driven session would flash it on every entry.
|
|
28
|
-
onInterfaceTypeChange (state,
|
|
29
|
-
this._setInterface(state,
|
|
28
|
+
onInterfaceTypeChange (state, event) {
|
|
29
|
+
this._setInterface(state, event.interfaceType, ['touch', 'keyboard'].includes(event.interfaceType))
|
|
30
30
|
this.onMove(state)
|
|
31
31
|
},
|
|
32
32
|
|
|
33
|
-
onBlur (state,
|
|
34
|
-
if (
|
|
33
|
+
onBlur (state, event) {
|
|
34
|
+
if (event.target !== state.container) {
|
|
35
35
|
this._hideCrossHair(state)
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
|
|
39
|
-
onMouseMove (state,
|
|
39
|
+
onMouseMove (state, event) {
|
|
40
40
|
if (isSnapEnabled(state)) {
|
|
41
41
|
const snap = getSnapInstance(this.map)
|
|
42
|
-
triggerSnapAtPoint(snap, this.map,
|
|
42
|
+
triggerSnapAtPoint(snap, this.map, event.point)
|
|
43
43
|
|
|
44
44
|
const snappedLngLat = getSnapLngLat(snap)
|
|
45
45
|
if (snappedLngLat) {
|
|
46
|
-
|
|
46
|
+
event = { ...event, lngLat: snappedLngLat }
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
ParentMode.onMouseMove.call(this, state,
|
|
50
|
+
ParentMode.onMouseMove.call(this, state, event)
|
|
51
51
|
|
|
52
52
|
// Fired after the parent updates the rubber band so the payload (and the live
|
|
53
53
|
// invalid-stroke check driven by it) reflects the current cursor position.
|
|
54
54
|
this.map.fire('draw.geometrychange', state.polygon || state.line)
|
|
55
55
|
},
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// The crosshair/rubber-band candidate normally only tracks the map centre for
|
|
58
|
+
// touch/keyboard interfaceType, since a real mouse cursor already drives it via
|
|
59
|
+
// onMouseMove. But Voice Control's simulated clicks report pointerType 'mouse' (so
|
|
60
|
+
// interfaceType never leaves 'mouse') and produce no mousemove at all — panning via
|
|
61
|
+
// MapControls while interfaceType is 'mouse' would otherwise leave the candidate
|
|
62
|
+
// stale. `map.on('move', ...)` fires this with the real MapLibre move event, whose
|
|
63
|
+
// `originalEvent` is only set for a live mouse/touch/wheel interaction (dragPan,
|
|
64
|
+
// scrollZoom, ...) — absent for a programmatic move like MapControls' panBy, which
|
|
65
|
+
// is exactly the case that needs this fallback.
|
|
66
|
+
onMove (state, event) {
|
|
67
|
+
const isProgrammaticMapMove = event?.type === 'move' && !event.originalEvent
|
|
68
|
+
if (['touch', 'keyboard'].includes(state.interfaceType) || isProgrammaticMapMove) {
|
|
59
69
|
if (isSnapEnabled(state)) {
|
|
60
70
|
triggerSnapAtCenter(getSnapInstance(this.map), this.map)
|
|
61
71
|
}
|
|
@@ -85,14 +95,14 @@ export const createPointerHandlers = ({ ParentMode, getFeature, getCoords }) =>
|
|
|
85
95
|
}
|
|
86
96
|
},
|
|
87
97
|
|
|
88
|
-
onPointerdown (state,
|
|
89
|
-
if (
|
|
98
|
+
onPointerdown (state, event) {
|
|
99
|
+
if (event.pointerType !== 'touch') {
|
|
90
100
|
this._setInterface(state, 'mouse', false)
|
|
91
101
|
}
|
|
92
102
|
},
|
|
93
103
|
|
|
94
|
-
onPointermove (state,
|
|
95
|
-
if (
|
|
104
|
+
onPointermove (state, event) {
|
|
105
|
+
if (event.pointerType !== 'touch') {
|
|
96
106
|
this._hideCrossHair(state)
|
|
97
107
|
}
|
|
98
108
|
},
|
|
@@ -33,7 +33,7 @@ describe('touch and pointer interface', () => {
|
|
|
33
33
|
clickAt(ctx, state, 0, 0)
|
|
34
34
|
ctx.map.fire.mockClear()
|
|
35
35
|
ctx.pointerupHandler({})
|
|
36
|
-
expect(firedWith(ctx.map, 'draw.vertexchange')).toEqual([{
|
|
36
|
+
expect(firedWith(ctx.map, 'draw.vertexchange')).toEqual([{ numVertices: 1 }])
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
test('blur away from the container hides the crosshair; blur on the container does not', () => {
|
|
@@ -44,7 +44,7 @@ describe('touch and pointer interface', () => {
|
|
|
44
44
|
expect(marker.style.display).toBe('none')
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
-
test('draw.interfacetypechange (e.g. switching to touch and panning via
|
|
47
|
+
test('draw.interfacetypechange (e.g. switching to touch and panning via MapControls mid-session) updates the interface and refreshes the rubber band immediately', () => {
|
|
48
48
|
const { ctx, state } = setup(DrawPolygonMode)
|
|
49
49
|
clickAt(ctx, state, 0, 0)
|
|
50
50
|
ctx.map.fire('draw.interfacetypechange', { interfaceType: 'touch' })
|
|
@@ -116,6 +116,21 @@ describe('rubber band and snapping while moving', () => {
|
|
|
116
116
|
expect(state.polygon.coordinates[0]).toEqual(before)
|
|
117
117
|
})
|
|
118
118
|
|
|
119
|
+
test('a programmatic map move (e.g. MapControls panBy while Voice Control keeps interfaceType mouse) still tracks the rubber band to center', () => {
|
|
120
|
+
const { ctx, state } = setup(DrawPolygonMode)
|
|
121
|
+
clickAt(ctx, state, 0, 0)
|
|
122
|
+
ctx.map.fire('move', { type: 'move' }) // no originalEvent — matches a programmatic move, unlike a real drag/scroll
|
|
123
|
+
expect(state.polygon.coordinates[0].at(-1)).toEqual([CENTER.lng, CENTER.lat])
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('a real mouse-driven map move (dragPan/scrollZoom) still leaves the rubber band alone', () => {
|
|
127
|
+
const { ctx, state } = setup(DrawPolygonMode)
|
|
128
|
+
clickAt(ctx, state, 0, 0)
|
|
129
|
+
const before = [...state.polygon.coordinates[0].map((c) => [...c])]
|
|
130
|
+
ctx.map.fire('move', { type: 'move', originalEvent: new MouseEvent('mousemove') })
|
|
131
|
+
expect(state.polygon.coordinates[0]).toEqual(before)
|
|
132
|
+
})
|
|
133
|
+
|
|
119
134
|
test('map move in keyboard line mode with snap fires geometrychange with state.line', () => {
|
|
120
135
|
const { ctx, state } = setup(DrawLineMode, { interfaceType: 'keyboard', getSnapEnabled: () => true })
|
|
121
136
|
clickAt(ctx, state, 0, 0)
|
|
@@ -126,7 +126,7 @@ describe('undo via draw.undo event and reinitialisation', () => {
|
|
|
126
126
|
expect(ctx._ctx.store.delete).toHaveBeenCalledWith([id])
|
|
127
127
|
expect(state.polygon.id).toBe(id)
|
|
128
128
|
expect(state.currentVertexPosition).toBe(0)
|
|
129
|
-
expect(firedWith(ctx.map, 'draw.vertexchange').pop()).toEqual({
|
|
129
|
+
expect(firedWith(ctx.map, 'draw.vertexchange').pop()).toEqual({ numVertices: 1 })
|
|
130
130
|
})
|
|
131
131
|
|
|
132
132
|
test('undoing the only line vertex restarts draw_line with the same feature id', () => {
|
|
@@ -62,8 +62,8 @@ const pointHandlers = {
|
|
|
62
62
|
// after the re-id (not before) so it writes to the feature under its final id — the
|
|
63
63
|
// resolver itself is injected by the adapter via changeMode's options so this mode has
|
|
64
64
|
// no direct dependency on symbolRegistry/mapProvider.
|
|
65
|
-
onCreate (state,
|
|
66
|
-
const feature =
|
|
65
|
+
onCreate (state, event) {
|
|
66
|
+
const feature = event.features[0]
|
|
67
67
|
this._ctx.api.delete(feature.id)
|
|
68
68
|
feature.id = state.featureId
|
|
69
69
|
this._ctx.api.add(feature, { userProperties: true })
|
|
@@ -78,52 +78,58 @@ const pointHandlers = {
|
|
|
78
78
|
// update (nothing to rubber-band for a single coordinate). Bound to MapLibre's own 'move'
|
|
79
79
|
// event by createLifecycle, so this fires continuously while panning; also called
|
|
80
80
|
// explicitly below wherever the interface type switches to touch/keyboard, so the
|
|
81
|
-
// indicator appears immediately rather than waiting for the first pan.
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
// indicator appears immediately rather than waiting for the first pan. Also runs for a
|
|
82
|
+
// programmatic move with interfaceType still 'mouse' — Voice Control's simulated clicks
|
|
83
|
+
// (e.g. on MapControls) report pointerType 'mouse' and produce no mousemove, so without
|
|
84
|
+
// this the indicator would never refresh while panning that way; a real map 'move' event's
|
|
85
|
+
// `originalEvent` is only set for a live mouse/touch/wheel interaction, not a programmatic
|
|
86
|
+
// one like MapControls' panBy.
|
|
87
|
+
onMove (state, event) {
|
|
88
|
+
const isProgrammaticMapMove = event?.type === 'move' && !event.originalEvent
|
|
89
|
+
if ((['touch', 'keyboard'].includes(state.interfaceType) || isProgrammaticMapMove) && isSnapEnabled(state)) {
|
|
84
90
|
triggerSnapAtCenter(getSnapInstance(this.map), this.map)
|
|
85
91
|
}
|
|
86
92
|
},
|
|
87
93
|
|
|
88
|
-
onBlur (state,
|
|
89
|
-
if (
|
|
94
|
+
onBlur (state, event) {
|
|
95
|
+
if (event.target !== state.container) { this._hideCrossHair(state) }
|
|
90
96
|
},
|
|
91
97
|
|
|
92
|
-
onPointerdown (state,
|
|
93
|
-
if (
|
|
98
|
+
onPointerdown (state, event) {
|
|
99
|
+
if (event.pointerType !== 'touch') { this._setInterface(state, 'mouse', false) }
|
|
94
100
|
},
|
|
95
101
|
|
|
96
|
-
onPointermove (state,
|
|
97
|
-
if (
|
|
102
|
+
onPointermove (state, event) {
|
|
103
|
+
if (event.pointerType !== 'touch') { this._hideCrossHair(state) }
|
|
98
104
|
},
|
|
99
105
|
|
|
100
106
|
onTouchStart (state) { this._setInterface(state, 'touch'); this.onMove(state) },
|
|
101
107
|
onTouchEnd (state) { this._setInterface(state, 'touch'); this.onMove(state) },
|
|
102
108
|
|
|
103
|
-
onInterfaceTypeChange (state,
|
|
104
|
-
this._setInterface(state,
|
|
109
|
+
onInterfaceTypeChange (state, event) {
|
|
110
|
+
this._setInterface(state, event.interfaceType, ['touch', 'keyboard'].includes(event.interfaceType))
|
|
105
111
|
this.onMove(state)
|
|
106
112
|
},
|
|
107
113
|
|
|
108
114
|
// Continuously track a snap candidate under the real cursor (mouse only — touch/keyboard
|
|
109
115
|
// placement tracks the crosshair instead, triggered once at commit time in _placeAtCrossHair).
|
|
110
|
-
onMouseMove (state,
|
|
116
|
+
onMouseMove (state, event) {
|
|
111
117
|
if (isSnapEnabled(state)) {
|
|
112
|
-
triggerSnapAtPoint(getSnapInstance(this.map), this.map,
|
|
118
|
+
triggerSnapAtPoint(getSnapInstance(this.map), this.map, event.point)
|
|
113
119
|
}
|
|
114
120
|
},
|
|
115
121
|
|
|
116
|
-
onVertexButtonClick (state,
|
|
117
|
-
if (state.addVertexButtonId && !this.map._undoInProgress &&
|
|
122
|
+
onVertexButtonClick (state, event) {
|
|
123
|
+
if (state.addVertexButtonId && !this.map._undoInProgress && event.target.closest(`#${state.addVertexButtonId}`)) {
|
|
118
124
|
this._placeAtCrossHair(state)
|
|
119
125
|
}
|
|
120
126
|
},
|
|
121
127
|
|
|
122
|
-
onKeydown (state,
|
|
128
|
+
onKeydown (state, event) {
|
|
123
129
|
if (document.activeElement !== state.container) { return }
|
|
124
|
-
if (
|
|
125
|
-
if (
|
|
126
|
-
if (!INTERFACE_KEYS.has(
|
|
130
|
+
if (event.key === 'Escape') { event.preventDefault(); return }
|
|
131
|
+
if (event.key === 'Enter') { state.isActive = true }
|
|
132
|
+
if (!INTERFACE_KEYS.has(event.key)) { return }
|
|
127
133
|
this._setInterface(state, 'keyboard')
|
|
128
134
|
this.onMove(state)
|
|
129
135
|
},
|
|
@@ -132,26 +138,26 @@ const pointHandlers = {
|
|
|
132
138
|
// Escape a pending placement without ever focusing the map, and keyboard users placing via
|
|
133
139
|
// crosshair can commit with Enter. A point has no partial ring to reinitialize the way
|
|
134
140
|
// line/polygon's Escape does, so Escape always routes to draw.cancel, keyboard-focused or not.
|
|
135
|
-
onKeyup (state,
|
|
136
|
-
if (
|
|
141
|
+
onKeyup (state, event) {
|
|
142
|
+
if (event.key === 'Escape') {
|
|
137
143
|
this.map.fire('draw.cancel')
|
|
138
144
|
return
|
|
139
145
|
}
|
|
140
146
|
if (document.activeElement !== state.container) { return }
|
|
141
|
-
if (!INTERFACE_KEYS.has(
|
|
147
|
+
if (!INTERFACE_KEYS.has(event.key)) { return }
|
|
142
148
|
this._setInterface(state, 'keyboard')
|
|
143
149
|
this.onMove(state)
|
|
144
|
-
if (
|
|
150
|
+
if (event.key === 'Enter' && state.isActive) { this._placeAtCrossHair(state) }
|
|
145
151
|
},
|
|
146
152
|
|
|
147
|
-
onClick (state,
|
|
148
|
-
if (
|
|
153
|
+
onClick (state, event) {
|
|
154
|
+
if (event.originalEvent.button > 0 || event.originalEvent.target !== this.map.getCanvas()) { return }
|
|
149
155
|
const snap = getSnapInstance(this.map)
|
|
150
156
|
const snapped = isSnapEnabled(state) && isSnapActive(snap) ? getSnapLngLat(snap) : null
|
|
151
|
-
this._commit(state, snapped ||
|
|
157
|
+
this._commit(state, snapped || event.lngLat)
|
|
152
158
|
},
|
|
153
159
|
|
|
154
|
-
onTap (state,
|
|
160
|
+
onTap (state, event) { this.onClick(state, event) },
|
|
155
161
|
|
|
156
162
|
// Touch "Add point" button and keyboard Enter both place at the crosshair (map centre),
|
|
157
163
|
// snapping to a candidate under it if one exists. Deliberately bypasses onClick — its
|
|
@@ -16,15 +16,15 @@ const createMap = () => {
|
|
|
16
16
|
getCenter: () => ({ ...CENTER }),
|
|
17
17
|
project: jest.fn(() => ({ x: 50, y: 50 })),
|
|
18
18
|
unproject: jest.fn(() => ({ ...CENTER })),
|
|
19
|
-
fire: jest.fn(function (type,
|
|
20
|
-
on: jest.fn((type,
|
|
21
|
-
off: jest.fn((type,
|
|
19
|
+
fire: jest.fn(function (type, event) { (listeners[type] ?? []).forEach((handler) => handler(event)) }),
|
|
20
|
+
on: jest.fn((type, handler) => { (listeners[type] ??= []).push(handler) }),
|
|
21
|
+
off: jest.fn((type, handler) => { listeners[type] = (listeners[type] ?? []).filter((x) => x !== handler) })
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const contexts = []
|
|
26
|
-
const removeListeners = (ctx) => ctx._listeners?.forEach(([
|
|
27
|
-
|
|
26
|
+
const removeListeners = (ctx) => ctx._listeners?.forEach(([target, eventName, handler]) =>
|
|
27
|
+
target.removeEventListener ? target.removeEventListener(eventName, handler) : target.off(eventName, handler))
|
|
28
28
|
|
|
29
29
|
const createModeContext = () => {
|
|
30
30
|
const map = createMap()
|
|
@@ -90,7 +90,7 @@ const clickEvent = (ctx, lng, lat, overrides = {}) => ({
|
|
|
90
90
|
originalEvent: { button: 0, target: ctx.map.getCanvas(), ...overrides }
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
-
const firedWith = (map, type) => map.fire.mock.calls.filter(([
|
|
93
|
+
const firedWith = (map, type) => map.fire.mock.calls.filter(([eventType]) => eventType === type).map(([, event]) => event)
|
|
94
94
|
|
|
95
95
|
afterEach(() => {
|
|
96
96
|
contexts.splice(0).forEach(removeListeners)
|
|
@@ -436,6 +436,24 @@ describe('DrawPointMode', () => {
|
|
|
436
436
|
expect(snap.snapToClosestPoint).not.toHaveBeenCalled()
|
|
437
437
|
})
|
|
438
438
|
|
|
439
|
+
// Voice Control's simulated clicks report pointerType 'mouse' (interfaceType never
|
|
440
|
+
// leaves 'mouse') and produce no mousemove, so panning via MapControls needs its own
|
|
441
|
+
// fallback: a real map 'move' event with no originalEvent (a programmatic move, unlike a
|
|
442
|
+
// live mouse drag/scroll) still refreshes the indicator even with interfaceType 'mouse'.
|
|
443
|
+
it('a programmatic map move still refreshes the indicator while interfaceType is mouse', () => {
|
|
444
|
+
const { ctx, state } = setup()
|
|
445
|
+
const snap = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
|
|
446
|
+
ctx.map._snapInstance = snap
|
|
447
|
+
state.getSnapEnabled = () => true
|
|
448
|
+
|
|
449
|
+
ctx.onMove(state, { type: 'move' })
|
|
450
|
+
expect(snap.snapToClosestPoint).toHaveBeenCalledWith({ point: expect.any(Object), lngLat: CENTER, _explicit: true })
|
|
451
|
+
|
|
452
|
+
snap.snapToClosestPoint.mockClear()
|
|
453
|
+
ctx.onMove(state, { type: 'move', originalEvent: new MouseEvent('mousemove') })
|
|
454
|
+
expect(snap.snapToClosestPoint).not.toHaveBeenCalled()
|
|
455
|
+
})
|
|
456
|
+
|
|
439
457
|
it('switching to touch or keyboard shows the snap indicator immediately, not just on the first pan', () => {
|
|
440
458
|
const snap = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
|
|
441
459
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ARROW_KEYS, ARROW_OFFSETS, isInteractiveElementFocused, isUndoShortcut, sharedKeyboardHandlers } from '../../utils/keyboardShortcuts.js'
|
|
2
|
+
import { stopIfGlobalAltKey } from '../../../../../../../src/utils/globalAltShortcuts.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Keyboard interaction for the point-edit mode: arrow-key/Shift+arrow nudge and Cmd/Ctrl+Z
|
|
@@ -10,7 +11,7 @@ import { ARROW_KEYS, ARROW_OFFSETS, isInteractiveElementFocused, isUndoShortcut,
|
|
|
10
11
|
export const keyboardHandlers = {
|
|
11
12
|
...sharedKeyboardHandlers,
|
|
12
13
|
|
|
13
|
-
onKeydown (state,
|
|
14
|
+
onKeydown (state, event) {
|
|
14
15
|
if (isInteractiveElementFocused(state)) {
|
|
15
16
|
return
|
|
16
17
|
}
|
|
@@ -18,30 +19,30 @@ export const keyboardHandlers = {
|
|
|
18
19
|
state.interfaceType = 'keyboard'
|
|
19
20
|
this.hideTouchPointIndicator(state)
|
|
20
21
|
|
|
21
|
-
if (
|
|
22
|
+
if (event.key === ' ') {
|
|
22
23
|
// Prevent the page from scrolling; there's nothing to select, the point already is.
|
|
23
|
-
|
|
24
|
+
event.preventDefault()
|
|
24
25
|
return
|
|
25
26
|
}
|
|
26
|
-
if (ARROW_KEYS.has(
|
|
27
|
-
this.handleArrowKey(state,
|
|
27
|
+
if (ARROW_KEYS.has(event.key)) {
|
|
28
|
+
this.handleArrowKey(state, event)
|
|
28
29
|
return
|
|
29
30
|
}
|
|
30
|
-
if (isUndoShortcut(
|
|
31
|
-
this.handleUndoShortcut(state,
|
|
31
|
+
if (isUndoShortcut(event)) {
|
|
32
|
+
this.handleUndoShortcut(state, event)
|
|
32
33
|
}
|
|
33
34
|
},
|
|
34
35
|
|
|
35
36
|
// A plain or Shift+arrow nudges the point (Shift's finer-step-vs-coarse-step distinction
|
|
36
37
|
// lives in pointOperations.js's getOffset). Alt+arrow is left unhandled — nothing to
|
|
37
38
|
// navigate to with a single coordinate.
|
|
38
|
-
handleArrowKey (state,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.movePointByKey(state,
|
|
39
|
+
handleArrowKey (state, event) {
|
|
40
|
+
event.preventDefault()
|
|
41
|
+
event.stopPropagation()
|
|
42
|
+
this.movePointByKey(state, event)
|
|
42
43
|
},
|
|
43
44
|
|
|
44
|
-
movePointByKey (state,
|
|
45
|
+
movePointByKey (state, event) {
|
|
45
46
|
const currentCoord = this.getPointCoord(state)
|
|
46
47
|
if (!currentCoord) {
|
|
47
48
|
return
|
|
@@ -52,25 +53,26 @@ export const keyboardHandlers = {
|
|
|
52
53
|
state._keyboardMoveStartPosition = [...currentCoord]
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
this.movePoint(state, this._keyboardMoveTarget(state,
|
|
56
|
+
this.movePoint(state, this._keyboardMoveTarget(state, event, currentCoord))
|
|
56
57
|
},
|
|
57
58
|
|
|
58
59
|
// Resolve the destination coordinate for a keyboard nudge, applying or breaking snap —
|
|
59
|
-
// delegates to the shared resolver (utils/snapMovement.js) also used by
|
|
60
|
+
// delegates to the shared resolver (utils/snapMovement.js) also used by MapControls'
|
|
60
61
|
// nudgePointByDelta, so both snap identically.
|
|
61
|
-
_keyboardMoveTarget (state,
|
|
62
|
-
const [dx, dy] = ARROW_OFFSETS[
|
|
63
|
-
return this.resolveSnapTarget(state, dx, dy, currentCoord, () => this.getNewCoord(state,
|
|
62
|
+
_keyboardMoveTarget (state, event, currentCoord) {
|
|
63
|
+
const [dx, dy] = ARROW_OFFSETS[event.key]
|
|
64
|
+
return this.resolveSnapTarget(state, dx, dy, currentCoord, () => this.getNewCoord(state, event))
|
|
64
65
|
},
|
|
65
66
|
|
|
66
|
-
onKeyup (state,
|
|
67
|
+
onKeyup (state, event) {
|
|
67
68
|
if (isInteractiveElementFocused(state)) {
|
|
68
69
|
return
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
state.interfaceType = 'keyboard'
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
stopIfGlobalAltKey(event)
|
|
74
|
+
if (ARROW_KEYS.has(event.key)) {
|
|
75
|
+
event.stopPropagation()
|
|
74
76
|
|
|
75
77
|
// Push undo for the whole held-key move sequence as one step
|
|
76
78
|
if (state._keyboardMoveStartPosition) {
|
|
@@ -39,7 +39,7 @@ export const pointOperations = {
|
|
|
39
39
|
},
|
|
40
40
|
|
|
41
41
|
// Moves the point by an explicit (dx, dy) unit direction — the entry point for
|
|
42
|
-
//
|
|
42
|
+
// MapControls' D-pad (see mapProvider.activeMoveTarget in events.js). Each call is one
|
|
43
43
|
// complete, undoable action (no held-key sequencing — a button click has no "held" state
|
|
44
44
|
// the way arrow keys do), honouring snap via the shared resolveSnapTarget above.
|
|
45
45
|
nudgePointByDelta (state, dx, dy, isLargeStep) {
|
|
@@ -89,10 +89,10 @@ export const EditPointMode = {
|
|
|
89
89
|
// every vertex selection change), a point never emits draw.vertexchange — that event
|
|
90
90
|
// unconditionally releases mapProvider.activeMoveTarget, so it must never fire here.
|
|
91
91
|
// attachEvents' resetState() (events.js) is the release point, on Done/Cancel/edit-finish.
|
|
92
|
-
//
|
|
92
|
+
// numVertices (misspelled) — MaplibreDrawAdapter.js's vertexselection handler only reads
|
|
93
93
|
// that exact key when normalising to the adapter contract's numVertices; firing the
|
|
94
94
|
// correctly-spelled key here would get silently clobbered to undefined instead.
|
|
95
|
-
this.map.fire(CUSTOM_DRAW_EVENTS.VERTEX_SELECTION, { index: 0,
|
|
95
|
+
this.map.fire(CUSTOM_DRAW_EVENTS.VERTEX_SELECTION, { index: 0, numVertices: 1 })
|
|
96
96
|
|
|
97
97
|
return state
|
|
98
98
|
},
|
|
@@ -33,9 +33,9 @@ describe('onSetup / onStop lifecycle', () => {
|
|
|
33
33
|
|
|
34
34
|
test('claims the D-pad exactly once at setup, never followed by draw.vertexchange', () => {
|
|
35
35
|
const { map } = createHarness()
|
|
36
|
-
//
|
|
36
|
+
// numVertices (misspelled) — MaplibreDrawAdapter.js's vertexselection handler only reads
|
|
37
37
|
// that exact key when normalising to the adapter contract's numVertices.
|
|
38
|
-
expect(map.fire).toHaveBeenCalledWith('draw.vertexselection', { index: 0,
|
|
38
|
+
expect(map.fire).toHaveBeenCalledWith('draw.vertexselection', { index: 0, numVertices: 1 })
|
|
39
39
|
expect(map.fire).not.toHaveBeenCalledWith('draw.vertexchange', expect.anything())
|
|
40
40
|
})
|
|
41
41
|
|
|
@@ -124,7 +124,7 @@ describe('scale, move and interface-type events', () => {
|
|
|
124
124
|
expect(state.touchPointTarget.style.display).toBe('block')
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
-
test('draw.nudgevertex moves the point and repositions the touch target — the inbound bridge for
|
|
127
|
+
test('draw.nudgevertex moves the point and repositions the touch target — the inbound bridge for MapControls.mapProvider.activeMoveTarget', () => {
|
|
128
128
|
jest.useFakeTimers()
|
|
129
129
|
const { state, map } = createHarness(undefined, { interfaceType: 'touch' })
|
|
130
130
|
jest.runAllTimers() // flush onSetup's deferred initial touch-target positioning
|
|
@@ -29,6 +29,37 @@ export const getCoords = (feature) => {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Get the midpoint coordinate between every pair of adjacent vertices, one per segment
|
|
34
|
+
* boundary — closed rings (Polygon) also include the last→first midpoint, open lines
|
|
35
|
+
* (LineString) don't wrap around. Pure function of the geometry alone (no live mode state),
|
|
36
|
+
* so both the live mode (vertexQueries.js's getMidpoints) and read-only consumers (e.g. the
|
|
37
|
+
* adapter's getVertexItems) share one implementation.
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} feature - GeoJSON geometry object
|
|
40
|
+
* @returns {Array<[number, number]>} Flat array of midpoint coordinates, same ordering/segment
|
|
41
|
+
* boundaries as getCoords/getRingSegments
|
|
42
|
+
*/
|
|
43
|
+
export const getMidpointCoords = (feature) => {
|
|
44
|
+
const coords = getCoords(feature)
|
|
45
|
+
const segments = getRingSegments(feature)
|
|
46
|
+
if (!coords.length || !segments.length) {
|
|
47
|
+
return []
|
|
48
|
+
}
|
|
49
|
+
const midpoints = []
|
|
50
|
+
for (const seg of segments) {
|
|
51
|
+
const count = seg.closed ? seg.length : seg.length - 1
|
|
52
|
+
for (let i = 0; i < count; i++) {
|
|
53
|
+
const idx = seg.start + i
|
|
54
|
+
const nextIdx = seg.start + ((i + 1) % seg.length)
|
|
55
|
+
const [x1, y1] = coords[idx]
|
|
56
|
+
const [x2, y2] = coords[nextIdx]
|
|
57
|
+
midpoints.push([(x1 + x2) / 2, (y1 + y2) / 2])
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return midpoints
|
|
61
|
+
}
|
|
62
|
+
|
|
32
63
|
/**
|
|
33
64
|
* Get segment metadata for multi-ring/multi-part geometries.
|
|
34
65
|
* Each segment represents a ring (for Polygon) or part (for Multi*).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
getCoords, getRingSegments, getSegmentForIndex, getModifiableCoords, coordPathToFlatIndex
|
|
2
|
+
getCoords, getMidpointCoords, getRingSegments, getSegmentForIndex, getModifiableCoords, coordPathToFlatIndex
|
|
3
3
|
} from './geometryHelpers.js'
|
|
4
4
|
|
|
5
5
|
const POLYGON = () => ({ type: 'Polygon', coordinates: [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]] })
|
|
@@ -26,6 +26,13 @@ describe('geometryHelpers', () => {
|
|
|
26
26
|
expect(getRingSegments(null)).toEqual([])
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
+
test('getMidpointCoords finds the midpoint between each adjacent pair — no wrap for an open line, wrapping last→first for a closed ring', () => {
|
|
30
|
+
expect(getMidpointCoords(LINE())).toEqual([[5, 0], [10, 5]])
|
|
31
|
+
expect(getMidpointCoords(POLYGON())).toHaveLength(5) // closed ring: wraps, including the duplicate closing point
|
|
32
|
+
expect(getMidpointCoords({ type: 'Point', coordinates: [0, 0] })).toEqual([])
|
|
33
|
+
expect(getMidpointCoords(null)).toEqual([])
|
|
34
|
+
})
|
|
35
|
+
|
|
29
36
|
test('getSegmentForIndex finds the owning segment or returns null', () => {
|
|
30
37
|
const segments = getRingSegments(POLYGON())
|
|
31
38
|
expect(getSegmentForIndex(segments, 2)).toEqual({ segment: segments[0], localIdx: 2 })
|