@defra/interactive-map 0.0.37-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-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/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- 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,385 @@
|
|
|
1
|
+
import { createMidpointLayer } from './midpointLayer.js'
|
|
2
|
+
import { createVertexLayer } from './vertexLayer.js'
|
|
3
|
+
import { createActiveVertexLayer } from './activeVertexLayer.js'
|
|
4
|
+
import { createSelectionState } from './selectionState.js'
|
|
5
|
+
import { createModifyInteraction, deriveModifyOp } from './modifyInteraction.js'
|
|
6
|
+
import { createPointerHandlers } from './pointerHandlers.js'
|
|
7
|
+
import { createTouchHandler } from './touchHandler.js'
|
|
8
|
+
import { createKeyboardHandler } from './keyboardHandler.js'
|
|
9
|
+
import { deleteVertex, insertAtMidpoint } from './vertexOps.js'
|
|
10
|
+
import { applyUndo } from './undoOps.js'
|
|
11
|
+
import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
|
|
12
|
+
import { STYLES_CHANGED_EVENT } from '../core/internalEvents.js'
|
|
13
|
+
import { createLiveStroke } from '../../../validation/liveStroke.js'
|
|
14
|
+
|
|
15
|
+
const TOUCH_INTERFACE = 'touch'
|
|
16
|
+
const VERTEX_TYPE = 'vertex'
|
|
17
|
+
const MOVE_VERTEX = 'commit-move'
|
|
18
|
+
const INSERT_VERTEX = 'commit-insert'
|
|
19
|
+
const DELETE_VERTEX = 'commit-delete'
|
|
20
|
+
|
|
21
|
+
// Map an undo-op type onto the geometry-change `phase` consumed by validation.
|
|
22
|
+
const OP_PHASE = {
|
|
23
|
+
move_vertex: MOVE_VERTEX,
|
|
24
|
+
insert_vertex: INSERT_VERTEX,
|
|
25
|
+
delete_vertex: DELETE_VERTEX
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Undoing an op commits the inverse change (undo of a delete re-inserts, etc.),
|
|
29
|
+
// so its re-validation reports the inverse phase.
|
|
30
|
+
const UNDO_INVERSE_PHASE = {
|
|
31
|
+
move_vertex: MOVE_VERTEX,
|
|
32
|
+
insert_vertex: DELETE_VERTEX,
|
|
33
|
+
delete_vertex: INSERT_VERTEX
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Live invalid-stroke wiring: re-validate the displayed geometry on every geometry
|
|
37
|
+
// change (Modify drag, touch drag, keyboard nudge) via the shared controller —
|
|
38
|
+
// default rules synchronously, user callback throttled. All committed vertices are
|
|
39
|
+
// "placed", so the polygon count drops only OL's closing duplicate.
|
|
40
|
+
//
|
|
41
|
+
// The style is set ONCE to a function backed by `invalid`, rather than calling
|
|
42
|
+
// Feature#setStyle() on every flip: setStyle() fires a feature-level 'change'
|
|
43
|
+
// event, which OL's Modify interaction also listens for (to detect out-of-band
|
|
44
|
+
// edits and rebuild its internal drag/segment index) — guarded only while Modify
|
|
45
|
+
// itself is synchronously mutating the geometry. The built-in rules' flip stays
|
|
46
|
+
// inside that guarded window, but the user-callback flip is deliberately
|
|
47
|
+
// throttled to the next animation frame (liveStroke.js), landing outside it —
|
|
48
|
+
// so calling setStyle() there makes Modify treat its own flip as an external
|
|
49
|
+
// edit and rebuild mid-drag, killing the in-progress drag. A style function +
|
|
50
|
+
// map.render() repaints without ever touching the feature, so Modify's listener
|
|
51
|
+
// never fires from our own bookkeeping.
|
|
52
|
+
const wireLiveStroke = ({ map, manager, olFeature }) => {
|
|
53
|
+
let invalid = false
|
|
54
|
+
olFeature.setStyle(() => (invalid ? manager.styles.editFeatureStyleInvalid : manager.styles.editFeatureStyle))
|
|
55
|
+
const liveStroke = createLiveStroke({
|
|
56
|
+
onChange: (next, reason) => {
|
|
57
|
+
invalid = next
|
|
58
|
+
map.render()
|
|
59
|
+
// In edit mode the displayed shape is exactly what Done finishes, so live
|
|
60
|
+
// validity flips also gate the Done button (events.js dispatches them).
|
|
61
|
+
manager.emit(ADAPTER_EVENTS.VALIDITY_CHANGE, { valid: !next, reason })
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
const updateLiveValidity = () => {
|
|
65
|
+
const geom = olFeature.getGeometry()
|
|
66
|
+
const type = geom.getType()
|
|
67
|
+
const coordinates = geom.getCoordinates()
|
|
68
|
+
const numVertices = type === 'Polygon'
|
|
69
|
+
? Math.max(0, (coordinates[0]?.length ?? 1) - 1)
|
|
70
|
+
: coordinates.length
|
|
71
|
+
liveStroke.update({
|
|
72
|
+
feature: { type: 'Feature', geometry: { type, coordinates } },
|
|
73
|
+
context: { mode: 'edit_vertex' },
|
|
74
|
+
numVertices,
|
|
75
|
+
onGeometryChange: manager._geometryValidator
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
olFeature.getGeometry().on('change', updateLiveValidity)
|
|
79
|
+
return {
|
|
80
|
+
liveStroke,
|
|
81
|
+
destroy () {
|
|
82
|
+
olFeature.getGeometry().un('change', updateLiveValidity)
|
|
83
|
+
liveStroke.destroy()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Edit vertex mode — handles edit_vertex.
|
|
90
|
+
*
|
|
91
|
+
* createEditMode composes the OL pieces along their natural seams:
|
|
92
|
+
* - selectionState: shared mutable state + layer/event sync
|
|
93
|
+
* - modifyInteraction: OL Modify (pointer drag / midpoint insert) + undo-op derivation
|
|
94
|
+
* - pointerHandlers: mouse selection + delete-vertex button
|
|
95
|
+
* - touchHandler / keyboardHandler: touch drag and keyboard nudge input
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
// Delete-selected-vertex and undo operations, shared by pointer, touch and keyboard input
|
|
99
|
+
const createVertexActions = ({ olFeature, undoStack, selection, getTouchHandler }) => {
|
|
100
|
+
const { state, setState, syncGeom, emitGeometryValidation } = selection
|
|
101
|
+
|
|
102
|
+
const doDeleteVertex = () => {
|
|
103
|
+
if (state.selectedVertexType !== VERTEX_TYPE || state.selectedVertexIndex < 0) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
const result = deleteVertex(olFeature, state.selectedVertexIndex)
|
|
107
|
+
if (!result) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
undoStack.push({ type: 'delete_vertex', vertexIndex: result.deletedIndex, deletedCoord: result.deletedCoord })
|
|
111
|
+
syncGeom()
|
|
112
|
+
emitGeometryValidation(DELETE_VERTEX, result.deletedIndex)
|
|
113
|
+
setState({ selectedVertexIndex: -1, selectedVertexType: null })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const doUndo = () => {
|
|
117
|
+
const op = undoStack.pop()
|
|
118
|
+
if (!op) {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
const previousIndex = state.selectedVertexIndex
|
|
122
|
+
const restoredIndex = applyUndo(olFeature, op)
|
|
123
|
+
syncGeom()
|
|
124
|
+
// An undo commits the inverse change, so it must re-validate like any other
|
|
125
|
+
// commit — otherwise the invalid stroke and the Done gate go stale.
|
|
126
|
+
emitGeometryValidation(UNDO_INVERSE_PHASE[op.type], restoredIndex)
|
|
127
|
+
// Only re-select if a vertex was already active — undo must not create a new selection
|
|
128
|
+
const newIndex = previousIndex >= 0 ? restoredIndex : -1
|
|
129
|
+
setState({
|
|
130
|
+
selectedVertexIndex: newIndex,
|
|
131
|
+
selectedVertexType: newIndex >= 0 ? VERTEX_TYPE : null
|
|
132
|
+
})
|
|
133
|
+
if (previousIndex >= 0 && newIndex >= 0 && state.interfaceType === TOUCH_INTERFACE) {
|
|
134
|
+
getTouchHandler().updateTargetPosition()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { doDeleteVertex, doUndo }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const wireTouchHandler = ({ map, container, manager, snap, olFeature, undoStack, selection }) => {
|
|
142
|
+
const { state, getState, setState, syncGeom, emitGeometryValidation } = selection
|
|
143
|
+
const selectVertex = (index) => setState({ selectedVertexIndex: index, selectedVertexType: VERTEX_TYPE })
|
|
144
|
+
|
|
145
|
+
const touchHandler = createTouchHandler({
|
|
146
|
+
map,
|
|
147
|
+
container,
|
|
148
|
+
getState,
|
|
149
|
+
setState,
|
|
150
|
+
colors: manager.colors,
|
|
151
|
+
snap,
|
|
152
|
+
onVertexMoved ({ vertexIndex, previousCoord }) {
|
|
153
|
+
undoStack.push({ type: 'move_vertex', vertexIndex, previousCoord })
|
|
154
|
+
syncGeom()
|
|
155
|
+
emitGeometryValidation(MOVE_VERTEX, vertexIndex)
|
|
156
|
+
selectVertex(vertexIndex)
|
|
157
|
+
touchHandler.updateTargetPosition()
|
|
158
|
+
},
|
|
159
|
+
onTap (hit) {
|
|
160
|
+
if (!hit) {
|
|
161
|
+
setState({ selectedVertexIndex: -1, selectedVertexType: null })
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
if (hit.type === VERTEX_TYPE) {
|
|
165
|
+
selectVertex(hit.index)
|
|
166
|
+
touchHandler.updateTargetPosition()
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
// Only vertex and midpoint hits reach here, and the vertex case returned above
|
|
170
|
+
const result = insertAtMidpoint(olFeature, state.midpoints, hit.index, state.vertices.length)
|
|
171
|
+
if (!result) {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
undoStack.push({ type: 'insert_vertex', vertexIndex: result.insertedIndex })
|
|
175
|
+
syncGeom()
|
|
176
|
+
emitGeometryValidation(INSERT_VERTEX, result.insertedIndex)
|
|
177
|
+
selectVertex(result.insertedIndex)
|
|
178
|
+
touchHandler.updateTargetPosition()
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
selection.setHooks({
|
|
183
|
+
onDeselect: () => touchHandler.hide(),
|
|
184
|
+
onUpdate () {
|
|
185
|
+
if (state.interfaceType === TOUCH_INTERFACE) {
|
|
186
|
+
touchHandler.updateTargetPosition()
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
return touchHandler
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const wireKeyboardHandler = ({ map, container, snap, undoStack, selection, touchHandler, actions }) => {
|
|
195
|
+
const { state, getState, setState, syncGeom, emitGeometryValidation } = selection
|
|
196
|
+
|
|
197
|
+
return createKeyboardHandler({
|
|
198
|
+
map,
|
|
199
|
+
getState,
|
|
200
|
+
setState,
|
|
201
|
+
snap,
|
|
202
|
+
onVertexMoved ({ vertexIndex, previousCoord }) {
|
|
203
|
+
undoStack.push({ type: 'move_vertex', vertexIndex, previousCoord })
|
|
204
|
+
syncGeom()
|
|
205
|
+
emitGeometryValidation(MOVE_VERTEX, vertexIndex)
|
|
206
|
+
setState({ selectedVertexIndex: vertexIndex, selectedVertexType: VERTEX_TYPE })
|
|
207
|
+
},
|
|
208
|
+
onInserted ({ insertedIndex }) {
|
|
209
|
+
undoStack.push({ type: 'insert_vertex', vertexIndex: insertedIndex })
|
|
210
|
+
syncGeom()
|
|
211
|
+
emitGeometryValidation(INSERT_VERTEX, insertedIndex)
|
|
212
|
+
},
|
|
213
|
+
onDeleted: actions.doDeleteVertex,
|
|
214
|
+
onUndo: actions.doUndo,
|
|
215
|
+
onKeyboardActive () {
|
|
216
|
+
if (state.interfaceType === 'keyboard') {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
state.interfaceType = 'keyboard'
|
|
220
|
+
touchHandler.hide()
|
|
221
|
+
container.focus({ preventScroll: true })
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Style hot-swap on map style change + touch-target reposition on map resize
|
|
227
|
+
const wireMapSync = ({ map, manager, layers, selection, touchHandler, live }) => {
|
|
228
|
+
const { state } = selection
|
|
229
|
+
|
|
230
|
+
const onStylesChanged = (styles) => {
|
|
231
|
+
// Re-assert through the live-stroke controller so an invalid (dashed) shape
|
|
232
|
+
// stays dashed across a map style change.
|
|
233
|
+
live.liveStroke.refresh()
|
|
234
|
+
layers.vertexLayer.updateStyle(styles.vertexStyle)
|
|
235
|
+
layers.midpointLayer.updateStyle(styles.midpointStyle)
|
|
236
|
+
layers.activeLayer.update(state)
|
|
237
|
+
touchHandler.updateColors(manager.colors)
|
|
238
|
+
}
|
|
239
|
+
manager.on(STYLES_CHANGED_EVENT, onStylesChanged)
|
|
240
|
+
|
|
241
|
+
// Reposition the touch target after OL re-renders with the new size.
|
|
242
|
+
// change:size fires before the render, so we wait for postrender to get
|
|
243
|
+
// correct pixel coords from getPixelFromCoordinate.
|
|
244
|
+
const onMapSizeChange = () => {
|
|
245
|
+
if (state.interfaceType !== TOUCH_INTERFACE || state.selectedVertexIndex < 0) {
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
map.once('postrender', () => touchHandler.updateTargetPosition())
|
|
249
|
+
}
|
|
250
|
+
map.on('change:size', onMapSizeChange)
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
destroy () {
|
|
254
|
+
manager.off(STYLES_CHANGED_EVENT, onStylesChanged)
|
|
255
|
+
map.un('change:size', onMapSizeChange)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// The mode interface consumed by OLDrawManager
|
|
261
|
+
const buildModeApi = ({ manager, store, olFeature, originalFeatureStyle, selection, actions, parts }) => {
|
|
262
|
+
const { state } = selection
|
|
263
|
+
const { touchHandler } = parts
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
setInterfaceType (type) {
|
|
267
|
+
if (type === state.interfaceType) {
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
state.interfaceType = type
|
|
271
|
+
if (type === TOUCH_INTERFACE) {
|
|
272
|
+
touchHandler.updateTargetPosition()
|
|
273
|
+
} else {
|
|
274
|
+
touchHandler.hide()
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
done () {
|
|
279
|
+
manager.emit(ADAPTER_EVENTS.EDIT_FINISH, store.toGeoJSON(olFeature))
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
// Committed-verdict write (events.js): routed through the live-stroke
|
|
283
|
+
// controller so its cached state stays in sync with the rendered style.
|
|
284
|
+
setInvalid (invalid) {
|
|
285
|
+
parts.live.liveStroke.set(invalid)
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// Nothing to restore here: the pre-edit feature is kept as tempFeature in the
|
|
289
|
+
// reducer and events.js re-adds it on cancel
|
|
290
|
+
cancel () {},
|
|
291
|
+
|
|
292
|
+
undo: actions.doUndo,
|
|
293
|
+
deleteVertex: actions.doDeleteVertex,
|
|
294
|
+
// MoveControl's D-pad, routed here via mapProvider.activeMoveTarget (see
|
|
295
|
+
// events.js) once a vertex is selected.
|
|
296
|
+
nudgeSelectedVertex: parts.keyboardHandler.nudgeByDelta,
|
|
297
|
+
|
|
298
|
+
destroy () {
|
|
299
|
+
parts.live.destroy()
|
|
300
|
+
olFeature.setStyle(originalFeatureStyle)
|
|
301
|
+
selection.destroy()
|
|
302
|
+
parts.mapSync.destroy()
|
|
303
|
+
parts.pointerHandlers.destroy()
|
|
304
|
+
parts.modify.destroy()
|
|
305
|
+
parts.layers.activeLayer.remove()
|
|
306
|
+
parts.layers.midpointLayer.remove()
|
|
307
|
+
parts.layers.vertexLayer.remove()
|
|
308
|
+
touchHandler.destroy()
|
|
309
|
+
parts.keyboardHandler.destroy()
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @returns {{ setInterfaceType, done, cancel, undo, deleteVertex, nudgeSelectedVertex, destroy } | null}
|
|
316
|
+
*/
|
|
317
|
+
export const createEditMode = ({ map, manager, options }) => {
|
|
318
|
+
const { featureId, container, interfaceType, deleteVertexButtonId, snap } = options
|
|
319
|
+
const { store, undoStack } = manager
|
|
320
|
+
|
|
321
|
+
const olFeature = store.getOL(featureId)
|
|
322
|
+
if (!olFeature) {
|
|
323
|
+
return null
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const originalFeatureStyle = olFeature.getStyle()
|
|
327
|
+
|
|
328
|
+
const midpointLayer = createMidpointLayer(map, manager.styles.midpointStyle)
|
|
329
|
+
const vertexLayer = createVertexLayer(map, manager.styles.vertexStyle)
|
|
330
|
+
const activeLayer = createActiveVertexLayer(map, () => manager.styles)
|
|
331
|
+
|
|
332
|
+
const selection = createSelectionState({
|
|
333
|
+
map,
|
|
334
|
+
manager,
|
|
335
|
+
store,
|
|
336
|
+
olFeature,
|
|
337
|
+
interfaceType,
|
|
338
|
+
layers: { vertexLayer, midpointLayer, activeLayer }
|
|
339
|
+
})
|
|
340
|
+
const { state, getState, setState, syncGeom, emitGeometryValidation } = selection
|
|
341
|
+
|
|
342
|
+
const modify = createModifyInteraction({
|
|
343
|
+
map,
|
|
344
|
+
olFeature,
|
|
345
|
+
getState,
|
|
346
|
+
onModifyEnd (prevCoords) {
|
|
347
|
+
syncGeom()
|
|
348
|
+
const op = prevCoords && deriveModifyOp(prevCoords, state.vertices)
|
|
349
|
+
if (!op) {
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
undoStack.push(op)
|
|
353
|
+
emitGeometryValidation(OP_PHASE[op.type], op.vertexIndex)
|
|
354
|
+
setState({ selectedVertexIndex: op.vertexIndex, selectedVertexType: VERTEX_TYPE })
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
syncGeom() // initial populate
|
|
359
|
+
|
|
360
|
+
const layers = { vertexLayer, midpointLayer, activeLayer }
|
|
361
|
+
const live = wireLiveStroke({ map, manager, olFeature })
|
|
362
|
+
const touchHandler = wireTouchHandler({ map, container, manager, snap, olFeature, undoStack, selection })
|
|
363
|
+
const actions = createVertexActions({ olFeature, undoStack, selection, getTouchHandler: () => touchHandler })
|
|
364
|
+
const keyboardHandler = wireKeyboardHandler({ map, container, snap, undoStack, selection, touchHandler, actions })
|
|
365
|
+
const pointerHandlers = createPointerHandlers({
|
|
366
|
+
map,
|
|
367
|
+
container,
|
|
368
|
+
getState,
|
|
369
|
+
setState,
|
|
370
|
+
touchHandler,
|
|
371
|
+
deleteVertexButtonId,
|
|
372
|
+
onDeleteVertex: actions.doDeleteVertex
|
|
373
|
+
})
|
|
374
|
+
const mapSync = wireMapSync({ map, manager, layers, selection, touchHandler, live })
|
|
375
|
+
|
|
376
|
+
return buildModeApi({
|
|
377
|
+
manager,
|
|
378
|
+
store,
|
|
379
|
+
olFeature,
|
|
380
|
+
originalFeatureStyle,
|
|
381
|
+
selection,
|
|
382
|
+
actions,
|
|
383
|
+
parts: { touchHandler, keyboardHandler, pointerHandlers, modify, mapSync, layers, live }
|
|
384
|
+
})
|
|
385
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { createEditMode } from './EditMode.js'
|
|
2
|
+
import { createFeatureStore } from '../core/featureStore.js'
|
|
3
|
+
import { STYLES_CHANGED_EVENT } from '../core/internalEvents.js'
|
|
4
|
+
import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
|
|
5
|
+
import { createFakeMap, createFakeManager, createContainer, domEvent } from '../__helpers__/harness.js'
|
|
6
|
+
import Style from 'ol/style/Style.js'
|
|
7
|
+
import Polygon from 'ol/geom/Polygon.js'
|
|
8
|
+
|
|
9
|
+
const RING = [[0, 0], [100, 0], [100, 100], [0, 100], [0, 0]] // square: 4 vertices, deletable
|
|
10
|
+
|
|
11
|
+
// The feature style is a live function (see wireLiveStroke in EditMode.js — avoids
|
|
12
|
+
// Feature#setStyle() on every flip, which would break an in-progress Modify drag),
|
|
13
|
+
// not a static Style instance, so tests must invoke it to read the current style.
|
|
14
|
+
const currentStyle = (olFeature) => olFeature.getStyle()()
|
|
15
|
+
|
|
16
|
+
const setup = (ring = RING) => {
|
|
17
|
+
const map = createFakeMap()
|
|
18
|
+
const manager = createFakeManager()
|
|
19
|
+
manager.store = createFeatureStore()
|
|
20
|
+
manager.store.add({ type: 'Feature', id: 'f1', properties: {}, geometry: { type: 'Polygon', coordinates: [ring] } })
|
|
21
|
+
const container = createContainer()
|
|
22
|
+
map.getViewport().appendChild(container) // keyboard handler treats focus inside the viewport as map focus
|
|
23
|
+
const mode = createEditMode({
|
|
24
|
+
map,
|
|
25
|
+
manager,
|
|
26
|
+
options: { featureId: 'f1', container, interfaceType: 'mouse', deleteVertexButtonId: 'del-v', snap: null }
|
|
27
|
+
})
|
|
28
|
+
liveModes.push(mode)
|
|
29
|
+
const olFeature = manager.store.getOL('f1')
|
|
30
|
+
const tapAt = (x, y) => {
|
|
31
|
+
container.dispatchEvent(domEvent('touchstart', { touches: [{ clientX: x, clientY: y }] }))
|
|
32
|
+
container.dispatchEvent(domEvent('touchend', { changedTouches: [{ clientX: x, clientY: y }] }))
|
|
33
|
+
}
|
|
34
|
+
return { map, manager, container, mode, olFeature, ring: () => olFeature.getGeometry().getCoordinates()[0], tapAt }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const liveModes = []
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
liveModes.splice(0).forEach((m) => m?.destroy())
|
|
40
|
+
document.body.innerHTML = ''
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const key = (type, props) => window.dispatchEvent(new KeyboardEvent(type, { cancelable: true, ...props }))
|
|
44
|
+
|
|
45
|
+
test('returns null for an unknown feature id', () => {
|
|
46
|
+
const { map, manager } = setup()
|
|
47
|
+
expect(createEditMode({ map, manager, options: { featureId: 'nope', container: createContainer() } })).toBeNull()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('entering edit mode swaps the feature style and reports the initial vertex state', () => {
|
|
51
|
+
const { manager, olFeature } = setup()
|
|
52
|
+
expect(currentStyle(olFeature)).toBe(manager.styles.editFeatureStyle)
|
|
53
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_CHANGE, { numVertices: 4 })
|
|
54
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.UPDATE, expect.objectContaining({ id: 'f1' }))
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('setInvalid swaps between the solid and dashed edit styles', () => {
|
|
58
|
+
const { manager, mode, olFeature } = setup()
|
|
59
|
+
mode.setInvalid(true)
|
|
60
|
+
expect(currentStyle(olFeature)).toBe(manager.styles.editFeatureStyleInvalid)
|
|
61
|
+
mode.setInvalid(false)
|
|
62
|
+
expect(currentStyle(olFeature)).toBe(manager.styles.editFeatureStyle)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('a mid-drag crossing turns the stroke dashed live, and back when it clears', () => {
|
|
66
|
+
const { manager, olFeature } = setup()
|
|
67
|
+
// Geometry mutation (as during a Modify/touch drag) — no commit yet.
|
|
68
|
+
olFeature.getGeometry().setCoordinates([[[0, 0], [100, 100], [100, 0], [0, 100], [0, 0]]])
|
|
69
|
+
expect(currentStyle(olFeature)).toBe(manager.styles.editFeatureStyleInvalid)
|
|
70
|
+
olFeature.getGeometry().setCoordinates([[[0, 0], [100, 0], [100, 100], [0, 100], [0, 0]]])
|
|
71
|
+
expect(currentStyle(olFeature)).toBe(manager.styles.editFeatureStyle)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('a validity flip repaints the map without touching the feature — Modify tracks its own drag via feature #change, which setStyle() would trip', () => {
|
|
75
|
+
const { map, olFeature } = setup()
|
|
76
|
+
const renderCallsBefore = map.render.mock.calls.length
|
|
77
|
+
olFeature.getGeometry().setCoordinates([[[0, 0], [100, 100], [100, 0], [0, 100], [0, 0]]])
|
|
78
|
+
expect(map.render.mock.calls.length).toBeGreaterThan(renderCallsBefore)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('validity flips while editing also gate the Done button', () => {
|
|
82
|
+
const { manager, olFeature } = setup()
|
|
83
|
+
olFeature.getGeometry().setCoordinates([[[0, 0], [100, 100], [100, 0], [0, 100], [0, 0]]])
|
|
84
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VALIDITY_CHANGE,
|
|
85
|
+
expect.objectContaining({ valid: false, reason: expect.any(String) }))
|
|
86
|
+
olFeature.getGeometry().setCoordinates([[[0, 0], [100, 0], [100, 100], [0, 100], [0, 0]]])
|
|
87
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VALIDITY_CHANGE,
|
|
88
|
+
expect.objectContaining({ valid: true }))
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('the user callback runs throttled during an edit drag', () => {
|
|
92
|
+
jest.useFakeTimers()
|
|
93
|
+
const { manager, olFeature } = setup()
|
|
94
|
+
manager._geometryValidator = jest.fn(() => ({ valid: false, reason: 'outside region' }))
|
|
95
|
+
olFeature.getGeometry().setCoordinates([[[0, 0], [110, 0], [100, 100], [0, 100], [0, 0]]])
|
|
96
|
+
olFeature.getGeometry().setCoordinates([[[0, 0], [120, 0], [100, 100], [0, 100], [0, 0]]])
|
|
97
|
+
expect(manager._geometryValidator).not.toHaveBeenCalled() // deferred to the frame
|
|
98
|
+
jest.runAllTimers()
|
|
99
|
+
expect(manager._geometryValidator).toHaveBeenCalledTimes(1) // trailing edge only
|
|
100
|
+
expect(currentStyle(olFeature)).toBe(manager.styles.editFeatureStyleInvalid)
|
|
101
|
+
jest.useRealTimers()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('done() emits the edited feature; cancel() is a no-op', () => {
|
|
105
|
+
const { manager, mode } = setup()
|
|
106
|
+
mode.done()
|
|
107
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.EDIT_FINISH, expect.objectContaining({ id: 'f1' }))
|
|
108
|
+
expect(mode.cancel()).toBeUndefined()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('select via pointer, delete the vertex, then undo restores it', () => {
|
|
112
|
+
const { container, manager, mode, ring } = setup()
|
|
113
|
+
container.dispatchEvent(domEvent('pointerdown', { pointerType: 'mouse', clientX: 100, clientY: 0 }))
|
|
114
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, expect.objectContaining({ index: 1 }))
|
|
115
|
+
|
|
116
|
+
mode.deleteVertex()
|
|
117
|
+
expect(ring()).toHaveLength(4) // one vertex fewer (closed ring keeps closing coord)
|
|
118
|
+
expect(manager.undoStack.length).toBe(1)
|
|
119
|
+
|
|
120
|
+
mode.undo()
|
|
121
|
+
expect(ring()).toHaveLength(5)
|
|
122
|
+
expect(ring()[1]).toEqual([100, 0])
|
|
123
|
+
mode.undo() // empty stack — no-op
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('nudgeSelectedVertex (MoveControl D-pad) moves the selected vertex and is undoable', () => {
|
|
127
|
+
const { container, manager, mode, ring } = setup()
|
|
128
|
+
container.dispatchEvent(domEvent('pointerdown', { pointerType: 'mouse', clientX: 100, clientY: 0 }))
|
|
129
|
+
mode.nudgeSelectedVertex(1, 0, true)
|
|
130
|
+
expect(ring()[1]).not.toEqual([100, 0])
|
|
131
|
+
expect(manager.undoStack.length).toBe(1)
|
|
132
|
+
|
|
133
|
+
mode.undo()
|
|
134
|
+
expect(ring()[1]).toEqual([100, 0])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('undo re-validates with the inverse change phase (undo of a delete re-inserts)', () => {
|
|
138
|
+
jest.useFakeTimers()
|
|
139
|
+
const { container, manager, mode } = setup()
|
|
140
|
+
container.dispatchEvent(domEvent('pointerdown', { pointerType: 'mouse', clientX: 100, clientY: 0 }))
|
|
141
|
+
mode.deleteVertex()
|
|
142
|
+
jest.runAllTimers()
|
|
143
|
+
manager.emit.mockClear()
|
|
144
|
+
mode.undo()
|
|
145
|
+
jest.runAllTimers()
|
|
146
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.GEOMETRY_CHANGE, expect.objectContaining({
|
|
147
|
+
phase: 'commit-insert',
|
|
148
|
+
vertexIndex: 1,
|
|
149
|
+
feature: expect.any(Object)
|
|
150
|
+
}))
|
|
151
|
+
jest.useRealTimers()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('a Modify drag pushes a move op derived from the before/after vertices', () => {
|
|
155
|
+
const { map, manager, olFeature } = setup()
|
|
156
|
+
const interaction = map.interactions[0]
|
|
157
|
+
interaction.dispatchEvent({ type: 'modifystart' })
|
|
158
|
+
olFeature.getGeometry().setCoordinates([[[0, 0], [120, 5], [100, 100], [0, 100], [0, 0]]])
|
|
159
|
+
interaction.dispatchEvent({ type: 'modifyend' })
|
|
160
|
+
expect(manager.undoStack.pop()).toEqual({ type: 'move_vertex', vertexIndex: 1, previousCoord: [100, 0] })
|
|
161
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, expect.objectContaining({ index: 1 }))
|
|
162
|
+
|
|
163
|
+
interaction.dispatchEvent({ type: 'modifystart' })
|
|
164
|
+
interaction.dispatchEvent({ type: 'modifyend' }) // no coordinate change — nothing pushed
|
|
165
|
+
expect(manager.undoStack.length).toBe(0)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('deleting needs a selected vertex and respects the minimum ring size', () => {
|
|
169
|
+
const { mode, manager } = setup()
|
|
170
|
+
mode.deleteVertex() // nothing selected
|
|
171
|
+
expect(manager.undoStack.length).toBe(0)
|
|
172
|
+
|
|
173
|
+
const triangle = setup([[0, 0], [100, 0], [100, 100], [0, 0]])
|
|
174
|
+
triangle.container.dispatchEvent(domEvent('pointerdown', { pointerType: 'mouse', clientX: 100, clientY: 0 }))
|
|
175
|
+
triangle.mode.deleteVertex() // 3 vertices is the minimum for a ring
|
|
176
|
+
expect(triangle.ring()).toHaveLength(4)
|
|
177
|
+
expect(triangle.manager.undoStack.length).toBe(0)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('keyboard: arrows nudge the selected vertex and commit one undo op on keyup', () => {
|
|
181
|
+
const { container, manager, ring } = setup()
|
|
182
|
+
container.dispatchEvent(domEvent('pointerdown', { pointerType: 'mouse', clientX: 100, clientY: 0 }))
|
|
183
|
+
key('keydown', { key: 'ArrowRight' })
|
|
184
|
+
key('keyup', { key: 'ArrowRight' })
|
|
185
|
+
expect(ring()[1]).toEqual([105, 0])
|
|
186
|
+
expect(manager.undoStack.pop()).toEqual({ type: 'move_vertex', vertexIndex: 1, previousCoord: [100, 0] })
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('keyboard: Space selects the midpoint nearest the crosshair; nudging inserts it as a vertex', () => {
|
|
190
|
+
const { manager, ring } = setup()
|
|
191
|
+
key('keydown', { key: ' ' }) // nearest handle to center [50,50] is the midpoint [50,0]... all midpoints are at 50
|
|
192
|
+
key('keydown', { key: 'ArrowRight' })
|
|
193
|
+
key('keyup', { key: 'ArrowRight' })
|
|
194
|
+
expect(ring()).toHaveLength(6)
|
|
195
|
+
expect(ring()[1]).toEqual([55, 0]) // inserted at the midpoint, nudged one step right
|
|
196
|
+
expect(manager.undoStack.pop()).toEqual({ type: 'move_vertex', vertexIndex: 1, previousCoord: [50, 0] })
|
|
197
|
+
expect(manager.undoStack.pop()).toEqual({ type: 'insert_vertex', vertexIndex: 1 })
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('touch: dragging the offset target moves the vertex and records one move op', () => {
|
|
201
|
+
const { container, manager, mode, tapAt, ring } = setup()
|
|
202
|
+
tapAt(100, 0)
|
|
203
|
+
mode.setInterfaceType('touch')
|
|
204
|
+
// The finger lands on the SVG's inner circle, not the SVG element itself
|
|
205
|
+
const grip = container.querySelector('[data-im-draw-touch-target] circle')
|
|
206
|
+
grip.dispatchEvent(domEvent('touchstart', { touches: [{ clientX: 100, clientY: 0 }] }))
|
|
207
|
+
grip.dispatchEvent(domEvent('touchmove', { touches: [{ clientX: 120, clientY: 10 }] }))
|
|
208
|
+
grip.dispatchEvent(domEvent('touchend', { changedTouches: [{ clientX: 120, clientY: 10 }] }))
|
|
209
|
+
expect(ring()[1]).toEqual([120, 10])
|
|
210
|
+
expect(manager.undoStack.pop()).toEqual({ type: 'move_vertex', vertexIndex: 1, previousCoord: [100, 0] })
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('touch: tapping a midpoint inserts a vertex there and selects it', () => {
|
|
214
|
+
const { manager, tapAt, ring } = setup()
|
|
215
|
+
tapAt(50, 0) // midpoint between [0,0] and [100,0]
|
|
216
|
+
expect(ring()).toHaveLength(6)
|
|
217
|
+
expect(ring()[1]).toEqual([50, 0])
|
|
218
|
+
expect(manager.undoStack.pop()).toEqual({ type: 'insert_vertex', vertexIndex: 1 })
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('touch: tapping a stale midpoint the geometry can no longer place is a no-op', () => {
|
|
222
|
+
const { manager, olFeature, ring, tapAt } = setup()
|
|
223
|
+
// Desync state from geometry: swap in a smaller geometry object so the
|
|
224
|
+
// selection-state change listener (bound to the previous geometry) never
|
|
225
|
+
// fires. state.midpoints still describes the 4-vertex square (4 midpoints),
|
|
226
|
+
// while the live geometry is a 3-vertex triangle that has only 3 midpoints.
|
|
227
|
+
olFeature.setGeometry(new Polygon([[[0, 0], [100, 0], [100, 100], [0, 0]]]))
|
|
228
|
+
tapAt(0, 50) // the stale square's left-edge midpoint (index 3) — the triangle can't place it
|
|
229
|
+
expect(manager.undoStack.length).toBe(0)
|
|
230
|
+
expect(ring()).toHaveLength(4) // triangle geometry left untouched
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('undo in touch mode repositions the offset target on the restored vertex', () => {
|
|
234
|
+
const { container, manager, mode, tapAt } = setup()
|
|
235
|
+
tapAt(100, 0)
|
|
236
|
+
key('keydown', { key: 'ArrowRight' }) // keyboard nudge (switches interface to keyboard)
|
|
237
|
+
key('keyup', { key: 'ArrowRight' })
|
|
238
|
+
mode.setInterfaceType('touch')
|
|
239
|
+
mode.undo()
|
|
240
|
+
const target = container.querySelector('[data-im-draw-touch-target]')
|
|
241
|
+
expect(target.style.display).toBe('block')
|
|
242
|
+
expect(manager.undoStack.length).toBe(0)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('touch tap selects a vertex and interface switching shows/hides the touch target', () => {
|
|
246
|
+
const { container, manager, mode, tapAt } = setup()
|
|
247
|
+
tapAt(100, 0)
|
|
248
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, expect.objectContaining({ index: 1 }))
|
|
249
|
+
|
|
250
|
+
const target = container.querySelector('[data-im-draw-touch-target]')
|
|
251
|
+
mode.setInterfaceType('touch')
|
|
252
|
+
expect(target.style.display).toBe('block')
|
|
253
|
+
mode.setInterfaceType('touch') // same type — no change
|
|
254
|
+
mode.setInterfaceType('mouse')
|
|
255
|
+
expect(target.style.display).toBe('none')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
test('a tap on empty map deselects', () => {
|
|
259
|
+
const { manager, tapAt } = setup()
|
|
260
|
+
tapAt(100, 0)
|
|
261
|
+
tapAt(500, 500)
|
|
262
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, expect.objectContaining({ index: -1 }))
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test('style changes re-style the feature and handles', () => {
|
|
266
|
+
const { manager, olFeature } = setup()
|
|
267
|
+
// The real manager swaps manager.styles before emitting (see setMapStyle).
|
|
268
|
+
const newStyles = { ...manager.styles, editFeatureStyle: new Style({}) }
|
|
269
|
+
manager.styles = newStyles
|
|
270
|
+
manager.emit(STYLES_CHANGED_EVENT, newStyles)
|
|
271
|
+
expect(currentStyle(olFeature)).toBe(newStyles.editFeatureStyle)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test('a style change while the shape is invalid keeps the dashed stroke', () => {
|
|
275
|
+
const { manager, olFeature } = setup()
|
|
276
|
+
olFeature.getGeometry().setCoordinates([[[0, 0], [100, 100], [100, 0], [0, 100], [0, 0]]]) // dashed
|
|
277
|
+
const newStyles = {
|
|
278
|
+
...manager.styles,
|
|
279
|
+
editFeatureStyle: new Style({}),
|
|
280
|
+
editFeatureStyleInvalid: new Style({})
|
|
281
|
+
}
|
|
282
|
+
manager.styles = newStyles
|
|
283
|
+
manager.emit(STYLES_CHANGED_EVENT, newStyles)
|
|
284
|
+
expect(currentStyle(olFeature)).toBe(newStyles.editFeatureStyleInvalid)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
test('map resize repositions the touch target after the next render — touch with a selection only', () => {
|
|
288
|
+
const { map, mode, tapAt } = setup()
|
|
289
|
+
map.emit('change:size') // mouse interface — EditMode's own resize hook ignores it
|
|
290
|
+
tapAt(100, 0)
|
|
291
|
+
mode.setInterfaceType('touch')
|
|
292
|
+
map.emit('change:size')
|
|
293
|
+
map.emit('postrender') // once-listener fires; must not throw
|
|
294
|
+
expect(map.once).toHaveBeenCalledWith('postrender', expect.any(Function))
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
test('destroy removes interactions, layers and listeners, and restores the feature style', () => {
|
|
298
|
+
const { map, mode, olFeature } = setup()
|
|
299
|
+
mode.destroy()
|
|
300
|
+
expect(map.interactions).toHaveLength(0)
|
|
301
|
+
expect(map.layers).toHaveLength(0)
|
|
302
|
+
expect(olFeature.getStyle()).toBeNull()
|
|
303
|
+
})
|