@defra/interactive-map 0.0.36-alpha → 0.0.38-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/templates/draw-tools.njk +4 -3
- 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 +32 -0
- package/docs/examples/draw-tools.mdx +0 -4
- package/docs/plugins/draw.md +482 -0
- package/docs/plugins.md +2 -6
- package/govuk-prototype-kit.config.json +3 -2
- package/package.json +2 -4
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/events.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/css/index.css +1 -97
- package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +13 -0
- package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -0
- package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
- package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
- package/plugins/draw/dist/esm/index.js +1 -0
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
- package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
- package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
- package/plugins/draw/dist/umd/index.js +2 -0
- package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
- package/plugins/draw/src/DrawInit.jsx +89 -0
- package/plugins/draw/src/DrawInit.test.jsx +184 -0
- package/plugins/draw/src/adapterEvents.js +127 -0
- package/plugins/draw/src/adapterEvents.test.js +20 -0
- package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
- package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
- package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
- package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
- package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
- package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
- package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
- package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
- package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
- package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
- package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
- package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
- package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
- package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
- package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
- package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
- package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
- package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
- package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
- package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
- package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
- package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
- package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
- package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
- package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
- package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
- package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
- package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
- package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
- package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
- package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
- package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
- package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
- package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
- package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
- package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
- package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
- package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
- package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
- package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
- package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
- package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
- package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
- package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
- package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
- package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
- package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
- package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
- package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
- package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
- package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
- package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
- package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
- package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
- package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
- package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
- package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
- package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
- package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
- package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
- package/plugins/draw/src/api/addFeature.js +22 -0
- package/plugins/draw/src/api/addFeature.test.js +41 -0
- package/plugins/draw/src/api/deleteFeature.js +11 -0
- package/plugins/draw/src/api/deleteFeature.test.js +24 -0
- package/plugins/draw/src/api/editFeature.js +54 -0
- package/plugins/draw/src/api/editFeature.test.js +134 -0
- package/plugins/draw/src/api/merge.js +30 -0
- package/plugins/draw/src/api/merge.test.js +57 -0
- package/plugins/draw/src/api/newLine.js +41 -0
- package/plugins/draw/src/api/newLine.test.js +93 -0
- package/plugins/draw/src/api/newPolygon.js +41 -0
- package/plugins/draw/src/api/newPolygon.test.js +93 -0
- package/plugins/draw/src/api/split.js +117 -0
- package/plugins/draw/src/api/split.test.js +204 -0
- package/plugins/draw/src/defaults.js +52 -0
- package/plugins/draw/src/defaults.test.js +18 -0
- package/plugins/draw/src/draw.scss +43 -0
- package/plugins/draw/src/events.js +237 -0
- package/plugins/draw/src/events.test.js +369 -0
- package/plugins/draw/src/index.js +10 -0
- package/plugins/draw/src/index.test.js +24 -0
- package/plugins/draw/src/manifest.js +170 -0
- package/plugins/draw/src/manifest.test.js +147 -0
- package/plugins/draw/src/reducer.js +69 -0
- package/plugins/draw/src/reducer.test.js +85 -0
- package/plugins/draw/src/utils/debounce.js +16 -0
- package/plugins/draw/src/utils/debounce.test.js +40 -0
- package/plugins/draw/src/utils/eventBus.js +36 -0
- package/plugins/draw/src/utils/eventBus.test.js +47 -0
- package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
- package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
- package/plugins/draw/src/utils/getValueForStyle.js +33 -0
- package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
- package/plugins/draw/src/utils/resolveColors.js +39 -0
- package/plugins/draw/src/utils/resolveColors.test.js +34 -0
- package/plugins/draw/src/utils/spatial.js +243 -0
- package/plugins/draw/src/utils/spatial.test.js +243 -0
- package/plugins/draw/src/utils/touchTarget.js +95 -0
- package/plugins/draw/src/utils/touchTarget.test.js +107 -0
- package/plugins/draw/src/utils/undoStack.js +31 -0
- package/plugins/draw/src/utils/undoStack.test.js +49 -0
- package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
- package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
- package/plugins/draw/src/validation/liveStroke.js +85 -0
- package/plugins/draw/src/validation/liveStroke.test.js +150 -0
- package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
- package/plugins/draw/src/validation/rules.js +186 -0
- package/plugins/draw/src/validation/rules.test.js +120 -0
- package/plugins/draw/src/validation/validateGeometry.js +119 -0
- package/plugins/draw/src/validation/validateGeometry.test.js +200 -0
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/src/hooks/useInteractionHandlers.js +1 -1
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
- package/plugins/interact/src/utils/buildStylesMap.js +1 -0
- package/plugins/search/dist/css/index.css +1 -317
- package/rollup.esm.mjs +35 -28
- package/sonar-project.properties +2 -2
- package/src/App/components/Attributions/Attributions.module.scss +1 -0
- package/src/App/components/Hints/Hints.module.scss +5 -4
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
- package/src/App/components/MoveControl/MoveControl.jsx +73 -9
- package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
- package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
- package/src/App/hooks/useHintsAPI.js +67 -0
- package/src/App/hooks/useHintsAPI.test.js +145 -0
- package/src/App/renderer/PluginInits.jsx +4 -0
- package/src/App/renderer/PluginInits.test.jsx +7 -1
- package/src/InteractiveMap/InteractiveMap.js +20 -0
- package/src/InteractiveMap/InteractiveMap.test.js +7 -0
- package/src/config/events.js +4 -0
- package/src/scss/settings/_dimensions.scss +3 -0
- package/src/services/announcer.js +38 -3
- package/src/services/announcer.test.js +133 -1
- package/src/services/hints.js +3 -0
- package/src/services/hints.test.js +19 -0
- package/src/types.js +12 -0
- package/src/utils/detectInterfaceType.js +5 -1
- package/src/utils/isMac.js +15 -0
- package/src/utils/isMac.test.js +37 -0
- package/webpack.umd.mjs +1 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { deriveModifyOp, buildModifyCondition, createModifyInteraction } from './modifyInteraction.js'
|
|
2
|
+
import { createFakeMap, polygonFeature } from '../__helpers__/harness.js'
|
|
3
|
+
|
|
4
|
+
const RING = [[0, 0], [10, 0], [10, 10], [0, 0]]
|
|
5
|
+
|
|
6
|
+
describe('deriveModifyOp', () => {
|
|
7
|
+
const prev = [[0, 0], [10, 0], [10, 10]]
|
|
8
|
+
|
|
9
|
+
test('a moved vertex becomes a move_vertex op with its previous coordinate', () => {
|
|
10
|
+
expect(deriveModifyOp(prev, [[0, 0], [12, 2], [10, 10]]))
|
|
11
|
+
.toEqual({ type: 'move_vertex', vertexIndex: 1, previousCoord: [10, 0] })
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('a grown array becomes an insert_vertex op at the first differing index', () => {
|
|
15
|
+
expect(deriveModifyOp(prev, [[0, 0], [5, 5], [10, 0], [10, 10]]))
|
|
16
|
+
.toEqual({ type: 'insert_vertex', vertexIndex: 1 })
|
|
17
|
+
expect(deriveModifyOp(prev, [...prev, [9, 9]]))
|
|
18
|
+
.toEqual({ type: 'insert_vertex', vertexIndex: 3 })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('no coordinate change or a shrunk array yields no op', () => {
|
|
22
|
+
expect(deriveModifyOp(prev, prev.map(c => [...c]))).toBeNull()
|
|
23
|
+
expect(deriveModifyOp(prev, prev.slice(0, 2))).toBeNull()
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('buildModifyCondition', () => {
|
|
28
|
+
const event = (x, y) => ({ originalEvent: { clientX: x, clientY: y } })
|
|
29
|
+
const conditionFor = (interfaceType) => buildModifyCondition({
|
|
30
|
+
map: createFakeMap(),
|
|
31
|
+
getState: () => ({ interfaceType, vertices: [[10, 10]], midpoints: [[50, 50]] })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('activates on a vertex or midpoint handle but not on empty map', () => {
|
|
35
|
+
const condition = conditionFor('mouse')
|
|
36
|
+
expect(condition(event(11, 11))).toBe(true)
|
|
37
|
+
expect(condition(event(51, 49))).toBe(true)
|
|
38
|
+
expect(condition(event(200, 200))).toBe(false)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('never activates for the touch interface', () => {
|
|
42
|
+
expect(conditionFor('touch')(event(11, 11))).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('createModifyInteraction', () => {
|
|
47
|
+
const setup = (interfaceType = 'mouse') => {
|
|
48
|
+
const map = createFakeMap()
|
|
49
|
+
const state = { interfaceType, vertices: [[0, 0], [10, 0]], midpoints: [] }
|
|
50
|
+
const onModifyEnd = jest.fn()
|
|
51
|
+
const modify = createModifyInteraction({ map, olFeature: polygonFeature(RING), getState: () => state, onModifyEnd })
|
|
52
|
+
return { map, state, onModifyEnd, modify, interaction: map.interactions[0] }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
test('reports a completed drag with the vertices snapshotted at drag start', () => {
|
|
56
|
+
const { state, onModifyEnd, interaction } = setup()
|
|
57
|
+
interaction.dispatchEvent({ type: 'modifystart' })
|
|
58
|
+
state.vertices = [[5, 5], [10, 0]] // drag mutates state via geometry change
|
|
59
|
+
interaction.dispatchEvent({ type: 'modifyend' })
|
|
60
|
+
expect(onModifyEnd).toHaveBeenCalledWith([[0, 0], [10, 0]])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('touch drags are ignored entirely', () => {
|
|
64
|
+
const { onModifyEnd, interaction } = setup('touch')
|
|
65
|
+
interaction.dispatchEvent({ type: 'modifystart' })
|
|
66
|
+
interaction.dispatchEvent({ type: 'modifyend' })
|
|
67
|
+
expect(onModifyEnd).not.toHaveBeenCalled()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('vertex handles are rendered by the vertex layer, not by Modify itself', () => {
|
|
71
|
+
const { interaction } = setup()
|
|
72
|
+
expect(interaction.getOverlay().getStyleFunction()()).toEqual([])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('destroy removes the interaction from the map', () => {
|
|
76
|
+
const { map, modify, interaction } = setup()
|
|
77
|
+
modify.destroy()
|
|
78
|
+
expect(map.removeInteraction).toHaveBeenCalledWith(interaction)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { nudgeCoord } from '../utils/olCoords.js'
|
|
2
|
+
import { moveVertex, insertAtMidpoint } from './vertexOps.js'
|
|
3
|
+
import { KEYBOARD } from '../defaults.js'
|
|
4
|
+
|
|
5
|
+
const SNAP_PROGRESS_RATIO = 0.5
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns snappedCoord, but escapes snap if it prevents sufficient progress in the
|
|
9
|
+
* nudge direction. Covers vertex-stuck (snap holds position) and edge-hugging
|
|
10
|
+
* (vertex slides along edge).
|
|
11
|
+
*/
|
|
12
|
+
export const resolveSnappedCoord = (snap, map, current, nudgedCoord, snappedCoord, dx, dy) => {
|
|
13
|
+
if (!snap) {
|
|
14
|
+
return snappedCoord
|
|
15
|
+
}
|
|
16
|
+
const nudgeVec = [nudgedCoord[0] - current[0], nudgedCoord[1] - current[1]]
|
|
17
|
+
const actualVec = [snappedCoord[0] - current[0], snappedCoord[1] - current[1]]
|
|
18
|
+
const nudgeLenSq = nudgeVec[0] ** 2 + nudgeVec[1] ** 2
|
|
19
|
+
const dot = actualVec[0] * nudgeVec[0] + actualVec[1] * nudgeVec[1]
|
|
20
|
+
if (nudgeLenSq > 0 && dot / nudgeLenSq < SNAP_PROGRESS_RATIO) {
|
|
21
|
+
const escapePx = snap.snapRadius + 1
|
|
22
|
+
return nudgeCoord(map, current, dx === 0 ? 0 : Math.sign(dx) * escapePx, dy === 0 ? 0 : Math.sign(dy) * escapePx)
|
|
23
|
+
}
|
|
24
|
+
return snappedCoord
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Arrow-key vertex nudging for edit mode.
|
|
29
|
+
*
|
|
30
|
+
* `keyMove` tracks the coordinate at the start of a nudge sequence so a single
|
|
31
|
+
* move_vertex undo op can be pushed on keyup (see keyboardHandler).
|
|
32
|
+
*
|
|
33
|
+
* @returns {{ nudge: (e: KeyboardEvent) => void, keyMove: { start, index }, nudgeByDelta: (dx: number, dy: number, isLargeStep: boolean) => void }}
|
|
34
|
+
*/
|
|
35
|
+
export const wireNudge = ({ map, snap, getState, setState, onInserted, onVertexMoved }) => {
|
|
36
|
+
const keyMove = { start: null, index: null }
|
|
37
|
+
|
|
38
|
+
// Shared core: moves the selected vertex by an already pixel-scaled delta,
|
|
39
|
+
// applying snap. Returns the previous coordinate (for undo) and vertex index,
|
|
40
|
+
// or null if there's no valid vertex selection to move.
|
|
41
|
+
const moveSelectedVertexBy = (dx, dy) => {
|
|
42
|
+
const { selectedVertexIndex, vertices, olFeature } = getState()
|
|
43
|
+
if (!olFeature || selectedVertexIndex < 0 || !vertices[selectedVertexIndex]) {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
const current = vertices[selectedVertexIndex]
|
|
47
|
+
const nudgedCoord = nudgeCoord(map, current, dx, dy)
|
|
48
|
+
const snappedCoord = snap ? snap.apply(nudgedCoord) : nudgedCoord
|
|
49
|
+
snap?.hideIndicator()
|
|
50
|
+
const newCoord = resolveSnappedCoord(snap, map, current, nudgedCoord, snappedCoord, dx, dy)
|
|
51
|
+
moveVertex(olFeature, selectedVertexIndex, newCoord)
|
|
52
|
+
setState({ vertices: vertices.map((c, i) => i === selectedVertexIndex ? newCoord : c) })
|
|
53
|
+
return { previousCoord: current, vertexIndex: selectedVertexIndex }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Insert the midpoint as a vertex then move it — midpoints stay as midpoints until actually moved.
|
|
57
|
+
const nudgeMidpoint = (olFeature, midpoints, selectedVertexIndex, vertices, dx, dy) => {
|
|
58
|
+
const result = insertAtMidpoint(olFeature, midpoints, selectedVertexIndex, vertices.length)
|
|
59
|
+
if (!result) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
onInserted({ insertedIndex: result.insertedIndex }) // pushes insert_vertex undo + syncGeom
|
|
63
|
+
const updatedVertices = getState().vertices
|
|
64
|
+
const insertedCoord = updatedVertices[result.insertedIndex]
|
|
65
|
+
if (!insertedCoord) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
keyMove.start = [...insertedCoord]
|
|
69
|
+
keyMove.index = result.insertedIndex
|
|
70
|
+
const nudgedCoord = nudgeCoord(map, insertedCoord, dx, dy)
|
|
71
|
+
const movedCoord = snap ? snap.apply(nudgedCoord) : nudgedCoord
|
|
72
|
+
moveVertex(olFeature, result.insertedIndex, movedCoord)
|
|
73
|
+
setState({
|
|
74
|
+
selectedVertexIndex: result.insertedIndex,
|
|
75
|
+
selectedVertexType: 'vertex',
|
|
76
|
+
vertices: updatedVertices.map((c, i) => i === result.insertedIndex ? movedCoord : c)
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const nudge = (e) => {
|
|
81
|
+
const { selectedVertexIndex, selectedVertexType, vertices, midpoints, olFeature } = getState()
|
|
82
|
+
if (!olFeature) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
const step = e.shiftKey ? KEYBOARD.nudgeAmount : KEYBOARD.stepAmount
|
|
86
|
+
const offsets = { ArrowUp: [0, -step], ArrowDown: [0, step], ArrowLeft: [-step, 0], ArrowRight: [step, 0] }
|
|
87
|
+
const [dx, dy] = offsets[e.key]
|
|
88
|
+
if (selectedVertexType === 'midpoint') {
|
|
89
|
+
nudgeMidpoint(olFeature, midpoints, selectedVertexIndex, vertices, dx, dy)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
if (selectedVertexIndex < 0 || !vertices[selectedVertexIndex]) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
if (!keyMove.start) {
|
|
96
|
+
keyMove.start = [...vertices[selectedVertexIndex]]
|
|
97
|
+
keyMove.index = selectedVertexIndex
|
|
98
|
+
}
|
|
99
|
+
moveSelectedVertexBy(dx, dy)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Explicit-delta counterpart to nudge(e) — the entry point for MoveControl's
|
|
103
|
+
// D-pad (see mapProvider.activeMoveTarget in events.js). Unlike nudge(e)'s
|
|
104
|
+
// held-key sequencing (undo pushed on keyup via keyMove, so repeated keydowns
|
|
105
|
+
// while held batch into one undo step), each call here is one complete,
|
|
106
|
+
// undoable action — a button click has no "held" state to batch. Midpoints
|
|
107
|
+
// aren't handled here: MoveControl only claims the D-pad once a real vertex is
|
|
108
|
+
// selected (see buildVertexMoveTarget in events.js).
|
|
109
|
+
const nudgeByDelta = (dx, dy, isLargeStep) => {
|
|
110
|
+
const step = isLargeStep ? KEYBOARD.stepAmount : KEYBOARD.nudgeAmount
|
|
111
|
+
const result = moveSelectedVertexBy(dx * step, dy * step)
|
|
112
|
+
if (result) {
|
|
113
|
+
onVertexMoved({ vertexIndex: result.vertexIndex, previousCoord: result.previousCoord })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { nudge, keyMove, nudgeByDelta }
|
|
118
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { wireNudge, resolveSnappedCoord } from './nudge.js'
|
|
2
|
+
import { getCoords } from '../utils/geometryHelpers.js'
|
|
3
|
+
import { createFakeMap, polygonFeature } from '../__helpers__/harness.js'
|
|
4
|
+
|
|
5
|
+
const RING = [[0, 0], [10, 0], [10, 10], [0, 0]]
|
|
6
|
+
|
|
7
|
+
const setup = ({ snap = null } = {}) => {
|
|
8
|
+
const map = createFakeMap()
|
|
9
|
+
const olFeature = polygonFeature(RING)
|
|
10
|
+
const state = {
|
|
11
|
+
olFeature,
|
|
12
|
+
selectedVertexIndex: -1,
|
|
13
|
+
selectedVertexType: null,
|
|
14
|
+
vertices: [[0, 0], [10, 0], [10, 10]],
|
|
15
|
+
midpoints: [[5, 0], [10, 5], [5, 5]]
|
|
16
|
+
}
|
|
17
|
+
const setState = jest.fn((updates) => Object.assign(state, updates))
|
|
18
|
+
const onInserted = jest.fn(() => {
|
|
19
|
+
// EditMode's onInserted runs syncGeom, refreshing vertices from the geometry
|
|
20
|
+
state.vertices = getCoords({ type: 'Polygon', coordinates: olFeature.getGeometry().getCoordinates() })
|
|
21
|
+
})
|
|
22
|
+
const onVertexMoved = jest.fn()
|
|
23
|
+
const { nudge, keyMove, nudgeByDelta } = wireNudge({ map, snap, getState: () => state, setState, onInserted, onVertexMoved })
|
|
24
|
+
return { olFeature, state, setState, onInserted, onVertexMoved, nudge, keyMove, nudgeByDelta }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ring = (olFeature) => olFeature.getGeometry().getCoordinates()[0]
|
|
28
|
+
|
|
29
|
+
describe('nudging a vertex', () => {
|
|
30
|
+
test('a plain arrow moves the selected vertex by the step amount, shift by the fine amount', () => {
|
|
31
|
+
const { state, nudge, olFeature } = setup()
|
|
32
|
+
Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
|
|
33
|
+
nudge({ key: 'ArrowRight', shiftKey: false })
|
|
34
|
+
expect(ring(olFeature)[1]).toEqual([15, 0])
|
|
35
|
+
nudge({ key: 'ArrowDown', shiftKey: true })
|
|
36
|
+
expect(ring(olFeature)[1]).toEqual([15, 1])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('the start coordinate is captured once for the whole nudge sequence (single undo op)', () => {
|
|
40
|
+
const { state, nudge, keyMove } = setup()
|
|
41
|
+
Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
|
|
42
|
+
nudge({ key: 'ArrowRight' })
|
|
43
|
+
nudge({ key: 'ArrowRight' })
|
|
44
|
+
expect(keyMove).toEqual({ start: [10, 0], index: 1 })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('does nothing without a feature or a valid selection', () => {
|
|
48
|
+
const { state, nudge, setState } = setup()
|
|
49
|
+
nudge({ key: 'ArrowRight' }) // nothing selected
|
|
50
|
+
state.olFeature = null
|
|
51
|
+
nudge({ key: 'ArrowRight' })
|
|
52
|
+
expect(setState).not.toHaveBeenCalled()
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('nudgeByDelta (MoveControl D-pad)', () => {
|
|
57
|
+
test('moves the selected vertex by an explicit direction, scaled by isLargeStep, and reports the move for undo', () => {
|
|
58
|
+
const { state, nudgeByDelta, olFeature, onVertexMoved } = setup()
|
|
59
|
+
Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
|
|
60
|
+
nudgeByDelta(1, 0, true)
|
|
61
|
+
expect(ring(olFeature)[1]).toEqual([15, 0])
|
|
62
|
+
expect(onVertexMoved).toHaveBeenCalledWith({ vertexIndex: 1, previousCoord: [10, 0] })
|
|
63
|
+
|
|
64
|
+
onVertexMoved.mockClear()
|
|
65
|
+
nudgeByDelta(0, 1, false)
|
|
66
|
+
expect(ring(olFeature)[1]).toEqual([15, 1])
|
|
67
|
+
expect(onVertexMoved).toHaveBeenCalledWith({ vertexIndex: 1, previousCoord: [15, 0] })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('each call is its own undo-able action, unlike nudge(e)\'s held-key batching', () => {
|
|
71
|
+
const { state, nudgeByDelta, onVertexMoved } = setup()
|
|
72
|
+
Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
|
|
73
|
+
nudgeByDelta(1, 0, true)
|
|
74
|
+
nudgeByDelta(1, 0, true)
|
|
75
|
+
expect(onVertexMoved).toHaveBeenCalledTimes(2)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('does nothing without a feature or a valid vertex selection, and never touches midpoints', () => {
|
|
79
|
+
const { state, nudgeByDelta, onVertexMoved } = setup()
|
|
80
|
+
nudgeByDelta(1, 0, true) // nothing selected
|
|
81
|
+
Object.assign(state, { selectedVertexIndex: 4, selectedVertexType: 'midpoint' })
|
|
82
|
+
nudgeByDelta(1, 0, true) // midpoint selected — MoveControl only claims a real vertex
|
|
83
|
+
state.olFeature = null
|
|
84
|
+
Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
|
|
85
|
+
nudgeByDelta(1, 0, true)
|
|
86
|
+
expect(onVertexMoved).not.toHaveBeenCalled()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('nudging a midpoint', () => {
|
|
91
|
+
test('inserts the midpoint as a vertex, then moves and selects it', () => {
|
|
92
|
+
const { state, nudge, onInserted, olFeature, setState } = setup()
|
|
93
|
+
Object.assign(state, { selectedVertexIndex: 4, selectedVertexType: 'midpoint' }) // midpoint [10, 5]
|
|
94
|
+
nudge({ key: 'ArrowRight', shiftKey: false })
|
|
95
|
+
expect(onInserted).toHaveBeenCalledWith({ insertedIndex: 2 })
|
|
96
|
+
expect(ring(olFeature)[2]).toEqual([15, 5])
|
|
97
|
+
expect(setState).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
98
|
+
selectedVertexIndex: 2, selectedVertexType: 'vertex'
|
|
99
|
+
}))
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('an invalid midpoint index is a no-op', () => {
|
|
103
|
+
const { state, nudge, onInserted } = setup()
|
|
104
|
+
Object.assign(state, { selectedVertexIndex: 99, selectedVertexType: 'midpoint' })
|
|
105
|
+
nudge({ key: 'ArrowRight' })
|
|
106
|
+
expect(onInserted).not.toHaveBeenCalled()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('snaps the inserted-then-moved coordinate when a snap manager is active', () => {
|
|
110
|
+
const snap = { apply: jest.fn((c) => [c[0] + 2, c[1]]), hideIndicator: jest.fn(), snapRadius: 12 }
|
|
111
|
+
const { state, nudge, olFeature } = setup({ snap })
|
|
112
|
+
Object.assign(state, { selectedVertexIndex: 4, selectedVertexType: 'midpoint' }) // midpoint [10, 5]
|
|
113
|
+
nudge({ key: 'ArrowRight', shiftKey: false })
|
|
114
|
+
expect(snap.apply).toHaveBeenCalled()
|
|
115
|
+
expect(ring(olFeature)[2]).toEqual([17, 5]) // inserted [10,5] nudged +5 -> [15,5], snapped +2 -> [17,5]
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('stops safely if the inserted vertex is not reflected in state', () => {
|
|
119
|
+
const { state, nudge, onInserted, setState } = setup()
|
|
120
|
+
onInserted.mockImplementation(() => {}) // consumer failed to sync vertices
|
|
121
|
+
Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'midpoint', vertices: [] })
|
|
122
|
+
nudge({ key: 'ArrowRight' }) // insert succeeds but the inserted coord is missing from state
|
|
123
|
+
expect(onInserted).toHaveBeenCalled()
|
|
124
|
+
expect(setState).not.toHaveBeenCalled()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('snapping while nudging', () => {
|
|
129
|
+
const snapReturning = (impl) => ({ apply: jest.fn(impl), hideIndicator: jest.fn(), snapRadius: 12 })
|
|
130
|
+
|
|
131
|
+
test('the snapped coordinate is used when it makes progress in the nudge direction', () => {
|
|
132
|
+
const snap = snapReturning((c) => [c[0] + 1, c[1]])
|
|
133
|
+
const { state, nudge, olFeature } = setup({ snap })
|
|
134
|
+
Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
|
|
135
|
+
nudge({ key: 'ArrowRight' })
|
|
136
|
+
expect(ring(olFeature)[1]).toEqual([16, 0])
|
|
137
|
+
expect(snap.hideIndicator).toHaveBeenCalled()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('escapes the snap radius when snapping blocks the nudge', () => {
|
|
141
|
+
const snap = snapReturning(() => [10, 0]) // snap holds the vertex in place
|
|
142
|
+
const { state, nudge, olFeature } = setup({ snap })
|
|
143
|
+
Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
|
|
144
|
+
nudge({ key: 'ArrowRight' })
|
|
145
|
+
expect(ring(olFeature)[1]).toEqual([23, 0]) // snapRadius + 1 past the original position
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('resolveSnappedCoord', () => {
|
|
150
|
+
test('returns the snapped coordinate without a snap manager or when progress is sufficient', () => {
|
|
151
|
+
const map = createFakeMap()
|
|
152
|
+
expect(resolveSnappedCoord(null, map, [0, 0], [5, 0], [4, 0], 5, 0)).toEqual([4, 0])
|
|
153
|
+
expect(resolveSnappedCoord({ snapRadius: 12 }, map, [0, 0], [5, 0], [4, 0], 5, 0)).toEqual([4, 0])
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('escapes along both axes of a diagonal nudge when blocked', () => {
|
|
157
|
+
const map = createFakeMap()
|
|
158
|
+
expect(resolveSnappedCoord({ snapRadius: 12 }, map, [0, 0], [5, 5], [0, 0], 5, 5)).toEqual([13, 13])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('escapes only the vertical axis when the horizontal delta is zero', () => {
|
|
162
|
+
const map = createFakeMap()
|
|
163
|
+
expect(resolveSnappedCoord({ snapRadius: 12 }, map, [0, 0], [0, 5], [0, 0], 0, 5)).toEqual([0, 13])
|
|
164
|
+
})
|
|
165
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { findNearest } from './vertexHitTest.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pointer/mouse selection handlers for edit mode, plus the global
|
|
5
|
+
* delete-vertex button listener.
|
|
6
|
+
*
|
|
7
|
+
* Interface-type flips here mutate state directly (not via setState) because
|
|
8
|
+
* switching device must not touch the current selection.
|
|
9
|
+
*
|
|
10
|
+
* @returns {{ destroy: () => void }}
|
|
11
|
+
*/
|
|
12
|
+
export const createPointerHandlers = ({ map, container, getState, setState, touchHandler, deleteVertexButtonId, onDeleteVertex }) => {
|
|
13
|
+
const hitAt = (e) => {
|
|
14
|
+
const { vertices, midpoints } = getState()
|
|
15
|
+
const olPixel = map.getEventPixel(e)
|
|
16
|
+
return findNearest(map, vertices, midpoints, { x: olPixel[0], y: olPixel[1] })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const onPointerdown = (e) => {
|
|
20
|
+
const state = getState()
|
|
21
|
+
if (e.pointerType === 'touch') {
|
|
22
|
+
state.interfaceType = 'touch'
|
|
23
|
+
touchHandler.updateTargetPosition()
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
state.interfaceType = 'mouse'
|
|
27
|
+
const hit = hitAt(e)
|
|
28
|
+
if (hit?.type === 'vertex') {
|
|
29
|
+
setState({ selectedVertexIndex: hit.index, selectedVertexType: 'vertex' })
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// click fires after OL Modify finishes, so state.vertices reflects any insertions/moves
|
|
34
|
+
const onContainerClick = (e) => {
|
|
35
|
+
if (getState().interfaceType === 'touch') {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
const hit = hitAt(e)
|
|
39
|
+
if (hit?.type === 'vertex') {
|
|
40
|
+
setState({ selectedVertexIndex: hit.index, selectedVertexType: 'vertex' })
|
|
41
|
+
} else if (hit?.type === 'midpoint') {
|
|
42
|
+
// modifyend already selected the inserted vertex — nothing to do here
|
|
43
|
+
} else {
|
|
44
|
+
setState({ selectedVertexIndex: -1, selectedVertexType: null })
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Switch to pointer mode and hide the touch target as soon as the mouse moves
|
|
49
|
+
const onPointerMove = (e) => {
|
|
50
|
+
const state = getState()
|
|
51
|
+
if (e.pointerType !== 'mouse' || state.interfaceType === 'mouse') {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
state.interfaceType = 'mouse'
|
|
55
|
+
touchHandler.hide()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const onButtonClick = (e) => {
|
|
59
|
+
if (deleteVertexButtonId && e.target.closest(`#${deleteVertexButtonId}`)) {
|
|
60
|
+
onDeleteVertex()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
container.addEventListener('pointerdown', onPointerdown)
|
|
65
|
+
container.addEventListener('pointerenter', onPointerMove)
|
|
66
|
+
container.addEventListener('pointermove', onPointerMove)
|
|
67
|
+
container.addEventListener('click', onContainerClick)
|
|
68
|
+
globalThis.addEventListener('click', onButtonClick)
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
destroy () {
|
|
72
|
+
container.removeEventListener('pointerdown', onPointerdown)
|
|
73
|
+
container.removeEventListener('pointerenter', onPointerMove)
|
|
74
|
+
container.removeEventListener('pointermove', onPointerMove)
|
|
75
|
+
container.removeEventListener('click', onContainerClick)
|
|
76
|
+
globalThis.removeEventListener('click', onButtonClick)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createPointerHandlers } from './pointerHandlers.js'
|
|
2
|
+
import { createFakeMap, createContainer, domEvent } from '../__helpers__/harness.js'
|
|
3
|
+
|
|
4
|
+
const setup = (interfaceType = 'mouse') => {
|
|
5
|
+
const map = createFakeMap()
|
|
6
|
+
const container = createContainer()
|
|
7
|
+
const button = document.createElement('button')
|
|
8
|
+
button.id = 'delete-vertex'
|
|
9
|
+
document.body.appendChild(button)
|
|
10
|
+
const state = { interfaceType, vertices: [[10, 10]], midpoints: [[50, 50]] }
|
|
11
|
+
const setState = jest.fn((updates) => Object.assign(state, updates))
|
|
12
|
+
const touchHandler = { updateTargetPosition: jest.fn(), hide: jest.fn() }
|
|
13
|
+
const onDeleteVertex = jest.fn()
|
|
14
|
+
const handlers = createPointerHandlers({
|
|
15
|
+
map, container, getState: () => state, setState, touchHandler, deleteVertexButtonId: 'delete-vertex', onDeleteVertex
|
|
16
|
+
})
|
|
17
|
+
return { container, button, state, setState, touchHandler, onDeleteVertex, handlers }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterEach(() => { document.body.innerHTML = '' })
|
|
21
|
+
|
|
22
|
+
const pointer = (type, pointerType, x = 0, y = 0) => domEvent(type, { pointerType, clientX: x, clientY: y })
|
|
23
|
+
|
|
24
|
+
test('touch pointerdown switches to touch interface and repositions the touch target', () => {
|
|
25
|
+
const { container, state, touchHandler, setState } = setup()
|
|
26
|
+
container.dispatchEvent(pointer('pointerdown', 'touch'))
|
|
27
|
+
expect(state.interfaceType).toBe('touch')
|
|
28
|
+
expect(touchHandler.updateTargetPosition).toHaveBeenCalled()
|
|
29
|
+
expect(setState).not.toHaveBeenCalled() // device switch must not touch the selection
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('mouse pointerdown selects a vertex under the pointer, but nothing elsewhere', () => {
|
|
33
|
+
const { container, state, setState } = setup('touch')
|
|
34
|
+
container.dispatchEvent(pointer('pointerdown', 'mouse', 11, 11))
|
|
35
|
+
expect(state.interfaceType).toBe('mouse')
|
|
36
|
+
expect(setState).toHaveBeenCalledWith({ selectedVertexIndex: 0, selectedVertexType: 'vertex' })
|
|
37
|
+
setState.mockClear()
|
|
38
|
+
container.dispatchEvent(pointer('pointerdown', 'mouse', 200, 200))
|
|
39
|
+
expect(setState).not.toHaveBeenCalled()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('click selects a vertex, deselects on empty map, leaves midpoints to Modify, and ignores touch', () => {
|
|
43
|
+
const { container, state, setState } = setup()
|
|
44
|
+
container.dispatchEvent(pointer('click', 'mouse', 11, 11))
|
|
45
|
+
expect(setState).toHaveBeenCalledWith({ selectedVertexIndex: 0, selectedVertexType: 'vertex' })
|
|
46
|
+
container.dispatchEvent(pointer('click', 'mouse', 51, 49))
|
|
47
|
+
expect(setState).toHaveBeenCalledTimes(1) // midpoint click: modifyend already handled selection
|
|
48
|
+
container.dispatchEvent(pointer('click', 'mouse', 200, 200))
|
|
49
|
+
expect(setState).toHaveBeenLastCalledWith({ selectedVertexIndex: -1, selectedVertexType: null })
|
|
50
|
+
|
|
51
|
+
state.interfaceType = 'touch'
|
|
52
|
+
setState.mockClear()
|
|
53
|
+
container.dispatchEvent(pointer('click', 'mouse', 11, 11))
|
|
54
|
+
expect(setState).not.toHaveBeenCalled()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('mouse movement switches the interface back to mouse and hides the touch target once', () => {
|
|
58
|
+
const { container, state, touchHandler } = setup('touch')
|
|
59
|
+
container.dispatchEvent(pointer('pointermove', 'pen'))
|
|
60
|
+
expect(state.interfaceType).toBe('touch')
|
|
61
|
+
container.dispatchEvent(pointer('pointermove', 'mouse'))
|
|
62
|
+
expect(state.interfaceType).toBe('mouse')
|
|
63
|
+
container.dispatchEvent(pointer('pointermove', 'mouse'))
|
|
64
|
+
expect(touchHandler.hide).toHaveBeenCalledTimes(1)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('the delete-vertex button triggers deletion; other clicks and destroy() do not', () => {
|
|
68
|
+
const { button, container, onDeleteVertex, handlers } = setup()
|
|
69
|
+
button.dispatchEvent(domEvent('click', {}))
|
|
70
|
+
expect(onDeleteVertex).toHaveBeenCalledTimes(1)
|
|
71
|
+
document.body.dispatchEvent(domEvent('click', {}))
|
|
72
|
+
expect(onDeleteVertex).toHaveBeenCalledTimes(1)
|
|
73
|
+
|
|
74
|
+
handlers.destroy()
|
|
75
|
+
button.dispatchEvent(domEvent('click', {}))
|
|
76
|
+
container.dispatchEvent(pointer('pointerdown', 'touch'))
|
|
77
|
+
expect(onDeleteVertex).toHaveBeenCalledTimes(1)
|
|
78
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { getCoords, getMidpoints } from '../utils/geometryHelpers.js'
|
|
2
|
+
import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
|
|
3
|
+
|
|
4
|
+
// Deferred commit-level geometrychange emitter (feature + change phase + vertex index)
|
|
5
|
+
// consumed by the validation layer. Deferred a tick so that a rejection's revert runs
|
|
6
|
+
// after the current mutation's undo bookkeeping has settled.
|
|
7
|
+
const createGeometryValidationEmitter = (manager, store, olFeature) => (phase, vertexIndex) => {
|
|
8
|
+
if (!phase) { return }
|
|
9
|
+
setTimeout(() => {
|
|
10
|
+
manager.emit(ADAPTER_EVENTS.GEOMETRY_CHANGE, { feature: store.toGeoJSON(olFeature), phase, vertexIndex })
|
|
11
|
+
}, 0)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Reflect the current selection onto the handle layers and emit VERTEX_SELECTION.
|
|
15
|
+
const applySelectionChange = (state, { vertexLayer, midpointLayer, activeLayer, manager, hooks }) => {
|
|
16
|
+
vertexLayer.setSelected(state.selectedVertexType === 'vertex' ? state.selectedVertexIndex : -1)
|
|
17
|
+
midpointLayer.setSelected(
|
|
18
|
+
state.selectedVertexType === 'midpoint' ? state.selectedVertexIndex - state.vertices.length : -1
|
|
19
|
+
)
|
|
20
|
+
if (state.selectedVertexIndex < 0) {
|
|
21
|
+
hooks.onDeselect?.()
|
|
22
|
+
}
|
|
23
|
+
activeLayer.update(state)
|
|
24
|
+
manager.emit(ADAPTER_EVENTS.VERTEX_SELECTION, {
|
|
25
|
+
index: state.selectedVertexType === 'vertex' ? state.selectedVertexIndex : -1,
|
|
26
|
+
numVertices: state.vertices.length
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Mutable edit-mode state shared by the Modify interaction, pointer, touch and
|
|
32
|
+
* keyboard handlers, plus the setState/sync helpers that keep the handle
|
|
33
|
+
* layers, the active-selection overlay and the adapter events in step with it.
|
|
34
|
+
*
|
|
35
|
+
* Hooks (`onDeselect`, `onUpdate`) are bound late via `setHooks` because the
|
|
36
|
+
* touch handler both consumes this state and needs to react to its changes.
|
|
37
|
+
*
|
|
38
|
+
* @returns {{ state, getState, setState, syncGeom, updateLayersFromGeom, setHooks, destroy }}
|
|
39
|
+
*/
|
|
40
|
+
export const createSelectionState = ({ map, manager, store, olFeature, interfaceType, layers }) => {
|
|
41
|
+
const { vertexLayer, midpointLayer, activeLayer } = layers
|
|
42
|
+
|
|
43
|
+
const state = {
|
|
44
|
+
olFeature,
|
|
45
|
+
selectedVertexIndex: -1,
|
|
46
|
+
selectedVertexType: null,
|
|
47
|
+
vertices: [],
|
|
48
|
+
midpoints: [],
|
|
49
|
+
interfaceType: interfaceType ?? 'mouse'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const hooks = { onDeselect: null, onUpdate: null }
|
|
53
|
+
const setHooks = ({ onDeselect, onUpdate }) => Object.assign(hooks, { onDeselect, onUpdate })
|
|
54
|
+
|
|
55
|
+
const plainGeom = () => {
|
|
56
|
+
const geom = olFeature.getGeometry()
|
|
57
|
+
return { type: geom.getType(), coordinates: geom.getCoordinates() }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const applySelectionChangeLocal = () => applySelectionChange(state, { vertexLayer, midpointLayer, activeLayer, manager, hooks })
|
|
61
|
+
|
|
62
|
+
const applyVertexChange = () => {
|
|
63
|
+
const geom = plainGeom()
|
|
64
|
+
midpointLayer.update(geom)
|
|
65
|
+
vertexLayer.update(geom)
|
|
66
|
+
state.midpoints = midpointLayer.getCoords()
|
|
67
|
+
activeLayer.update(state)
|
|
68
|
+
hooks.onUpdate?.()
|
|
69
|
+
map.render()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const setState = (updates) => {
|
|
73
|
+
Object.assign(state, updates)
|
|
74
|
+
if (updates.selectedVertexIndex !== undefined) {
|
|
75
|
+
applySelectionChangeLocal()
|
|
76
|
+
}
|
|
77
|
+
if (updates.vertices !== undefined) {
|
|
78
|
+
applyVertexChange()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Lightweight per-frame update during drag — refreshes layers without emitting events
|
|
83
|
+
const updateLayersFromGeom = () => {
|
|
84
|
+
const geom = plainGeom()
|
|
85
|
+
state.vertices = getCoords(geom)
|
|
86
|
+
state.midpoints = getMidpoints(geom)
|
|
87
|
+
midpointLayer.update(geom)
|
|
88
|
+
vertexLayer.update(geom)
|
|
89
|
+
activeLayer.update(state)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const syncGeom = () => {
|
|
93
|
+
updateLayersFromGeom()
|
|
94
|
+
manager.emit(ADAPTER_EVENTS.VERTEX_CHANGE, { numVertices: state.vertices.length })
|
|
95
|
+
manager.emit(ADAPTER_EVENTS.UPDATE, store.toGeoJSON(olFeature))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const emitGeometryValidation = createGeometryValidationEmitter(manager, store, olFeature)
|
|
99
|
+
|
|
100
|
+
// Keep overlay layers in sync on every geometry change (e.g. during pointer drag)
|
|
101
|
+
const onGeometryChange = () => updateLayersFromGeom()
|
|
102
|
+
olFeature.getGeometry().on('change', onGeometryChange)
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
state,
|
|
106
|
+
getState: () => state,
|
|
107
|
+
setState,
|
|
108
|
+
syncGeom,
|
|
109
|
+
emitGeometryValidation,
|
|
110
|
+
updateLayersFromGeom,
|
|
111
|
+
setHooks,
|
|
112
|
+
destroy () {
|
|
113
|
+
olFeature.getGeometry().un('change', onGeometryChange)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|