@defra/interactive-map 0.0.36-alpha → 0.0.38-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/templates/draw-tools.njk +4 -3
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api.md +32 -0
- package/docs/examples/draw-tools.mdx +0 -4
- package/docs/plugins/draw.md +482 -0
- package/docs/plugins.md +2 -6
- package/govuk-prototype-kit.config.json +3 -2
- package/package.json +2 -4
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/events.js +1 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/css/index.css +1 -97
- package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +13 -0
- package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -0
- package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
- package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
- package/plugins/draw/dist/esm/index.js +1 -0
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
- package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
- package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
- package/plugins/draw/dist/umd/index.js +2 -0
- package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
- package/plugins/draw/src/DrawInit.jsx +89 -0
- package/plugins/draw/src/DrawInit.test.jsx +184 -0
- package/plugins/draw/src/adapterEvents.js +127 -0
- package/plugins/draw/src/adapterEvents.test.js +20 -0
- package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
- package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
- package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
- package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
- package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
- package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
- package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
- package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
- package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
- package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
- package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
- package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
- package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
- package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
- package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
- package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
- package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
- package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
- package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
- package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
- package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
- package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
- package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
- package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
- package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
- package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
- package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
- package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
- package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
- package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
- package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
- package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
- package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
- package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
- package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
- package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
- package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
- package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
- package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
- package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
- package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
- package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
- package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
- package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
- package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
- package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
- package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
- package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
- package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
- package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
- package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
- package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
- package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
- package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
- package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
- package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
- package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
- package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
- package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
- package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
- package/plugins/draw/src/api/addFeature.js +22 -0
- package/plugins/draw/src/api/addFeature.test.js +41 -0
- package/plugins/draw/src/api/deleteFeature.js +11 -0
- package/plugins/draw/src/api/deleteFeature.test.js +24 -0
- package/plugins/draw/src/api/editFeature.js +54 -0
- package/plugins/draw/src/api/editFeature.test.js +134 -0
- package/plugins/draw/src/api/merge.js +30 -0
- package/plugins/draw/src/api/merge.test.js +57 -0
- package/plugins/draw/src/api/newLine.js +41 -0
- package/plugins/draw/src/api/newLine.test.js +93 -0
- package/plugins/draw/src/api/newPolygon.js +41 -0
- package/plugins/draw/src/api/newPolygon.test.js +93 -0
- package/plugins/draw/src/api/split.js +117 -0
- package/plugins/draw/src/api/split.test.js +204 -0
- package/plugins/draw/src/defaults.js +52 -0
- package/plugins/draw/src/defaults.test.js +18 -0
- package/plugins/draw/src/draw.scss +43 -0
- package/plugins/draw/src/events.js +237 -0
- package/plugins/draw/src/events.test.js +369 -0
- package/plugins/draw/src/index.js +10 -0
- package/plugins/draw/src/index.test.js +24 -0
- package/plugins/draw/src/manifest.js +170 -0
- package/plugins/draw/src/manifest.test.js +147 -0
- package/plugins/draw/src/reducer.js +69 -0
- package/plugins/draw/src/reducer.test.js +85 -0
- package/plugins/draw/src/utils/debounce.js +16 -0
- package/plugins/draw/src/utils/debounce.test.js +40 -0
- package/plugins/draw/src/utils/eventBus.js +36 -0
- package/plugins/draw/src/utils/eventBus.test.js +47 -0
- package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
- package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
- package/plugins/draw/src/utils/getValueForStyle.js +33 -0
- package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
- package/plugins/draw/src/utils/resolveColors.js +39 -0
- package/plugins/draw/src/utils/resolveColors.test.js +34 -0
- package/plugins/draw/src/utils/spatial.js +243 -0
- package/plugins/draw/src/utils/spatial.test.js +243 -0
- package/plugins/draw/src/utils/touchTarget.js +95 -0
- package/plugins/draw/src/utils/touchTarget.test.js +107 -0
- package/plugins/draw/src/utils/undoStack.js +31 -0
- package/plugins/draw/src/utils/undoStack.test.js +49 -0
- package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
- package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
- package/plugins/draw/src/validation/liveStroke.js +85 -0
- package/plugins/draw/src/validation/liveStroke.test.js +150 -0
- package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
- package/plugins/draw/src/validation/rules.js +186 -0
- package/plugins/draw/src/validation/rules.test.js +120 -0
- package/plugins/draw/src/validation/validateGeometry.js +119 -0
- package/plugins/draw/src/validation/validateGeometry.test.js +200 -0
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/src/hooks/useInteractionHandlers.js +1 -1
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
- package/plugins/interact/src/utils/buildStylesMap.js +1 -0
- package/plugins/search/dist/css/index.css +1 -317
- package/rollup.esm.mjs +35 -28
- package/sonar-project.properties +2 -2
- package/src/App/components/Attributions/Attributions.module.scss +1 -0
- package/src/App/components/Hints/Hints.module.scss +5 -4
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
- package/src/App/components/MoveControl/MoveControl.jsx +73 -9
- package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
- package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
- package/src/App/hooks/useHintsAPI.js +67 -0
- package/src/App/hooks/useHintsAPI.test.js +145 -0
- package/src/App/renderer/PluginInits.jsx +4 -0
- package/src/App/renderer/PluginInits.test.jsx +7 -1
- package/src/InteractiveMap/InteractiveMap.js +20 -0
- package/src/InteractiveMap/InteractiveMap.test.js +7 -0
- package/src/config/events.js +4 -0
- package/src/scss/settings/_dimensions.scss +3 -0
- package/src/services/announcer.js +38 -3
- package/src/services/announcer.test.js +133 -1
- package/src/services/hints.js +3 -0
- package/src/services/hints.test.js +19 -0
- package/src/types.js +12 -0
- package/src/utils/detectInterfaceType.js +5 -1
- package/src/utils/isMac.js +15 -0
- package/src/utils/isMac.test.js +37 -0
- package/webpack.umd.mjs +1 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { splitPolygon } from '../utils/spatial.js'
|
|
2
|
+
import { ADAPTER_EVENTS } from '../adapterEvents.js'
|
|
3
|
+
|
|
4
|
+
const INVALID_REASON = 'Line does not split the shape into two parts'
|
|
5
|
+
const MIN_LINE_VERTICES = 2
|
|
6
|
+
const SPLITTER_ID = '_splitter'
|
|
7
|
+
|
|
8
|
+
const computeIsValid = (polygonFeature, feature) => {
|
|
9
|
+
const coordinates = feature.geometry?.coordinates
|
|
10
|
+
if (!coordinates || coordinates.length < MIN_LINE_VERTICES) {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
return !!splitPolygon(polygonFeature, { id: SPLITTER_ID, geometry: feature.geometry })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Colours the splitter line preview. Uses setDrawingPreviewProperty rather than
|
|
17
|
+
// setFeatureProperty since the line has no stable id until it's created.
|
|
18
|
+
const applySplitPreview = ({ draw, polygonFeature, feature }) => {
|
|
19
|
+
const isValid = computeIsValid(polygonFeature, feature)
|
|
20
|
+
draw.setDrawingPreviewProperty('splitter', isValid ? 'valid' : 'invalid')
|
|
21
|
+
return isValid
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Colours the preview and gates Done/the finish gesture for a committed vertex.
|
|
25
|
+
const applySplitCommit = ({ draw, dispatch, polygonFeature, feature }) => {
|
|
26
|
+
const isValid = applySplitPreview({ draw, polygonFeature, feature })
|
|
27
|
+
dispatch({ type: 'SET_ACTION', payload: { name: 'split', isValid } })
|
|
28
|
+
dispatch({ type: 'SET_GEOMETRY_VALID', payload: isValid })
|
|
29
|
+
draw.setGeometryValid(isValid)
|
|
30
|
+
return isValid
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Installed as draw._geometryValidator. 'place' and 'create' are no-ops — a split
|
|
34
|
+
// isn't complete until its last vertex, so checking those would block placement
|
|
35
|
+
// and hijack an early finish into edit mode.
|
|
36
|
+
const createSplitValidator = ({ draw, dispatch, polygonFeature }) => ({ feature, phase }) => {
|
|
37
|
+
let isValid
|
|
38
|
+
if (phase === 'preview') {
|
|
39
|
+
isValid = applySplitPreview({ draw, polygonFeature, feature })
|
|
40
|
+
} else if (phase?.startsWith('commit-')) {
|
|
41
|
+
isValid = applySplitCommit({ draw, dispatch, polygonFeature, feature })
|
|
42
|
+
} else {
|
|
43
|
+
return { valid: true }
|
|
44
|
+
}
|
|
45
|
+
return isValid ? { valid: true } : { valid: false, reason: INVALID_REASON }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Start drawing a split line for a polygon.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} context - plugin context
|
|
52
|
+
* @param {string} featureId - ID of the polygon to split
|
|
53
|
+
* @param {object} options - Options including snapLayers.
|
|
54
|
+
*/
|
|
55
|
+
export const split = ({ appState, appConfig, pluginState, mapState, mapProvider, services }, featureId, options = {}) => {
|
|
56
|
+
const { dispatch } = pluginState
|
|
57
|
+
const { draw } = mapProvider
|
|
58
|
+
const { eventBus } = services
|
|
59
|
+
|
|
60
|
+
if (!draw) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const polygonFeature = draw.get(featureId)
|
|
65
|
+
|
|
66
|
+
// Swap in split's own rule; restored in stopListening.
|
|
67
|
+
const previousValidator = draw._geometryValidator
|
|
68
|
+
draw._geometryValidator = createSplitValidator({ draw, dispatch, polygonFeature })
|
|
69
|
+
|
|
70
|
+
// Always include the draw outline layer so the split line snaps to it
|
|
71
|
+
const snapLayers = ['stroke-inactive.cold', ...(options.snapLayers || [])]
|
|
72
|
+
draw.setSnapLayers(snapLayers)
|
|
73
|
+
dispatch({ type: 'SET_HAS_SNAP_LAYERS', payload: true })
|
|
74
|
+
|
|
75
|
+
draw.changeMode('draw_line', {
|
|
76
|
+
container: appState.layoutRefs.viewportRef.current,
|
|
77
|
+
addVertexButtonId: `${appConfig.id}-draw-add-point`,
|
|
78
|
+
interfaceType: appState.interfaceType,
|
|
79
|
+
crossHair: mapState?.crossHair,
|
|
80
|
+
getSnapEnabled: () => draw.isSnapEnabled(),
|
|
81
|
+
featureId: SPLITTER_ID,
|
|
82
|
+
properties: { splitter: 'invalid' }
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Unregister everything scoped to this session.
|
|
86
|
+
const stopListening = () => {
|
|
87
|
+
draw._geometryValidator = previousValidator
|
|
88
|
+
draw.off(ADAPTER_EVENTS.CREATE, onSplitCreate)
|
|
89
|
+
draw.off(ADAPTER_EVENTS.CANCEL, onSplitCancel)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Compute split result once the line is finalised. The splitter line only ever
|
|
93
|
+
// exists to compute this — it must never linger in the draw store afterwards.
|
|
94
|
+
const onSplitCreate = (geojsonFeature) => {
|
|
95
|
+
stopListening()
|
|
96
|
+
draw.delete(SPLITTER_ID)
|
|
97
|
+
const featureCollection = splitPolygon(polygonFeature, geojsonFeature)
|
|
98
|
+
|
|
99
|
+
dispatch({ type: 'SET_ACTION', payload: { name: 'split', isValid: !!featureCollection } })
|
|
100
|
+
|
|
101
|
+
if (featureCollection) {
|
|
102
|
+
eventBus.emit('draw:split', {
|
|
103
|
+
originalFeatureId: featureId,
|
|
104
|
+
featureCollection
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Abandoned (e.g. Escape) — just stop listening.
|
|
110
|
+
const onSplitCancel = () => { stopListening() }
|
|
111
|
+
|
|
112
|
+
draw.on(ADAPTER_EVENTS.CREATE, onSplitCreate)
|
|
113
|
+
draw.on(ADAPTER_EVENTS.CANCEL, onSplitCancel)
|
|
114
|
+
|
|
115
|
+
dispatch({ type: 'SET_MODE', payload: 'draw_line' })
|
|
116
|
+
dispatch({ type: 'SET_ACTION', payload: { name: 'split' } })
|
|
117
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { split } from './split.js'
|
|
2
|
+
import { splitPolygon } from '../utils/spatial.js'
|
|
3
|
+
|
|
4
|
+
jest.mock('../utils/spatial.js', () => ({ splitPolygon: jest.fn() }))
|
|
5
|
+
|
|
6
|
+
const makeContext = (overrides = {}) => {
|
|
7
|
+
const dispatch = jest.fn()
|
|
8
|
+
const eventBus = { emit: jest.fn() }
|
|
9
|
+
const draw = {
|
|
10
|
+
get: jest.fn(() => ({ id: 'poly' })),
|
|
11
|
+
setSnapLayers: jest.fn(),
|
|
12
|
+
changeMode: jest.fn(),
|
|
13
|
+
on: jest.fn(),
|
|
14
|
+
off: jest.fn(),
|
|
15
|
+
isSnapEnabled: jest.fn(() => true),
|
|
16
|
+
setGeometryValid: jest.fn(),
|
|
17
|
+
setDrawingPreviewProperty: jest.fn(),
|
|
18
|
+
delete: jest.fn()
|
|
19
|
+
}
|
|
20
|
+
const context = {
|
|
21
|
+
appState: { layoutRefs: { viewportRef: { current: 'viewport' } }, interfaceType: 'mouse' },
|
|
22
|
+
appConfig: { id: 'app' },
|
|
23
|
+
pluginState: { dispatch },
|
|
24
|
+
mapState: { crossHair: true },
|
|
25
|
+
mapProvider: { draw },
|
|
26
|
+
services: { eventBus },
|
|
27
|
+
...overrides
|
|
28
|
+
}
|
|
29
|
+
return { context, dispatch, draw, eventBus }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const handlerFor = (draw, event) => draw.on.mock.calls.find(([name]) => name === event)?.[1]
|
|
33
|
+
|
|
34
|
+
const lineFeature = (coordinates) => ({ type: 'Feature', geometry: { type: 'LineString', coordinates } })
|
|
35
|
+
|
|
36
|
+
beforeEach(() => jest.clearAllMocks())
|
|
37
|
+
|
|
38
|
+
describe('split', () => {
|
|
39
|
+
test('does nothing when there is no draw instance', () => {
|
|
40
|
+
const { context, dispatch } = makeContext({ mapProvider: { draw: null } })
|
|
41
|
+
expect(() => split(context, 'poly')).not.toThrow()
|
|
42
|
+
expect(dispatch).not.toHaveBeenCalled()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('sets up the splitter line drawing, installs a validator, and registers listeners', () => {
|
|
46
|
+
const { context, dispatch, draw } = makeContext()
|
|
47
|
+
|
|
48
|
+
split(context, 'poly')
|
|
49
|
+
|
|
50
|
+
expect(typeof draw._geometryValidator).toBe('function')
|
|
51
|
+
expect(draw.setSnapLayers).toHaveBeenCalledWith(['stroke-inactive.cold'])
|
|
52
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_HAS_SNAP_LAYERS', payload: true })
|
|
53
|
+
expect(draw.changeMode).toHaveBeenCalledWith('draw_line', expect.objectContaining({
|
|
54
|
+
container: 'viewport',
|
|
55
|
+
addVertexButtonId: 'app-draw-add-point',
|
|
56
|
+
interfaceType: 'mouse',
|
|
57
|
+
crossHair: true,
|
|
58
|
+
featureId: '_splitter',
|
|
59
|
+
properties: { splitter: 'invalid' }
|
|
60
|
+
}))
|
|
61
|
+
expect(draw.on).toHaveBeenCalledWith('create', expect.any(Function))
|
|
62
|
+
expect(draw.on).toHaveBeenCalledWith('cancel', expect.any(Function))
|
|
63
|
+
expect(draw.on).not.toHaveBeenCalledWith('geometrychange', expect.anything())
|
|
64
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_MODE', payload: 'draw_line' })
|
|
65
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_ACTION', payload: { name: 'split' } })
|
|
66
|
+
expect(draw.changeMode.mock.calls[0][1].getSnapEnabled()).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('merges option snapLayers with the outline layer', () => {
|
|
70
|
+
const { context, draw } = makeContext()
|
|
71
|
+
split(context, 'poly', { snapLayers: ['extra'] })
|
|
72
|
+
expect(draw.setSnapLayers).toHaveBeenCalledWith(['stroke-inactive.cold', 'extra'])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('finalising the line computes a valid split, emits the result, and restores the previous validator', () => {
|
|
76
|
+
const { context, dispatch, draw, eventBus } = makeContext()
|
|
77
|
+
const polygonFeature = { id: 'poly' }
|
|
78
|
+
const featureCollection = { type: 'FeatureCollection' }
|
|
79
|
+
const previousValidator = jest.fn()
|
|
80
|
+
draw._geometryValidator = previousValidator
|
|
81
|
+
draw.get.mockReturnValue(polygonFeature)
|
|
82
|
+
splitPolygon.mockReturnValue(featureCollection)
|
|
83
|
+
|
|
84
|
+
split(context, 'poly')
|
|
85
|
+
const onCreate = handlerFor(draw, 'create')
|
|
86
|
+
const geojson = { id: 'line' }
|
|
87
|
+
onCreate(geojson)
|
|
88
|
+
|
|
89
|
+
expect(draw.off).toHaveBeenCalledWith('create', onCreate)
|
|
90
|
+
expect(draw.off).toHaveBeenCalledWith('cancel', expect.any(Function))
|
|
91
|
+
expect(splitPolygon).toHaveBeenCalledWith(polygonFeature, geojson)
|
|
92
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_ACTION', payload: { name: 'split', isValid: true } })
|
|
93
|
+
expect(eventBus.emit).toHaveBeenCalledWith('draw:split', { originalFeatureId: 'poly', featureCollection })
|
|
94
|
+
expect(draw._geometryValidator).toBe(previousValidator)
|
|
95
|
+
// The splitter line only ever existed to compute the split — it must not
|
|
96
|
+
// linger in the draw store afterwards.
|
|
97
|
+
expect(draw.delete).toHaveBeenCalledWith('_splitter')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('cancelling the splitter line stops listening and restores the previous validator', () => {
|
|
101
|
+
const { context, draw } = makeContext()
|
|
102
|
+
const previousValidator = jest.fn()
|
|
103
|
+
draw._geometryValidator = previousValidator
|
|
104
|
+
|
|
105
|
+
split(context, 'poly')
|
|
106
|
+
const onCancel = handlerFor(draw, 'cancel')
|
|
107
|
+
onCancel()
|
|
108
|
+
|
|
109
|
+
expect(draw.off).toHaveBeenCalledWith('create', expect.any(Function))
|
|
110
|
+
expect(draw.off).toHaveBeenCalledWith('cancel', onCancel)
|
|
111
|
+
expect(splitPolygon).not.toHaveBeenCalled()
|
|
112
|
+
expect(draw._geometryValidator).toBe(previousValidator)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('finalising the line computes an invalid split and does not emit', () => {
|
|
116
|
+
const { context, dispatch, draw, eventBus } = makeContext()
|
|
117
|
+
splitPolygon.mockReturnValue(null)
|
|
118
|
+
|
|
119
|
+
split(context, 'poly')
|
|
120
|
+
handlerFor(draw, 'create')({ id: 'line' })
|
|
121
|
+
|
|
122
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_ACTION', payload: { name: 'split', isValid: false } })
|
|
123
|
+
expect(eventBus.emit).not.toHaveBeenCalled()
|
|
124
|
+
expect(draw.delete).toHaveBeenCalledWith('_splitter')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('the installed draw._geometryValidator', () => {
|
|
128
|
+
test('never blocks a hard placement veto (phase: place)', () => {
|
|
129
|
+
const { context, dispatch, draw } = makeContext()
|
|
130
|
+
split(context, 'poly')
|
|
131
|
+
dispatch.mockClear()
|
|
132
|
+
|
|
133
|
+
const result = draw._geometryValidator({ feature: lineFeature([[0, 0], [1, 1]]), phase: 'place', mode: 'draw_line', vertexIndex: 1 })
|
|
134
|
+
|
|
135
|
+
expect(result).toEqual({ valid: true })
|
|
136
|
+
expect(splitPolygon).not.toHaveBeenCalled()
|
|
137
|
+
expect(draw.setDrawingPreviewProperty).not.toHaveBeenCalled()
|
|
138
|
+
expect(dispatch).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'SET_ACTION' }))
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('never blocks the whole-feature finish check (phase: create)', () => {
|
|
142
|
+
const { context, dispatch, draw } = makeContext()
|
|
143
|
+
split(context, 'poly')
|
|
144
|
+
dispatch.mockClear()
|
|
145
|
+
|
|
146
|
+
const result = draw._geometryValidator({ feature: lineFeature([[0, 0], [1, 1]]), phase: 'create', mode: 'draw_line' })
|
|
147
|
+
|
|
148
|
+
expect(result).toEqual({ valid: true })
|
|
149
|
+
expect(splitPolygon).not.toHaveBeenCalled()
|
|
150
|
+
expect(dispatch).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'SET_ACTION' }))
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('the live preview (phase: preview) only updates the visual colour, never Done-gating — even with just 1 placed vertex', () => {
|
|
154
|
+
const { context, dispatch, draw } = makeContext()
|
|
155
|
+
const polygonFeature = { id: 'poly' }
|
|
156
|
+
draw.get.mockReturnValue(polygonFeature)
|
|
157
|
+
splitPolygon.mockReturnValue({ type: 'FeatureCollection' })
|
|
158
|
+
split(context, 'poly')
|
|
159
|
+
dispatch.mockClear()
|
|
160
|
+
|
|
161
|
+
// 1 placed vertex + the rubber-band cursor. validateDisplayedGeometry only
|
|
162
|
+
// gates the built-in rules by numVertices, not a caller's own rule, so this
|
|
163
|
+
// must still reach the validator (numVertices: 1, below MIN_VERTICES.LineString).
|
|
164
|
+
const feature = lineFeature([[0, 0], [1, 1]])
|
|
165
|
+
const result = draw._geometryValidator({ feature, phase: 'preview', mode: 'draw_line', numVertices: 1 })
|
|
166
|
+
|
|
167
|
+
expect(splitPolygon).toHaveBeenCalledWith(polygonFeature, { id: '_splitter', geometry: feature.geometry })
|
|
168
|
+
expect(draw.setDrawingPreviewProperty).toHaveBeenCalledWith('splitter', 'valid')
|
|
169
|
+
// Committed Done-gating (pluginState.geometryValid / map._drawGeometryValid) must only
|
|
170
|
+
// change on an actual commit — otherwise a live "what if you added a point here" check
|
|
171
|
+
// could flicker a just-committed valid split back to disabled.
|
|
172
|
+
expect(dispatch).not.toHaveBeenCalled()
|
|
173
|
+
expect(draw.setGeometryValid).not.toHaveBeenCalled()
|
|
174
|
+
expect(result).toEqual({ valid: true })
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('a committed vertex (phase: commit-add) updates both the visual colour and Done-gating', () => {
|
|
178
|
+
const { context, dispatch, draw } = makeContext()
|
|
179
|
+
splitPolygon.mockReturnValue(null)
|
|
180
|
+
split(context, 'poly')
|
|
181
|
+
|
|
182
|
+
const feature = lineFeature([[0, 0], [1, 1]])
|
|
183
|
+
const result = draw._geometryValidator({ feature, phase: 'commit-add', mode: 'draw_line', vertexIndex: 1 })
|
|
184
|
+
|
|
185
|
+
expect(draw.setDrawingPreviewProperty).toHaveBeenCalledWith('splitter', 'invalid')
|
|
186
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_ACTION', payload: { name: 'split', isValid: false } })
|
|
187
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_GEOMETRY_VALID', payload: false })
|
|
188
|
+
expect(draw.setGeometryValid).toHaveBeenCalledWith(false)
|
|
189
|
+
expect(result).toEqual({ valid: false, reason: expect.any(String) })
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('a line with fewer than two coordinates is treated as invalid without calling splitPolygon', () => {
|
|
193
|
+
const { context, draw } = makeContext()
|
|
194
|
+
split(context, 'poly')
|
|
195
|
+
|
|
196
|
+
const feature = lineFeature([[0, 0]])
|
|
197
|
+
const result = draw._geometryValidator({ feature, phase: 'preview', mode: 'draw_line', numVertices: 0 })
|
|
198
|
+
|
|
199
|
+
expect(splitPolygon).not.toHaveBeenCalled()
|
|
200
|
+
expect(draw.setDrawingPreviewProperty).toHaveBeenCalledWith('splitter', 'invalid')
|
|
201
|
+
expect(result).toEqual({ valid: false, reason: expect.any(String) })
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const BLUE = 'rgba(29,112,184,1)'
|
|
2
|
+
const WHITE = '#ffffff'
|
|
3
|
+
const BLACK = 'rgba(11,12,12,1)'
|
|
4
|
+
const LIGHT_BLUE = 'rgba(29,112,184,0.1)'
|
|
5
|
+
const LIGHT_WHITE = 'rgba(255,255,255,0.1)'
|
|
6
|
+
const RED = 'rgba(212,53,28,1)'
|
|
7
|
+
const MID_ORANGE = 'rgba(212,53,28,0.5)'
|
|
8
|
+
const MID_BLUE = 'rgba(29,112,184,0.5)'
|
|
9
|
+
const GREEN = 'rgba(40,161,151,1)'
|
|
10
|
+
|
|
11
|
+
export const COLORS = {
|
|
12
|
+
editStroke: { light: BLUE, dark: WHITE },
|
|
13
|
+
editFill: { light: LIGHT_BLUE, dark: LIGHT_WHITE },
|
|
14
|
+
editVertex: { light: BLUE, dark: WHITE },
|
|
15
|
+
editMidpoint: { light: BLUE, dark: WHITE },
|
|
16
|
+
editHalo: { light: WHITE, dark: BLACK },
|
|
17
|
+
editActive: { light: BLACK, dark: WHITE },
|
|
18
|
+
splitInvalid: { light: BLUE, dark: WHITE },
|
|
19
|
+
splitValid: { light: BLUE, dark: WHITE },
|
|
20
|
+
invalidStroke: { light: BLUE, dark: WHITE },
|
|
21
|
+
shapeStroke: RED,
|
|
22
|
+
shapeFill: MID_ORANGE,
|
|
23
|
+
snapVertex: MID_ORANGE,
|
|
24
|
+
snapMidpoint: GREEN,
|
|
25
|
+
snapEdge: MID_BLUE
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const SIZES = {
|
|
29
|
+
strokeWidth: 2,
|
|
30
|
+
vertexRadius: 6,
|
|
31
|
+
midpointRadius: 4,
|
|
32
|
+
vertexHaloRadius: 8,
|
|
33
|
+
midpointHaloRadius: 6,
|
|
34
|
+
touchTargetSize: 48,
|
|
35
|
+
touchIndicatorRadius: 30
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const TOLERANCES = {
|
|
39
|
+
snapRadius: 12
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const KEYBOARD = {
|
|
43
|
+
nudgeAmount: 1,
|
|
44
|
+
stepAmount: 5
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Scale factor applied to draw UI (touch targets, vertex handles) per app map size
|
|
48
|
+
export const MAP_SIZE_SCALES = {
|
|
49
|
+
small: 1,
|
|
50
|
+
medium: 1.5,
|
|
51
|
+
large: 2
|
|
52
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { COLORS, SIZES, TOLERANCES, KEYBOARD, MAP_SIZE_SCALES } from './defaults.js'
|
|
2
|
+
|
|
3
|
+
describe('draw defaults', () => {
|
|
4
|
+
test('exposes colour variants for the edit palette', () => {
|
|
5
|
+
expect(COLORS.editStroke).toEqual(expect.objectContaining({ light: expect.any(String), dark: expect.any(String) }))
|
|
6
|
+
expect(COLORS.shapeStroke).toEqual(expect.any(String))
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test('exposes numeric sizes and tolerances', () => {
|
|
10
|
+
expect(SIZES.touchTargetSize).toBe(48)
|
|
11
|
+
expect(TOLERANCES.snapRadius).toBe(12)
|
|
12
|
+
expect(KEYBOARD).toEqual({ nudgeAmount: 1, stepAmount: 5 })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('maps app map sizes to scale factors', () => {
|
|
16
|
+
expect(MAP_SIZE_SCALES).toEqual({ small: 1, medium: 1.5, large: 2 })
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Touch vertex target (MapLibre)
|
|
2
|
+
.touch-vertex-target {
|
|
3
|
+
circle {
|
|
4
|
+
fill: var(--map-overlay-foreground-color);
|
|
5
|
+
}
|
|
6
|
+
path {
|
|
7
|
+
fill: var(--map-overlay-halo-color);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Touch vertex target (OpenLayers)
|
|
12
|
+
.im-draw-touch-target {
|
|
13
|
+
color: #3b82f6;
|
|
14
|
+
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15));
|
|
15
|
+
cursor: grab;
|
|
16
|
+
|
|
17
|
+
&:active {
|
|
18
|
+
cursor: grabbing;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Accessibility: focus visible on buttons
|
|
23
|
+
.im-draw-button:focus-visible {
|
|
24
|
+
outline: 2px solid #3b82f6;
|
|
25
|
+
outline-offset: 2px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Ensure action buttons take up same width
|
|
29
|
+
.im-c-actions {
|
|
30
|
+
.im-c-button-wrapper--draw-done,
|
|
31
|
+
.im-c-button-wrapper--draw-menu,
|
|
32
|
+
.im-c-button-wrapper--draw-cancel {
|
|
33
|
+
width: 33.33%;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
.im-o-app--tablet .im-c-actions,
|
|
37
|
+
.im-o-app--desktop .im-c-actions {
|
|
38
|
+
.im-c-button-wrapper--draw-done,
|
|
39
|
+
.im-c-button-wrapper--draw-menu,
|
|
40
|
+
.im-c-button-wrapper--draw-cancel {
|
|
41
|
+
width: 100px;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Button and map-event wiring for the draw plugin.
|
|
3
|
+
*
|
|
4
|
+
* Uses the normalised adapter interface (draw.on/off, draw.done(), draw.cancel(),
|
|
5
|
+
* draw.setSnapEnabled(), etc.) so this file is map-framework-agnostic.
|
|
6
|
+
* All MapLibre / OL specifics live in the adapter.
|
|
7
|
+
*/
|
|
8
|
+
import { ADAPTER_EVENTS } from './adapterEvents.js'
|
|
9
|
+
import { validateGeometry } from './validation/validateGeometry.js'
|
|
10
|
+
import { MIN_VERTICES_REASONS } from './validation/rules.js'
|
|
11
|
+
import { MAP_SIZE_SCALES } from './defaults.js'
|
|
12
|
+
|
|
13
|
+
const EDIT_VERTEX_MODE = 'edit_vertex'
|
|
14
|
+
const GEOMETRY_INVALID_EVENT = 'draw:geometryinvalid'
|
|
15
|
+
|
|
16
|
+
// A shape that simply hasn't reached its minimum vertex count yet isn't a mistake —
|
|
17
|
+
// it's the normal, expected state of "still being drawn" — so it's excluded below
|
|
18
|
+
// even though it's a genuine SOFT_RULES failure like any other.
|
|
19
|
+
const isIncompleteShape = (reason) => Object.values(MIN_VERTICES_REASONS).includes(reason)
|
|
20
|
+
|
|
21
|
+
// A discrete, one-shot invalid result (a rejected placement, a finished-but-invalid
|
|
22
|
+
// shape, or a completed commit that failed validation — never a continuous live-drag
|
|
23
|
+
// flip, which goes via onValidityChange/CAN_PLACE_CHANGE instead) gets a hint toast
|
|
24
|
+
// alongside the existing public event, so the reason is visible without every
|
|
25
|
+
// consumer having to wire up their own listener for it. The public event still
|
|
26
|
+
// fires either way — only the hint is skipped for an incomplete shape.
|
|
27
|
+
const createGeometryInvalidEmitter = (eventBus, hints) => (payload) => {
|
|
28
|
+
eventBus.emit(GEOMETRY_INVALID_EVENT, payload)
|
|
29
|
+
if (payload.reason && !isIncompleteShape(payload.reason)) { hints.show(payload.reason) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Claims MoveControl's D-pad for the currently selected vertex — see the generic
|
|
33
|
+
// mapProvider.activeMoveTarget contract (MoveControl.jsx). Any plugin could use
|
|
34
|
+
// this slot; the draw plugin is just its first consumer.
|
|
35
|
+
const buildVertexMoveTarget = (draw) => ({
|
|
36
|
+
move: (dx, dy, isLargeStep) => draw.nudgeSelectedVertex(dx, dy, isLargeStep),
|
|
37
|
+
label: 'vertex'
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Vertex-selection handlers: sync pluginState.selectedVertexIndex, and claim/release
|
|
41
|
+
// MoveControl's D-pad via buildVertexMoveTarget above.
|
|
42
|
+
const createVertexSelectionHandlers = ({ draw, pluginState, mapProvider, eventBus }) => ({
|
|
43
|
+
onVertexSelection: (e) => {
|
|
44
|
+
pluginState.dispatch({ type: 'SET_SELECTED_VERTEX_INDEX', payload: e })
|
|
45
|
+
mapProvider.activeMoveTarget = e.index >= 0 ? buildVertexMoveTarget(draw) : null
|
|
46
|
+
eventBus.emit('draw:vertexselection', e)
|
|
47
|
+
},
|
|
48
|
+
onVertexChange: (e) => {
|
|
49
|
+
pluginState.dispatch({ type: 'SET_SELECTED_VERTEX_INDEX', payload: { index: -1, numVertices: e.numVertices } })
|
|
50
|
+
mapProvider.activeMoveTarget = null
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Re-open a feature in vertex-edit mode (used when a drawn shape finishes invalid).
|
|
55
|
+
// Mirrors the options built by api/editFeature.js. Edit-mode Done is gated by the
|
|
56
|
+
// same validity, so the shape can't be finished until it's fixed.
|
|
57
|
+
const enterEditVertexMode = ({ draw, appState, appConfig, mapState, dispatch }, featureId) => {
|
|
58
|
+
draw.changeMode(EDIT_VERTEX_MODE, {
|
|
59
|
+
container: appState?.layoutRefs?.viewportRef?.current,
|
|
60
|
+
deleteVertexButtonId: `${appConfig?.id}-draw-delete-point`,
|
|
61
|
+
undoButtonId: `${appConfig?.id}-draw-undo`,
|
|
62
|
+
isPanEnabled: appState?.interfaceType !== 'keyboard',
|
|
63
|
+
interfaceType: appState?.interfaceType,
|
|
64
|
+
scale: MAP_SIZE_SCALES[mapState?.mapSize],
|
|
65
|
+
featureId,
|
|
66
|
+
getSnapEnabled: () => draw.isSnapEnabled()
|
|
67
|
+
})
|
|
68
|
+
const editing = draw.get(featureId)
|
|
69
|
+
dispatch({ type: 'SET_FEATURE', payload: { feature: editing, tempFeature: editing } })
|
|
70
|
+
dispatch({ type: 'SET_MODE', payload: EDIT_VERTEX_MODE })
|
|
71
|
+
dispatch({ type: 'SET_GEOMETRY_VALID', payload: false })
|
|
72
|
+
draw.setGeometryValid?.(false)
|
|
73
|
+
draw.setInvalid?.(true)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createHandlers ({ appState, appConfig, mapState, pluginState, mapProvider, eventBus, hints, resetState }) {
|
|
77
|
+
const { draw } = mapProvider
|
|
78
|
+
const emitGeometryInvalid = createGeometryInvalidEmitter(eventBus, hints)
|
|
79
|
+
const { feature, tempFeature } = pluginState
|
|
80
|
+
const { dispatch } = pluginState
|
|
81
|
+
// A shape that finished invalid and was re-opened in edit mode: its eventual
|
|
82
|
+
// edit-finish must report as a creation, not an edit.
|
|
83
|
+
let pendingCreateId = null
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
handleDone: () => { draw.done() },
|
|
87
|
+
handleCancel: () => {
|
|
88
|
+
const mode = draw.getMode()
|
|
89
|
+
if (mode === EDIT_VERTEX_MODE && tempFeature?.id) { draw.add(feature) }
|
|
90
|
+
pendingCreateId = null
|
|
91
|
+
draw.cancel(); resetState()
|
|
92
|
+
eventBus.emit('draw:cancelled', feature)
|
|
93
|
+
},
|
|
94
|
+
handleUndo: () => draw.undo(),
|
|
95
|
+
handleDeleteVertex: () => draw.deleteVertex(),
|
|
96
|
+
handleSnap: () => {
|
|
97
|
+
pluginState.dispatch({ type: 'TOGGLE_SNAP' })
|
|
98
|
+
draw.setSnapEnabled(!pluginState.snap)
|
|
99
|
+
},
|
|
100
|
+
onCreate: (f) => {
|
|
101
|
+
// A shape can be finished by the Done button OR a map gesture (double-click,
|
|
102
|
+
// clicking the first vertex, Enter). Only the button is gated, so re-validate
|
|
103
|
+
// here to catch the gesture paths — an invalid shape must never be finalised.
|
|
104
|
+
const { valid, reason } = validateGeometry(f, { phase: 'create', mode: draw.getMode() }, { onGeometryChange: draw._geometryValidator })
|
|
105
|
+
if (!valid) {
|
|
106
|
+
pendingCreateId = f.id
|
|
107
|
+
emitGeometryInvalid({ feature: f, reason, phase: 'create', mode: EDIT_VERTEX_MODE })
|
|
108
|
+
setTimeout(() => enterEditVertexMode({ draw, appState, appConfig, mapState, dispatch }, f.id), 0)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
resetState(); setTimeout(() => draw.changeMode('disabled'), 0); eventBus.emit('draw:created', f)
|
|
112
|
+
},
|
|
113
|
+
onEditFinish: (f) => {
|
|
114
|
+
resetState()
|
|
115
|
+
setTimeout(() => draw.changeMode('disabled'), 0)
|
|
116
|
+
// A shape that was drawn-then-fixed reports as a creation, not an edit.
|
|
117
|
+
if (pendingCreateId && f.id === pendingCreateId) {
|
|
118
|
+
pendingCreateId = null
|
|
119
|
+
eventBus.emit('draw:created', f)
|
|
120
|
+
} else {
|
|
121
|
+
eventBus.emit('draw:edited', f)
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
onCancel: () => {},
|
|
125
|
+
...createVertexSelectionHandlers({ draw, pluginState, mapProvider, eventBus }),
|
|
126
|
+
onUndoChange: (l) => { pluginState.dispatch({ type: 'SET_UNDO_STACK_LENGTH', payload: l }) },
|
|
127
|
+
onUpdate: (f) => { eventBus.emit('draw:updated', f) },
|
|
128
|
+
onGeometryChange: (e) => {
|
|
129
|
+
// Only commit-level changes (commit-add/move/insert/delete) carry a `phase`.
|
|
130
|
+
// Preview events (e.g. split's live preview) have none and are ignored.
|
|
131
|
+
if (!e?.phase) { return }
|
|
132
|
+
|
|
133
|
+
const mode = draw.getMode()
|
|
134
|
+
const context = { phase: e.phase, vertexIndex: e.vertexIndex, mode }
|
|
135
|
+
const { valid, reason } = validateGeometry(e.feature, context, { onGeometryChange: draw._geometryValidator })
|
|
136
|
+
|
|
137
|
+
// Rules only gate the Done button — never revert. A shape can pass through
|
|
138
|
+
// interim invalid states while being built or reshaped.
|
|
139
|
+
pluginState.dispatch({ type: 'SET_GEOMETRY_VALID', payload: valid })
|
|
140
|
+
draw.setGeometryValid?.(valid)
|
|
141
|
+
// The invalid stroke is committed-validity-driven in edit mode only; in draw
|
|
142
|
+
// mode the adapters drive it live from the displayed geometry (placed
|
|
143
|
+
// vertices + cursor) on every rubber-band move.
|
|
144
|
+
if (mode === EDIT_VERTEX_MODE) {
|
|
145
|
+
draw.setInvalid?.(!valid)
|
|
146
|
+
}
|
|
147
|
+
if (!valid) {
|
|
148
|
+
emitGeometryInvalid({ reason, ...context, feature: e.feature })
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
// A vertex placement was rejected (hard rule or user callback veto). Surface it
|
|
152
|
+
// on the public bus with phase 'place', and as a hint toast, so the reason is visible.
|
|
153
|
+
onPlacementBlocked: (e) => {
|
|
154
|
+
emitGeometryInvalid(e)
|
|
155
|
+
},
|
|
156
|
+
// Live (mid-drag) validity flip while editing — the displayed shape is exactly
|
|
157
|
+
// what Done finishes there, so it gates the Done button in real time. Emitted
|
|
158
|
+
// by the adapters' edit wiring only, on flips only.
|
|
159
|
+
onValidityChange: (e) => {
|
|
160
|
+
pluginState.dispatch({ type: 'SET_GEOMETRY_VALID', payload: e.valid })
|
|
161
|
+
draw.setGeometryValid?.(e.valid)
|
|
162
|
+
},
|
|
163
|
+
// Live placement-veto flip while drawing — gates the Add point button so it
|
|
164
|
+
// never looks active when a tap would be rejected.
|
|
165
|
+
onCanPlaceChange: (e) => {
|
|
166
|
+
pluginState.dispatch({ type: 'SET_CAN_ADD_POINT', payload: e.canPlace })
|
|
167
|
+
},
|
|
168
|
+
// The mode's interface type changed (device switch mid-draw, or the final
|
|
169
|
+
// value on mode exit) — relay so the app can sync appState.interfaceType.
|
|
170
|
+
onInterfaceTypeChange: (e) => {
|
|
171
|
+
eventBus.emit('draw:interfacetypechange', { interfaceType: e.interfaceType })
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function attachButtonHandlers (buttonConfig, handlers) {
|
|
177
|
+
const { drawDone, drawCancel, drawUndo, drawDeletePoint, drawSnap } = buttonConfig
|
|
178
|
+
drawDone.onClick = handlers.handleDone
|
|
179
|
+
drawCancel.onClick = handlers.handleCancel
|
|
180
|
+
drawUndo.onClick = handlers.handleUndo
|
|
181
|
+
if (drawDeletePoint) { drawDeletePoint.onClick = handlers.handleDeleteVertex }
|
|
182
|
+
if (drawSnap) { drawSnap.onClick = handlers.handleSnap }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function attachDrawEvents (draw, handlers) {
|
|
186
|
+
draw.on(ADAPTER_EVENTS.CREATE, handlers.onCreate)
|
|
187
|
+
draw.on(ADAPTER_EVENTS.EDIT_FINISH, handlers.onEditFinish)
|
|
188
|
+
draw.on(ADAPTER_EVENTS.CANCEL, handlers.onCancel)
|
|
189
|
+
draw.on(ADAPTER_EVENTS.VERTEX_SELECTION, handlers.onVertexSelection)
|
|
190
|
+
draw.on(ADAPTER_EVENTS.VERTEX_CHANGE, handlers.onVertexChange)
|
|
191
|
+
draw.on(ADAPTER_EVENTS.UNDO_CHANGE, handlers.onUndoChange)
|
|
192
|
+
draw.on(ADAPTER_EVENTS.UPDATE, handlers.onUpdate)
|
|
193
|
+
draw.on(ADAPTER_EVENTS.GEOMETRY_CHANGE, handlers.onGeometryChange)
|
|
194
|
+
draw.on(ADAPTER_EVENTS.PLACEMENT_BLOCKED, handlers.onPlacementBlocked)
|
|
195
|
+
draw.on(ADAPTER_EVENTS.VALIDITY_CHANGE, handlers.onValidityChange)
|
|
196
|
+
draw.on(ADAPTER_EVENTS.CAN_PLACE_CHANGE, handlers.onCanPlaceChange)
|
|
197
|
+
draw.on(ADAPTER_EVENTS.INTERFACE_TYPE_CHANGE, handlers.onInterfaceTypeChange)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function detachButtonHandlers (buttonConfig) {
|
|
201
|
+
const { drawDone, drawCancel, drawUndo, drawDeletePoint, drawSnap } = buttonConfig
|
|
202
|
+
drawDone.onClick = null
|
|
203
|
+
drawCancel.onClick = null
|
|
204
|
+
drawUndo.onClick = null
|
|
205
|
+
if (drawDeletePoint) { drawDeletePoint.onClick = null }
|
|
206
|
+
if (drawSnap) { drawSnap.onClick = null }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function detachDrawEvents (draw, handlers) {
|
|
210
|
+
draw.off(ADAPTER_EVENTS.CREATE, handlers.onCreate)
|
|
211
|
+
draw.off(ADAPTER_EVENTS.EDIT_FINISH, handlers.onEditFinish)
|
|
212
|
+
draw.off(ADAPTER_EVENTS.CANCEL, handlers.onCancel)
|
|
213
|
+
draw.off(ADAPTER_EVENTS.VERTEX_SELECTION, handlers.onVertexSelection)
|
|
214
|
+
draw.off(ADAPTER_EVENTS.VERTEX_CHANGE, handlers.onVertexChange)
|
|
215
|
+
draw.off(ADAPTER_EVENTS.UNDO_CHANGE, handlers.onUndoChange)
|
|
216
|
+
draw.off(ADAPTER_EVENTS.UPDATE, handlers.onUpdate)
|
|
217
|
+
draw.off(ADAPTER_EVENTS.GEOMETRY_CHANGE, handlers.onGeometryChange)
|
|
218
|
+
draw.off(ADAPTER_EVENTS.PLACEMENT_BLOCKED, handlers.onPlacementBlocked)
|
|
219
|
+
draw.off(ADAPTER_EVENTS.VALIDITY_CHANGE, handlers.onValidityChange)
|
|
220
|
+
draw.off(ADAPTER_EVENTS.CAN_PLACE_CHANGE, handlers.onCanPlaceChange)
|
|
221
|
+
draw.off(ADAPTER_EVENTS.INTERFACE_TYPE_CHANGE, handlers.onInterfaceTypeChange)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function attachEvents ({ appState, appConfig, mapState, pluginState, mapProvider, buttonConfig, eventBus, hints }) {
|
|
225
|
+
const { draw } = mapProvider
|
|
226
|
+
const resetState = () => {
|
|
227
|
+
pluginState.dispatch({ type: 'SET_MODE', payload: null })
|
|
228
|
+
pluginState.dispatch({ type: 'SET_FEATURE', payload: { feature: null, tempFeature: null } })
|
|
229
|
+
// Release MoveControl's D-pad back to panning the map whenever a draw/edit
|
|
230
|
+
// session ends — a stale claim here would silently hijack it for good.
|
|
231
|
+
mapProvider.activeMoveTarget = null
|
|
232
|
+
}
|
|
233
|
+
const handlers = createHandlers({ appState, appConfig, mapState, pluginState, mapProvider, eventBus, hints, resetState })
|
|
234
|
+
attachButtonHandlers(buttonConfig, handlers)
|
|
235
|
+
attachDrawEvents(draw, handlers)
|
|
236
|
+
return () => { detachButtonHandlers(buttonConfig); detachDrawEvents(draw, handlers) }
|
|
237
|
+
}
|