@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,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undo handling for the shared draw mode: pushing draw_vertex operations, undoing the
|
|
3
|
+
* last placed vertex, reinitialising a feature, and the rubber-band update that follows.
|
|
4
|
+
* Part of createDrawMode.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Undo-stack and event wiring: pushing operations and reacting to undo triggers.
|
|
8
|
+
const createUndoStackHandlers = ({ geometryType, getFeature }) => ({
|
|
9
|
+
/**
|
|
10
|
+
* Push an undo operation for the last added vertex
|
|
11
|
+
*/
|
|
12
|
+
pushDrawUndo (state) {
|
|
13
|
+
const undoStack = this.map._undoStack
|
|
14
|
+
// Don't push during undo operations
|
|
15
|
+
if (!undoStack || this.map._undoInProgress) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
undoStack.push({
|
|
19
|
+
type: 'draw_vertex',
|
|
20
|
+
geometryType,
|
|
21
|
+
featureId: getFeature(state).id
|
|
22
|
+
})
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Handle the draw.undo event (fired by the undo button via the adapter). Pops the
|
|
27
|
+
* undo stack itself so it works regardless of whether the caller passes an operation.
|
|
28
|
+
*/
|
|
29
|
+
onUndo (state) {
|
|
30
|
+
const undoStack = this.map._undoStack
|
|
31
|
+
if (!undoStack || undoStack.length === 0) {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
const operation = undoStack.pop()
|
|
35
|
+
if (operation?.type === 'draw_vertex') {
|
|
36
|
+
// Prevent click interference during undo
|
|
37
|
+
this.map._undoInProgress = true
|
|
38
|
+
setTimeout(() => { this.map._undoInProgress = false }, 100)
|
|
39
|
+
this.undoVertex(state)
|
|
40
|
+
// An undo commits a vertex removal, so it must re-validate like any other
|
|
41
|
+
// commit — otherwise the Done gate goes stale.
|
|
42
|
+
this.emitDrawValidation(state, 'commit-delete')
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
_handleUndoKeydown (state, e) {
|
|
47
|
+
const tag = document.activeElement?.tagName
|
|
48
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
e.preventDefault()
|
|
52
|
+
e.stopPropagation()
|
|
53
|
+
this.onUndo(state)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Vertex-level undo: removing the last vertex and keeping the rubber band in sync.
|
|
58
|
+
const createVertexUndoHandlers = ({ ParentMode, geometryType, getCoords, getFeature, RUBBER_BAND_OFFSET }) => ({
|
|
59
|
+
/**
|
|
60
|
+
* Undo the last added vertex during drawing
|
|
61
|
+
*/
|
|
62
|
+
undoVertex (state) {
|
|
63
|
+
const feature = getFeature(state)
|
|
64
|
+
const coords = getCoords(feature)
|
|
65
|
+
|
|
66
|
+
if (coords.length < 2) {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Undoing last vertex requires reinitializing the feature
|
|
71
|
+
if (coords.length === 2) {
|
|
72
|
+
return this._reinitializeFeature(state, feature)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this._removeLastVertex(state, feature, coords)
|
|
76
|
+
return true
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Remove the last committed vertex and update rubber band
|
|
81
|
+
*/
|
|
82
|
+
_removeLastVertex (state, feature, coords) {
|
|
83
|
+
// Structure during drawing: [v1, v2, ..., vN, rubber_band]
|
|
84
|
+
const ring = geometryType === 'Polygon' ? feature.coordinates[0] : coords
|
|
85
|
+
ring.splice(-RUBBER_BAND_OFFSET, 1)
|
|
86
|
+
|
|
87
|
+
// Snap rubber band to new last vertex position. undoVertex only calls here with
|
|
88
|
+
// 3+ coords, so after the splice the ring always has a preceding vertex.
|
|
89
|
+
ring[ring.length - 1] = [...ring[ring.length - 2]]
|
|
90
|
+
|
|
91
|
+
// Keep parent mode's vertex counter in sync (min 1 for rubber band)
|
|
92
|
+
state.currentVertexPosition = Math.max(1, state.currentVertexPosition - 1)
|
|
93
|
+
|
|
94
|
+
this._ctx.store.render()
|
|
95
|
+
this._updateRubberBand(state, getCoords(feature))
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Update rubber band position based on interface type
|
|
100
|
+
*/
|
|
101
|
+
_updateRubberBand (state, coords) {
|
|
102
|
+
if (['touch', 'keyboard'].includes(state.interfaceType)) {
|
|
103
|
+
// Touch/keyboard: move to map center for add point to work
|
|
104
|
+
this._simulateMouse('mousemove', ParentMode.onMouseMove, state)
|
|
105
|
+
this._ctx.store.render()
|
|
106
|
+
} else {
|
|
107
|
+
// Mouse: keep rubber band at current position. After a vertex removal the
|
|
108
|
+
// rubber band index always resolves to a real coordinate.
|
|
109
|
+
const rubberBandIndex = geometryType === 'Polygon' ? coords.length - 2 : coords.length - 1
|
|
110
|
+
const rubberBandPos = coords[rubberBandIndex]
|
|
111
|
+
const lngLat = { lng: rubberBandPos[0], lat: rubberBandPos[1] }
|
|
112
|
+
const point = this.map.project(lngLat)
|
|
113
|
+
ParentMode.onMouseMove.call(this, state, {
|
|
114
|
+
lngLat,
|
|
115
|
+
point,
|
|
116
|
+
originalEvent: new MouseEvent('mousemove', { clientX: point.x, clientY: point.y })
|
|
117
|
+
})
|
|
118
|
+
this._ctx.store.render()
|
|
119
|
+
// Parity with _simulateMouse: an undo can add/remove a crossing, so the live
|
|
120
|
+
// invalid-stroke check must see the new displayed geometry immediately.
|
|
121
|
+
this.map.fire('draw.geometrychange', state.polygon || state.line)
|
|
122
|
+
}
|
|
123
|
+
this.dispatchVertexChange(coords)
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Reinitialising back to zero vertices: reinitialize Polygon in place, restart LineString.
|
|
128
|
+
const createFeatureReinitHandlers = ({ ParentMode, featureProp, geometryType }) => ({
|
|
129
|
+
/**
|
|
130
|
+
* Reinitialize feature when undoing to 0 vertices
|
|
131
|
+
*/
|
|
132
|
+
_reinitializeFeature (state, feature) {
|
|
133
|
+
const featureId = feature.id
|
|
134
|
+
this._ctx.store.delete([featureId])
|
|
135
|
+
|
|
136
|
+
// LineString: restart the draw mode with fresh state but same feature ID
|
|
137
|
+
if (geometryType === 'LineString') {
|
|
138
|
+
return this._restartLineStringDraw(state, featureId)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Polygon: reinitialize in place
|
|
142
|
+
const center = this.map.getCenter()
|
|
143
|
+
const initialCoords = [[center.lng, center.lat], [center.lng, center.lat]]
|
|
144
|
+
const newFeature = this.newFeature({
|
|
145
|
+
type: 'Feature',
|
|
146
|
+
properties: state.properties || {},
|
|
147
|
+
geometry: {
|
|
148
|
+
type: geometryType,
|
|
149
|
+
coordinates: [initialCoords]
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
newFeature.id = featureId
|
|
153
|
+
this._ctx.store.add(newFeature)
|
|
154
|
+
|
|
155
|
+
state[featureProp] = newFeature
|
|
156
|
+
state.currentVertexPosition = 0
|
|
157
|
+
|
|
158
|
+
this._ctx.store.render()
|
|
159
|
+
this._simulateMouse('mousemove', ParentMode.onMouseMove, state)
|
|
160
|
+
this._ctx.store.render()
|
|
161
|
+
|
|
162
|
+
this.dispatchVertexChange(initialCoords)
|
|
163
|
+
return true
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Restart the LineString draw mode with fresh state but the same feature ID
|
|
168
|
+
*/
|
|
169
|
+
_restartLineStringDraw (state, featureId) {
|
|
170
|
+
const undoStack = this.map._undoStack
|
|
171
|
+
if (undoStack) {
|
|
172
|
+
undoStack.clear()
|
|
173
|
+
}
|
|
174
|
+
// Restart draw with same options (excludeFeatureIdFromSetup prevents "continue" mode)
|
|
175
|
+
this._ctx.api.changeMode('draw_line', {
|
|
176
|
+
featureId,
|
|
177
|
+
container: state.container,
|
|
178
|
+
interfaceType: state.interfaceType,
|
|
179
|
+
crossHair: state.crossHair,
|
|
180
|
+
vertexMarkerId: state.vertexMarkerId,
|
|
181
|
+
addVertexButtonId: state.addVertexButtonId,
|
|
182
|
+
getSnapEnabled: state.getSnapEnabled,
|
|
183
|
+
properties: state.properties
|
|
184
|
+
})
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
export const createUndoHandlers = (deps) => ({
|
|
190
|
+
...createUndoStackHandlers(deps),
|
|
191
|
+
...createVertexUndoHandlers(deps),
|
|
192
|
+
...createFeatureReinitHandlers(deps)
|
|
193
|
+
})
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { setup, clickAt, firedWith, DrawPolygonMode, DrawLineMode, CENTER } from './__helpers__/harness.js'
|
|
2
|
+
|
|
3
|
+
describe('cmd/ctrl+z undo', () => {
|
|
4
|
+
const undoKey = (overrides = {}) => {
|
|
5
|
+
const e = new KeyboardEvent('keydown', { key: 'z', metaKey: true, ...overrides })
|
|
6
|
+
e.preventDefault = jest.fn()
|
|
7
|
+
e.stopPropagation = jest.fn()
|
|
8
|
+
return e
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
test('removes the last placed vertex without switching to keyboard interface', () => {
|
|
12
|
+
jest.useFakeTimers()
|
|
13
|
+
const { ctx, state, marker } = setup(DrawPolygonMode)
|
|
14
|
+
clickAt(ctx, state, 0, 0)
|
|
15
|
+
clickAt(ctx, state, 10, 0)
|
|
16
|
+
clickAt(ctx, state, 10, 10)
|
|
17
|
+
|
|
18
|
+
ctx.keydownHandler(undoKey())
|
|
19
|
+
expect(state.polygon.coordinates[0].map(([x]) => x)).toEqual([0, 10, 10])
|
|
20
|
+
expect(state.polygon.coordinates[0]).toHaveLength(3)
|
|
21
|
+
expect(state.interfaceType).toBe('mouse')
|
|
22
|
+
expect(marker.style.display).toBe('none')
|
|
23
|
+
expect(ctx.map._undoInProgress).toBe(true)
|
|
24
|
+
jest.advanceTimersByTime(100)
|
|
25
|
+
expect(ctx.map._undoInProgress).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('mouse interface keeps the rubber band on the remaining last vertex', () => {
|
|
29
|
+
const { ctx, state } = setup(DrawPolygonMode)
|
|
30
|
+
clickAt(ctx, state, 0, 0)
|
|
31
|
+
clickAt(ctx, state, 10, 0)
|
|
32
|
+
clickAt(ctx, state, 10, 10)
|
|
33
|
+
ctx.keydownHandler(undoKey())
|
|
34
|
+
const ring = state.polygon.coordinates[0]
|
|
35
|
+
expect(ring[ring.length - 1]).toEqual(ring[ring.length - 2])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('is ignored when typing in an input, when the stack is empty, or for other operations', () => {
|
|
39
|
+
const { ctx, state } = setup(DrawPolygonMode)
|
|
40
|
+
clickAt(ctx, state, 0, 0)
|
|
41
|
+
clickAt(ctx, state, 10, 0)
|
|
42
|
+
|
|
43
|
+
const input = document.createElement('input')
|
|
44
|
+
document.body.appendChild(input)
|
|
45
|
+
input.focus()
|
|
46
|
+
ctx.keydownHandler(undoKey())
|
|
47
|
+
expect(state.polygon.coordinates[0]).toHaveLength(3)
|
|
48
|
+
input.blur()
|
|
49
|
+
|
|
50
|
+
ctx.map._undoStack.clear()
|
|
51
|
+
ctx.keydownHandler(undoKey())
|
|
52
|
+
ctx.map._undoStack.push({ type: 'edit_vertex' })
|
|
53
|
+
ctx.keydownHandler(undoKey())
|
|
54
|
+
expect(state.polygon.coordinates[0]).toHaveLength(3)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('cmd+shift+z is not treated as undo', () => {
|
|
58
|
+
const { ctx, state } = setup(DrawPolygonMode)
|
|
59
|
+
clickAt(ctx, state, 0, 0)
|
|
60
|
+
clickAt(ctx, state, 10, 0)
|
|
61
|
+
ctx.keydownHandler(undoKey({ shiftKey: true }))
|
|
62
|
+
expect(state.polygon.coordinates[0]).toHaveLength(3)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('ctrl+z (Windows/Linux) also undoes the last vertex', () => {
|
|
66
|
+
const { ctx, state } = setup(DrawPolygonMode)
|
|
67
|
+
clickAt(ctx, state, 0, 0)
|
|
68
|
+
clickAt(ctx, state, 10, 0)
|
|
69
|
+
clickAt(ctx, state, 10, 10)
|
|
70
|
+
ctx.keydownHandler(undoKey({ metaKey: false, ctrlKey: true }))
|
|
71
|
+
expect(state.polygon.coordinates[0]).toHaveLength(3)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('undo via draw.undo event and reinitialisation', () => {
|
|
76
|
+
test('draw.undo pops the stack and removes the last vertex; non-draw ops are ignored', () => {
|
|
77
|
+
const { ctx, state } = setup(DrawPolygonMode)
|
|
78
|
+
clickAt(ctx, state, 0, 0)
|
|
79
|
+
clickAt(ctx, state, 10, 0)
|
|
80
|
+
clickAt(ctx, state, 10, 10)
|
|
81
|
+
// A non-draw op on top of the stack is popped but not undone.
|
|
82
|
+
ctx.map._undoStack.push({ type: 'edit_vertex' })
|
|
83
|
+
ctx.map.fire('draw.undo')
|
|
84
|
+
expect(state.polygon.coordinates[0]).toHaveLength(4)
|
|
85
|
+
// The next op is a draw_vertex → the last vertex is removed.
|
|
86
|
+
ctx.map.fire('draw.undo')
|
|
87
|
+
expect(state.polygon.coordinates[0]).toHaveLength(3)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('undo re-validates the committed shape (deferred, phase commit-delete)', () => {
|
|
91
|
+
jest.useFakeTimers()
|
|
92
|
+
const { ctx, state } = setup(DrawPolygonMode)
|
|
93
|
+
clickAt(ctx, state, 0, 0)
|
|
94
|
+
clickAt(ctx, state, 10, 0)
|
|
95
|
+
clickAt(ctx, state, 10, 10)
|
|
96
|
+
jest.runAllTimers()
|
|
97
|
+
ctx.map.fire.mockClear()
|
|
98
|
+
ctx.onUndo(state)
|
|
99
|
+
jest.runAllTimers()
|
|
100
|
+
expect(firedWith(ctx.map, 'draw.geometrychange').pop()).toEqual(expect.objectContaining({
|
|
101
|
+
phase: 'commit-delete',
|
|
102
|
+
feature: expect.any(Object)
|
|
103
|
+
}))
|
|
104
|
+
jest.useRealTimers()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('keyboard interface moves the rubber band to center after undo', () => {
|
|
108
|
+
const { ctx, state } = setup(DrawPolygonMode, { interfaceType: 'keyboard' })
|
|
109
|
+
clickAt(ctx, state, 0, 0)
|
|
110
|
+
clickAt(ctx, state, 10, 0)
|
|
111
|
+
clickAt(ctx, state, 10, 10)
|
|
112
|
+
ctx.undoVertex(state)
|
|
113
|
+
expect(state.polygon.coordinates[0].at(-1)).toEqual([CENTER.lng, CENTER.lat])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('undoVertex returns false with nothing placed', () => {
|
|
117
|
+
const { ctx, state } = setup(DrawPolygonMode)
|
|
118
|
+
expect(ctx.undoVertex(state)).toBe(false)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('undoing the only polygon vertex reinitialises the feature in place with the same id', () => {
|
|
122
|
+
const { ctx, state } = setup(DrawPolygonMode)
|
|
123
|
+
clickAt(ctx, state, 0, 0)
|
|
124
|
+
const id = state.polygon.id
|
|
125
|
+
expect(ctx.undoVertex(state)).toBe(true)
|
|
126
|
+
expect(ctx._ctx.store.delete).toHaveBeenCalledWith([id])
|
|
127
|
+
expect(state.polygon.id).toBe(id)
|
|
128
|
+
expect(state.currentVertexPosition).toBe(0)
|
|
129
|
+
expect(firedWith(ctx.map, 'draw.vertexchange').pop()).toEqual({ numVertecies: 1 })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('undoing the only line vertex restarts draw_line with the same feature id', () => {
|
|
133
|
+
const { ctx, state } = setup(DrawLineMode)
|
|
134
|
+
ctx.doClick(state)
|
|
135
|
+
ctx.map._undoStack.push({ type: 'draw_vertex' })
|
|
136
|
+
expect(ctx.undoVertex(state)).toBe(true)
|
|
137
|
+
expect(ctx.map._undoStack.length).toBe(0)
|
|
138
|
+
expect(ctx._ctx.api.changeMode).toHaveBeenCalledWith('draw_line', expect.objectContaining({
|
|
139
|
+
featureId: state.line.id,
|
|
140
|
+
container: state.container,
|
|
141
|
+
properties: state.properties
|
|
142
|
+
}))
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('Escape (container keyUp) during keyboard drawing clears the undo stack and reinitialises', () => {
|
|
146
|
+
const { ctx, state, container } = setup(DrawPolygonMode, { interfaceType: 'keyboard' })
|
|
147
|
+
clickAt(ctx, state, 0, 0)
|
|
148
|
+
container.focus()
|
|
149
|
+
const id = state.polygon.id
|
|
150
|
+
ctx.onKeyUp(state, { key: 'Escape' })
|
|
151
|
+
expect(ctx.map._undoStack.length).toBe(0)
|
|
152
|
+
expect(state.polygon.id).toBe(id)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('container keyUp defers to the parent mode when focus is elsewhere, and ignores focus inside the container', () => {
|
|
156
|
+
const { ctx, state, button, container } = setup(DrawPolygonMode)
|
|
157
|
+
ctx.onKeyUp(state, { key: 'Enter' }) // focus not on container → parent finishes on Enter
|
|
158
|
+
expect(ctx.changeMode).toHaveBeenCalledWith('simple_select', expect.anything())
|
|
159
|
+
|
|
160
|
+
button.focus()
|
|
161
|
+
ctx.changeMode.mockClear()
|
|
162
|
+
ctx.onKeyUp(state, { key: 'Enter' }) // focus on a child element → ignored
|
|
163
|
+
expect(ctx.changeMode).not.toHaveBeenCalled()
|
|
164
|
+
|
|
165
|
+
container.focus()
|
|
166
|
+
ctx.onKeyUp(state, { key: 'Escape' }) // non-keyboard Escape → handled by window keyup instead
|
|
167
|
+
expect(ctx._ctx.store.delete).not.toHaveBeenCalled()
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
describe('undo-stack and reinitialisation edge cases', () => {
|
|
172
|
+
test('placing a vertex with no undo stack does not throw and still places the vertex', () => {
|
|
173
|
+
const { ctx, state } = setup(DrawPolygonMode)
|
|
174
|
+
ctx.map._undoStack = null
|
|
175
|
+
clickAt(ctx, state, 0, 0)
|
|
176
|
+
expect(state.polygon.coordinates[0][0]).toEqual([0, 0])
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('reinitialising a polygon without properties uses an empty properties object', () => {
|
|
180
|
+
const { ctx, state } = setup(DrawPolygonMode, { properties: undefined })
|
|
181
|
+
clickAt(ctx, state, 0, 0)
|
|
182
|
+
expect(ctx.undoVertex(state)).toBe(true)
|
|
183
|
+
expect(state.polygon.properties).toEqual({})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('restarting a line with no undo stack still changes mode', () => {
|
|
187
|
+
const { ctx, state } = setup(DrawLineMode)
|
|
188
|
+
ctx.doClick(state)
|
|
189
|
+
ctx.map._undoStack = null
|
|
190
|
+
expect(ctx.undoVertex(state)).toBe(true)
|
|
191
|
+
expect(ctx._ctx.api.changeMode).toHaveBeenCalledWith('draw_line', expect.objectContaining({
|
|
192
|
+
featureId: state.line.id
|
|
193
|
+
}))
|
|
194
|
+
})
|
|
195
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import MapboxDraw from '@mapbox/mapbox-gl-draw'
|
|
2
|
+
import { isValidClick } from '../../../utils/spatial.js'
|
|
3
|
+
import { createDrawMode } from './createDrawMode.js'
|
|
4
|
+
|
|
5
|
+
// During drawing the ring is [v0...vN, rubber_band, v0_closing]; the last two are not placed vertices
|
|
6
|
+
const RUBBER_BAND_AND_CLOSING = 2
|
|
7
|
+
|
|
8
|
+
// Extend the built-in mode via the package's public API (MapboxDraw.modes) rather than a deep internal import
|
|
9
|
+
export const DrawPolygonMode = createDrawMode(MapboxDraw.modes.draw_polygon, {
|
|
10
|
+
featureProp: 'polygon',
|
|
11
|
+
geometryType: 'Polygon',
|
|
12
|
+
getCoords: (feature) => feature.coordinates[0],
|
|
13
|
+
validateClick: (feature) => isValidClick(feature.coordinates),
|
|
14
|
+
// Display ring during drawing: [v0...vN, rubber_band, v0_closing]
|
|
15
|
+
getPlacedCoords: (geojson) => geojson.geometry.coordinates[0].slice(0, -RUBBER_BAND_AND_CLOSING)
|
|
16
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { setup, clickAt, DrawPolygonMode } from './drawMode/__helpers__/harness.js'
|
|
2
|
+
|
|
3
|
+
const drawVertexMarkers = (ctx, state, geometry) => {
|
|
4
|
+
const display = jest.fn()
|
|
5
|
+
ctx.toDisplayFeatures(state, { type: 'Feature', properties: { id: state.polygon.id }, geometry }, display)
|
|
6
|
+
return display.mock.calls.map(([f]) => f)
|
|
7
|
+
.filter((f) => f.properties.meta === 'draw-vertex')
|
|
8
|
+
.map((m) => m.geometry.coordinates)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('DrawPolygonMode config', () => {
|
|
12
|
+
test('getCoords reads the polygon ring; validateClick (isValidClick) rejects a duplicate click', () => {
|
|
13
|
+
const { ctx, state } = setup(DrawPolygonMode)
|
|
14
|
+
clickAt(ctx, state, 0, 0)
|
|
15
|
+
clickAt(ctx, state, 10, 0)
|
|
16
|
+
expect(state.polygon.coordinates[0].map(([x]) => x)).toEqual([0, 10, 10]) // v0, v1, rubber band
|
|
17
|
+
clickAt(ctx, state, 10, 0) // same spot → rejected by isValidClick
|
|
18
|
+
expect(state.polygon.coordinates[0]).toHaveLength(3)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('getPlacedCoords marks every placed vertex, excluding the rubber band and closing point', () => {
|
|
22
|
+
const { ctx, state } = setup(DrawPolygonMode)
|
|
23
|
+
// Display ring: [v0..vN, rubber_band, v0_closing] → placed = slice(0, -2)
|
|
24
|
+
const ring = [[0, 0], [10, 0], [10, 10], [5, 5], [0, 0]]
|
|
25
|
+
expect(drawVertexMarkers(ctx, state, { type: 'Polygon', coordinates: [ring] })).toEqual([[0, 0], [10, 0], [10, 10]])
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { EditVertexMode } from '../../editVertexMode.js'
|
|
2
|
+
import { createUndoStack } from '../../../../../utils/undoStack.js'
|
|
3
|
+
import PolygonFeature from '../../../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/feature_types/polygon.js'
|
|
4
|
+
import LineStringFeature from '../../../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/feature_types/line_string.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shared test harness for the vertex-edit mode and its handler modules. Builds the real
|
|
8
|
+
* EditVertexMode (with the real DirectSelect parent and real mapbox-gl-draw feature
|
|
9
|
+
* classes) over test doubles for the map, store, api and DOM. Excluded from coverage.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Deterministic projection: lngLat <-> pixel is a x10 scale.
|
|
13
|
+
const project = (p) => {
|
|
14
|
+
const [lng, lat] = Array.isArray(p) ? p : [p.lng, p.lat]
|
|
15
|
+
return { x: lng * 10, y: lat * 10 }
|
|
16
|
+
}
|
|
17
|
+
const unproject = ({ x, y }) => ({ lng: x / 10, lat: y / 10 })
|
|
18
|
+
|
|
19
|
+
export const POLYGON = () => ({ type: 'Polygon', coordinates: [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]] })
|
|
20
|
+
export const LINE = () => ({ type: 'LineString', coordinates: [[0, 0], [10, 0], [10, 10]] })
|
|
21
|
+
export const dragEvt = (lng, lat) => ({ originalEvent: { stopPropagation: jest.fn() }, lngLat: { lng, lat }, point: { x: lng * 10, y: lat * 10 } })
|
|
22
|
+
export const svgTarget = (state) => ({ parentNode: state.touchVertexTarget })
|
|
23
|
+
|
|
24
|
+
const contexts = []
|
|
25
|
+
|
|
26
|
+
export const createHarness = (geometry = POLYGON(), setupOptions = {}) => {
|
|
27
|
+
const features = new Map()
|
|
28
|
+
const store = {
|
|
29
|
+
featureChanged: jest.fn(),
|
|
30
|
+
render: jest.fn(),
|
|
31
|
+
get: (id) => features.get(id)
|
|
32
|
+
}
|
|
33
|
+
const api = {
|
|
34
|
+
add: jest.fn((geojson) => {
|
|
35
|
+
const FeatureClass = geojson.geometry.type === 'Polygon' ? PolygonFeature : LineStringFeature
|
|
36
|
+
// Mirror Draw.add: re-create the feature so Polygon closing coords are normalised
|
|
37
|
+
features.set(geojson.id, new FeatureClass(_ctx, { ...geojson, id: geojson.id }))
|
|
38
|
+
return [geojson.id]
|
|
39
|
+
}),
|
|
40
|
+
changeMode: jest.fn(),
|
|
41
|
+
delete: jest.fn(),
|
|
42
|
+
trash: jest.fn()
|
|
43
|
+
}
|
|
44
|
+
const _ctx = { store, api, options: {} }
|
|
45
|
+
|
|
46
|
+
const FeatureClass = geometry.type === 'Polygon' ? PolygonFeature : LineStringFeature
|
|
47
|
+
const feature = new FeatureClass(_ctx, { type: 'Feature', id: 'feat-1', properties: {}, geometry })
|
|
48
|
+
features.set('feat-1', feature)
|
|
49
|
+
|
|
50
|
+
const listeners = {}
|
|
51
|
+
const map = {
|
|
52
|
+
_undoStack: createUndoStack(() => {}),
|
|
53
|
+
_snapInstance: null,
|
|
54
|
+
_lastEditFeatureId: null,
|
|
55
|
+
_drawEditContainer: null,
|
|
56
|
+
_editingFeatureId: null,
|
|
57
|
+
_drawCurrentMapStyle: { mapColorScheme: 'light' },
|
|
58
|
+
dragPan: { enable: jest.fn(), disable: jest.fn(), isEnabled: () => true },
|
|
59
|
+
doubleClickZoom: { enable: jest.fn(), disable: jest.fn() },
|
|
60
|
+
getSource: jest.fn(() => ({ setData: jest.fn() })),
|
|
61
|
+
getLayer: jest.fn(() => null),
|
|
62
|
+
setLayoutProperty: jest.fn(),
|
|
63
|
+
getCenter: () => ({ lng: 5, lat: 5 }),
|
|
64
|
+
project: jest.fn(project),
|
|
65
|
+
unproject: jest.fn(unproject),
|
|
66
|
+
fire: jest.fn(function (type, e) { (listeners[type] ?? []).forEach((h) => h(e)) }),
|
|
67
|
+
on: jest.fn((type, h) => { (listeners[type] ??= []).push(h) }),
|
|
68
|
+
off: jest.fn((type, h) => { listeners[type] = (listeners[type] ?? []).filter((x) => x !== h) })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const container = document.createElement('div')
|
|
72
|
+
container.tabIndex = 0
|
|
73
|
+
document.body.appendChild(container)
|
|
74
|
+
|
|
75
|
+
const ctx = {
|
|
76
|
+
map,
|
|
77
|
+
_ctx,
|
|
78
|
+
getFeature: (id) => features.get(id),
|
|
79
|
+
getSelected: jest.fn(() => [feature]),
|
|
80
|
+
setSelected: jest.fn(),
|
|
81
|
+
setSelectedCoordinates: jest.fn(),
|
|
82
|
+
clearSelectedCoordinates: jest.fn(),
|
|
83
|
+
setActionableState: jest.fn(),
|
|
84
|
+
updateUIClasses: jest.fn(),
|
|
85
|
+
fire: jest.fn()
|
|
86
|
+
}
|
|
87
|
+
Object.assign(ctx, EditVertexMode)
|
|
88
|
+
|
|
89
|
+
const options = {
|
|
90
|
+
container,
|
|
91
|
+
featureId: 'feat-1',
|
|
92
|
+
interfaceType: 'mouse',
|
|
93
|
+
deleteVertexButtonId: 'delete-vertex',
|
|
94
|
+
undoButtonId: 'undo-vertex',
|
|
95
|
+
isPanEnabled: true,
|
|
96
|
+
...setupOptions
|
|
97
|
+
}
|
|
98
|
+
const state = ctx.onSetup(options)
|
|
99
|
+
contexts.push({ ctx, state })
|
|
100
|
+
return { ctx, state, feature, features, map, store, api, container, options }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
contexts.splice(0).forEach(({ ctx, state }) => { try { ctx.onStop(state) } catch { /* already stopped */ } })
|
|
105
|
+
document.body.innerHTML = ''
|
|
106
|
+
jest.useRealTimers()
|
|
107
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure geometry helper functions for multi-ring/multi-part geometry support.
|
|
3
|
+
* These functions handle coordinate transformations between flat arrays and
|
|
4
|
+
* hierarchical GeoJSON structures for Polygon, MultiPolygon, LineString, and MultiLineString.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get flat coordinates array from feature for all geometry types.
|
|
9
|
+
* Flattens all rings/parts into a single array for unified vertex navigation.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} feature - GeoJSON geometry object
|
|
12
|
+
* @returns {Array<[number, number]>} Flat array of all coordinates
|
|
13
|
+
*/
|
|
14
|
+
export const getCoords = (feature) => {
|
|
15
|
+
if (!feature?.coordinates) {
|
|
16
|
+
return []
|
|
17
|
+
}
|
|
18
|
+
switch (feature.type) {
|
|
19
|
+
case 'LineString':
|
|
20
|
+
return feature.coordinates
|
|
21
|
+
case 'Polygon':
|
|
22
|
+
return feature.coordinates.flat(1)
|
|
23
|
+
case 'MultiLineString':
|
|
24
|
+
return feature.coordinates.flat(1)
|
|
25
|
+
case 'MultiPolygon':
|
|
26
|
+
return feature.coordinates.flat(2)
|
|
27
|
+
default:
|
|
28
|
+
return []
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get segment metadata for multi-ring/multi-part geometries.
|
|
34
|
+
* Each segment represents a ring (for Polygon) or part (for Multi*).
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} feature - GeoJSON geometry object
|
|
37
|
+
* @returns {Array<{start: number, length: number, path: number[], closed: boolean}>}
|
|
38
|
+
* Array of segment metadata objects where:
|
|
39
|
+
* - start: Starting index in flat coordinates array
|
|
40
|
+
* - length: Number of coordinates in this segment
|
|
41
|
+
* - path: Hierarchical path indices to reach this segment in GeoJSON
|
|
42
|
+
* - closed: Whether this segment is closed (true for Polygon rings)
|
|
43
|
+
*/
|
|
44
|
+
export const getRingSegments = (feature) => {
|
|
45
|
+
if (!feature?.coordinates) {
|
|
46
|
+
return []
|
|
47
|
+
}
|
|
48
|
+
const segments = []
|
|
49
|
+
let start = 0
|
|
50
|
+
|
|
51
|
+
switch (feature.type) {
|
|
52
|
+
case 'LineString':
|
|
53
|
+
segments.push({ start: 0, length: feature.coordinates.length, path: [], closed: false })
|
|
54
|
+
break
|
|
55
|
+
case 'Polygon':
|
|
56
|
+
feature.coordinates.forEach((ring, ringIdx) => {
|
|
57
|
+
segments.push({ start, length: ring.length, path: [ringIdx], closed: true })
|
|
58
|
+
start += ring.length
|
|
59
|
+
})
|
|
60
|
+
break
|
|
61
|
+
case 'MultiLineString':
|
|
62
|
+
feature.coordinates.forEach((line, lineIdx) => {
|
|
63
|
+
segments.push({ start, length: line.length, path: [lineIdx], closed: false })
|
|
64
|
+
start += line.length
|
|
65
|
+
})
|
|
66
|
+
break
|
|
67
|
+
case 'MultiPolygon':
|
|
68
|
+
feature.coordinates.forEach((polygon, polyIdx) => {
|
|
69
|
+
polygon.forEach((ring, ringIdx) => {
|
|
70
|
+
segments.push({ start, length: ring.length, path: [polyIdx, ringIdx], closed: true })
|
|
71
|
+
start += ring.length
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
break
|
|
75
|
+
default:
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return segments
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Find which segment a flat vertex index belongs to.
|
|
84
|
+
*
|
|
85
|
+
* @param {Array} segments - Array of segment metadata from getRingSegments
|
|
86
|
+
* @param {number} flatIdx - Flat vertex index
|
|
87
|
+
* @returns {{segment: Object, localIdx: number}|null}
|
|
88
|
+
* Object with segment metadata and local index within that segment, or null if not found
|
|
89
|
+
*/
|
|
90
|
+
export const getSegmentForIndex = (segments, flatIdx) => {
|
|
91
|
+
for (const seg of segments) {
|
|
92
|
+
if (flatIdx >= seg.start && flatIdx < seg.start + seg.length) {
|
|
93
|
+
return { segment: seg, localIdx: flatIdx - seg.start }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get modifiable coordinate array at a specific hierarchical path.
|
|
101
|
+
* Returns a reference to the actual coordinate array in the GeoJSON structure.
|
|
102
|
+
*
|
|
103
|
+
* @param {Object} geojson - Full GeoJSON feature object
|
|
104
|
+
* @param {number[]} path - Hierarchical path indices (e.g., [0] for first ring, [1, 0] for second polygon's first ring)
|
|
105
|
+
* @returns {Array<[number, number]>} Reference to coordinate array at path
|
|
106
|
+
*/
|
|
107
|
+
export const getModifiableCoords = (geojson, path) => {
|
|
108
|
+
let coords = geojson.geometry.coordinates
|
|
109
|
+
for (const idx of path) {
|
|
110
|
+
coords = coords[idx]
|
|
111
|
+
}
|
|
112
|
+
return coords
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Convert mapbox-gl-draw coord_path string to flat vertex index.
|
|
117
|
+
* coord_path format: "ringIdx.vertexIdx" for Polygon, "polyIdx.ringIdx.vertexIdx" for MultiPolygon, etc.
|
|
118
|
+
*
|
|
119
|
+
* @param {Object} feature - GeoJSON geometry object
|
|
120
|
+
* @param {string} coordPath - coord_path string from mapbox-gl-draw
|
|
121
|
+
* @returns {number} Flat vertex index in the unified coordinate array
|
|
122
|
+
*/
|
|
123
|
+
export const coordPathToFlatIndex = (feature, coordPath) => {
|
|
124
|
+
const parts = coordPath.split('.').map(Number)
|
|
125
|
+
const segments = getRingSegments(feature)
|
|
126
|
+
|
|
127
|
+
// Match coord_path to segment
|
|
128
|
+
for (const seg of segments) {
|
|
129
|
+
// Check if path matches (compare all but last element which is the local vertex index)
|
|
130
|
+
const pathMatches = seg.path.every((val, idx) => val === parts[idx])
|
|
131
|
+
if (pathMatches && parts.length === seg.path.length + 1) {
|
|
132
|
+
const localIdx = parts[parts.length - 1] // NOSONAR, .length greater borwser support
|
|
133
|
+
return seg.start + localIdx
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fallback: just use the last number (works for simple geometries)
|
|
138
|
+
return parts[parts.length - 1] // NOSONAR, .length greater borwser support
|
|
139
|
+
}
|