@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,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCoords,
|
|
3
|
+
getRingSegments,
|
|
4
|
+
getSegmentForIndex,
|
|
5
|
+
getModifiableCoords
|
|
6
|
+
} from './geometryHelpers.js'
|
|
7
|
+
import { KEYBOARD } from '../../defaults.js'
|
|
8
|
+
import {
|
|
9
|
+
getSnapInstance, isSnapActive, isSnapEnabled, getSnapLngLat,
|
|
10
|
+
getSnapRadius, triggerSnapAtPoint, clearSnapIndicator
|
|
11
|
+
} from '../../utils/snapHelpers.js'
|
|
12
|
+
import { MIN_VERTICES } from '../../../../validation/rules.js'
|
|
13
|
+
|
|
14
|
+
const ARROW_OFFSETS = { ArrowUp: [0, -1], ArrowDown: [0, 1], ArrowLeft: [-1, 0], ArrowRight: [1, 0] }
|
|
15
|
+
|
|
16
|
+
export const vertexOperations = {
|
|
17
|
+
updateMidpoint (coordinates) {
|
|
18
|
+
setTimeout(() => {
|
|
19
|
+
this.map.getSource('mapbox-gl-draw-hot').setData({
|
|
20
|
+
type: 'Feature',
|
|
21
|
+
properties: { meta: 'midpoint', active: 'true', id: 'active-midpoint' },
|
|
22
|
+
geometry: { type: 'Point', coordinates }
|
|
23
|
+
})
|
|
24
|
+
}, 0)
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
updateVertex (state, direction) {
|
|
28
|
+
const [idx, type] = this.getVertexOrMidpoint(state, direction)
|
|
29
|
+
if (idx < 0 || !type) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
this.changeMode(state, { selectedVertexIndex: idx, selectedVertexType: type, ...(type === 'vertex' && { coordPath: this.getCoordPath(state, idx) }) })
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
getOffset (coord, e) {
|
|
36
|
+
const pt = this.map.project(coord)
|
|
37
|
+
const offset = e?.shiftKey ? KEYBOARD.nudgeAmount : KEYBOARD.stepAmount
|
|
38
|
+
const [dx, dy] = e ? ARROW_OFFSETS[e.key].map(v => v * offset) : [0, 0]
|
|
39
|
+
return this.map.unproject({ x: pt.x + dx, y: pt.y + dy })
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
getNewCoord (state, e) {
|
|
43
|
+
return this.getOffset(getCoords(this.getFeature(state.featureId))[state.selectedVertexIndex], e)
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Explicit-delta counterpart to getOffset, driven by a unit direction vector and
|
|
47
|
+
// MoveControl's own Precision toggle rather than a KeyboardEvent — used by
|
|
48
|
+
// nudgeVertexByDelta below.
|
|
49
|
+
getOffsetByDelta (coord, dx, dy, isLargeStep) {
|
|
50
|
+
const pt = this.map.project(coord)
|
|
51
|
+
const amount = isLargeStep ? KEYBOARD.stepAmount : KEYBOARD.nudgeAmount
|
|
52
|
+
return this.map.unproject({ x: pt.x + dx * amount, y: pt.y + dy * amount })
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// Resolves the destination coordinate for a vertex nudge by (dx, dy) unit
|
|
56
|
+
// direction, applying snap or breaking out of an already-active one — shared by
|
|
57
|
+
// the keyboard arrow-key path (_keyboardMoveTarget) and MoveControl's
|
|
58
|
+
// explicit-delta path (nudgeVertexByDelta) so both snap identically. dx/dy are
|
|
59
|
+
// only needed for the snap-escape offset; getCandidate computes the raw,
|
|
60
|
+
// un-snapped destination the caller would otherwise have used.
|
|
61
|
+
resolveSnapTarget (state, dx, dy, currentCoord, getCandidate) {
|
|
62
|
+
const snap = getSnapInstance(this.map)
|
|
63
|
+
|
|
64
|
+
// Break out of an active snap by moving beyond the snap radius
|
|
65
|
+
if (isSnapEnabled(state) && state._isSnapped && snap) {
|
|
66
|
+
const offset = getSnapRadius(snap) + 1
|
|
67
|
+
const pt = this.map.project(currentCoord)
|
|
68
|
+
state._isSnapped = false
|
|
69
|
+
clearSnapIndicator(snap, this.map)
|
|
70
|
+
return this.map.unproject({ x: pt.x + dx * offset, y: pt.y + dy * offset })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const newCoord = getCandidate()
|
|
74
|
+
if (isSnapEnabled(state) && snap) {
|
|
75
|
+
triggerSnapAtPoint(snap, this.map, this.map.project(newCoord))
|
|
76
|
+
if (isSnapActive(snap)) {
|
|
77
|
+
state._isSnapped = true
|
|
78
|
+
return getSnapLngLat(snap)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
state._isSnapped = false
|
|
82
|
+
return newCoord
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Moves the selected vertex by an explicit (dx, dy) unit direction — the entry
|
|
86
|
+
// point for MoveControl's D-pad (see mapProvider.activeMoveTarget in events.js),
|
|
87
|
+
// as opposed to moveVertexByKey's KeyboardEvent-driven path. Each call is treated
|
|
88
|
+
// as one complete, undoable action (no held-key sequencing, since a button click
|
|
89
|
+
// has no "held" state to batch the way arrow keys do) — but still honours snap
|
|
90
|
+
// via the shared resolveSnapTarget, same as keyboard nudging.
|
|
91
|
+
nudgeVertexByDelta (state, dx, dy, isLargeStep) {
|
|
92
|
+
if (state.selectedVertexType !== 'vertex' || state.selectedVertexIndex < 0) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
const feature = this.getFeature(state.featureId)
|
|
96
|
+
const currentCoord = feature && getCoords(feature)?.[state.selectedVertexIndex]
|
|
97
|
+
if (!currentCoord) {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
const previousPosition = [...currentCoord]
|
|
101
|
+
const vertexIndex = state.selectedVertexIndex
|
|
102
|
+
const target = this.resolveSnapTarget(state, dx, dy, currentCoord, () => this.getOffsetByDelta(currentCoord, dx, dy, isLargeStep))
|
|
103
|
+
this.moveVertex(state, target)
|
|
104
|
+
this.pushUndo({ type: 'move_vertex', featureId: state.featureId, vertexIndex, previousPosition })
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
insertVertex (state, e) {
|
|
108
|
+
const midIdx = state.selectedVertexIndex - state.vertecies.length
|
|
109
|
+
const newCoord = this.getOffset(state.midpoints[midIdx], e)
|
|
110
|
+
const feature = this.getFeature(state.featureId)
|
|
111
|
+
const geojson = feature.toGeoJSON()
|
|
112
|
+
|
|
113
|
+
// Find which segment this midpoint belongs to and calculate insertion position
|
|
114
|
+
const segments = getRingSegments(feature)
|
|
115
|
+
let globalInsertIdx = midIdx + 1
|
|
116
|
+
let insertSegment = null
|
|
117
|
+
let localInsertIdx = 0
|
|
118
|
+
|
|
119
|
+
// Map midpoint index to segment and local position
|
|
120
|
+
let midpointCounter = 0
|
|
121
|
+
for (const seg of segments) {
|
|
122
|
+
// Must match getMidpoints calculation
|
|
123
|
+
const segMidpoints = seg.closed ? seg.length : seg.length - 1
|
|
124
|
+
if (midIdx < midpointCounter + segMidpoints) {
|
|
125
|
+
insertSegment = seg
|
|
126
|
+
localInsertIdx = (midIdx - midpointCounter) + 1
|
|
127
|
+
globalInsertIdx = seg.start + localInsertIdx
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
midpointCounter += segMidpoints
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!insertSegment) { return }
|
|
134
|
+
|
|
135
|
+
const coords = getModifiableCoords(geojson, insertSegment.path)
|
|
136
|
+
coords.splice(localInsertIdx, 0, [newCoord.lng, newCoord.lat])
|
|
137
|
+
this._ctx.api.add(geojson)
|
|
138
|
+
|
|
139
|
+
this.pushUndo({ type: 'insert_vertex', featureId: state.featureId, vertexIndex: globalInsertIdx })
|
|
140
|
+
this.changeMode(state, { selectedVertexIndex: globalInsertIdx, selectedVertexType: 'vertex', coordPath: this.getCoordPath(state, globalInsertIdx) })
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
moveVertex (state, coord, options = {}) {
|
|
144
|
+
if (options.checkSnap && state.enableSnap !== false) {
|
|
145
|
+
const snap = this.map._snapInstance
|
|
146
|
+
if (snap?.snapStatus && snap.snapCoords?.length >= 2) {
|
|
147
|
+
coord = { lng: snap.snapCoords[0], lat: snap.snapCoords[1] }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const feature = this.getFeature(state.featureId)
|
|
152
|
+
const geojson = feature.toGeoJSON()
|
|
153
|
+
const segments = getRingSegments(feature)
|
|
154
|
+
const result = getSegmentForIndex(segments, state.selectedVertexIndex)
|
|
155
|
+
if (!result) { return }
|
|
156
|
+
|
|
157
|
+
const coords = getModifiableCoords(geojson, result.segment.path)
|
|
158
|
+
coords[result.localIdx] = [coord.lng, coord.lat]
|
|
159
|
+
this._ctx.api.add(geojson)
|
|
160
|
+
state.vertecies = this.getVerticies(state.featureId)
|
|
161
|
+
|
|
162
|
+
this.map.fire('draw.geometrychange', state.feature)
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
deleteVertex (state) {
|
|
166
|
+
const feature = this.getFeature(state.featureId)
|
|
167
|
+
if (!feature) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const segments = getRingSegments(feature)
|
|
172
|
+
const result = getSegmentForIndex(segments, state.selectedVertexIndex)
|
|
173
|
+
if (!result) {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const { segment } = result
|
|
178
|
+
// Minimum vertices per segment (mapbox-gl-draw's internal representation is
|
|
179
|
+
// a closed ring for polygons, an open path for lines) — MIN_VERTICES is the
|
|
180
|
+
// single source for this threshold (validation/rules.js).
|
|
181
|
+
const minVertices = segment.closed ? MIN_VERTICES.Polygon : MIN_VERTICES.LineString
|
|
182
|
+
if (segment.length <= minVertices) {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Save position for undo before deletion
|
|
187
|
+
const deletedPosition = [...state.vertecies[state.selectedVertexIndex]]
|
|
188
|
+
const deletedIndex = state.selectedVertexIndex
|
|
189
|
+
|
|
190
|
+
// Remove the coordinate directly rather than via this._ctx.api.trash(), which routes through
|
|
191
|
+
// DirectSelect.trash() and calls onSetup({ featureId }) with incomplete options, crashing event registration.
|
|
192
|
+
const coordPath = [...result.segment.path, result.localIdx].join('.')
|
|
193
|
+
feature.removeCoordinate(coordPath)
|
|
194
|
+
this.fireUpdate()
|
|
195
|
+
this.clearSelectedCoordinates()
|
|
196
|
+
feature.changed()
|
|
197
|
+
this._ctx.store.render()
|
|
198
|
+
|
|
199
|
+
// Push undo operation
|
|
200
|
+
this.pushUndo({
|
|
201
|
+
type: 'delete_vertex',
|
|
202
|
+
featureId: state.featureId,
|
|
203
|
+
vertexIndex: deletedIndex,
|
|
204
|
+
position: deletedPosition
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// Clear selection after delete
|
|
208
|
+
this.changeMode(state, { selectedVertexIndex: -1, selectedVertexType: null })
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { createHarness, LINE } from './__helpers__/harness.js'
|
|
2
|
+
|
|
3
|
+
describe('vertexOperations', () => {
|
|
4
|
+
test('updateMidpoint pushes point data to the hot source', () => {
|
|
5
|
+
jest.useFakeTimers()
|
|
6
|
+
const { ctx, map } = createHarness()
|
|
7
|
+
ctx.updateMidpoint([1, 2])
|
|
8
|
+
jest.runAllTimers()
|
|
9
|
+
expect(map.getSource).toHaveBeenCalledWith('mapbox-gl-draw-hot')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('updateVertex changes mode for a valid target and no-ops otherwise', () => {
|
|
13
|
+
const { ctx, state, api } = createHarness()
|
|
14
|
+
state.selectedVertexIndex = 0
|
|
15
|
+
ctx.updateVertex(state, 'ArrowRight')
|
|
16
|
+
expect(api.changeMode).toHaveBeenCalledWith('edit_vertex', expect.objectContaining({
|
|
17
|
+
selectedVertexIndex: expect.any(Number)
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
api.changeMode.mockClear()
|
|
21
|
+
ctx.updateVertex({ featureId: 'missing', vertecies: [], selectedVertexIndex: -1 }, 'ArrowRight')
|
|
22
|
+
expect(api.changeMode).not.toHaveBeenCalled()
|
|
23
|
+
|
|
24
|
+
// A vertex target carries a coordPath; a midpoint target does not
|
|
25
|
+
jest.spyOn(ctx, 'getVertexOrMidpoint').mockReturnValueOnce([1, 'vertex'])
|
|
26
|
+
ctx.updateVertex(state, 'ArrowRight')
|
|
27
|
+
expect(api.changeMode.mock.calls.at(-1)[1].coordPath).toBeDefined()
|
|
28
|
+
jest.spyOn(ctx, 'getVertexOrMidpoint').mockReturnValueOnce([5, 'midpoint'])
|
|
29
|
+
ctx.updateVertex(state, 'ArrowRight')
|
|
30
|
+
expect(api.changeMode.mock.calls.at(-1)[1].coordPath).toBeUndefined()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('getOffset applies step/nudge amounts and returns the coord unchanged without an event', () => {
|
|
34
|
+
const { ctx } = createHarness()
|
|
35
|
+
const stepped = ctx.getOffset([5, 5], { key: 'ArrowRight', shiftKey: false })
|
|
36
|
+
const nudged = ctx.getOffset([5, 5], { key: 'ArrowRight', shiftKey: true })
|
|
37
|
+
expect(stepped.lng).not.toBe(5)
|
|
38
|
+
expect(Math.abs(nudged.lng - 5)).toBeLessThan(Math.abs(stepped.lng - 5))
|
|
39
|
+
expect(ctx.getOffset([5, 5], null)).toEqual({ lng: 5, lat: 5 })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('getNewCoord offsets the currently selected vertex', () => {
|
|
43
|
+
const { ctx, state } = createHarness()
|
|
44
|
+
state.selectedVertexIndex = 1
|
|
45
|
+
expect(ctx.getNewCoord(state, { key: 'ArrowRight', shiftKey: false })).toHaveProperty('lng')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('getOffsetByDelta applies step/nudge amounts along an explicit direction, mirroring getOffset\'s shiftKey polarity', () => {
|
|
49
|
+
const { ctx } = createHarness()
|
|
50
|
+
const large = ctx.getOffsetByDelta([5, 5], 1, 0, true)
|
|
51
|
+
const small = ctx.getOffsetByDelta([5, 5], 1, 0, false)
|
|
52
|
+
expect(large.lng).not.toBe(5)
|
|
53
|
+
expect(Math.abs(small.lng - 5)).toBeLessThan(Math.abs(large.lng - 5))
|
|
54
|
+
expect(ctx.getOffsetByDelta([5, 5], 0, 0, true)).toEqual({ lng: 5, lat: 5 })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('nudgeVertexByDelta moves the selected vertex and pushes a single undo entry, or no-ops for a midpoint/no selection', () => {
|
|
58
|
+
const { ctx, state, map } = createHarness()
|
|
59
|
+
state.selectedVertexIndex = 0
|
|
60
|
+
state.selectedVertexType = 'vertex'
|
|
61
|
+
const before = [...state.vertecies[0]]
|
|
62
|
+
ctx.nudgeVertexByDelta(state, 1, 0, true)
|
|
63
|
+
expect(state.vertecies[0]).not.toEqual(before)
|
|
64
|
+
expect(map._undoStack.pop()).toMatchObject({ type: 'move_vertex', vertexIndex: 0, previousPosition: before })
|
|
65
|
+
|
|
66
|
+
map._undoStack.clear()
|
|
67
|
+
ctx.nudgeVertexByDelta({ ...state, selectedVertexType: 'midpoint' }, 1, 0, true)
|
|
68
|
+
ctx.nudgeVertexByDelta({ ...state, selectedVertexIndex: -1 }, 1, 0, true)
|
|
69
|
+
// Passes the type/index guard but resolves to no coordinate — missing feature,
|
|
70
|
+
// and a valid feature with an out-of-range index.
|
|
71
|
+
ctx.nudgeVertexByDelta({ ...state, featureId: 'missing' }, 1, 0, true)
|
|
72
|
+
ctx.nudgeVertexByDelta({ ...state, selectedVertexIndex: 99 }, 1, 0, true)
|
|
73
|
+
expect(map._undoStack).toHaveLength(0)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('nudgeVertexByDelta snaps to a nearby target, breaks out of an active snap, and falls back to the raw coord when snap is inactive — same as keyboard nudging (regression: MoveControl bypassed snap entirely before resolveSnapTarget was shared)', () => {
|
|
77
|
+
const { ctx, state, map } = createHarness()
|
|
78
|
+
state.selectedVertexIndex = 1
|
|
79
|
+
state.selectedVertexType = 'vertex'
|
|
80
|
+
state.getSnapEnabled = () => true
|
|
81
|
+
|
|
82
|
+
map._snapInstance = { status: true, snapStatus: true, snapCoords: [7, 8], snapToClosestPoint: jest.fn() }
|
|
83
|
+
ctx.nudgeVertexByDelta(state, 0, -1, true)
|
|
84
|
+
expect(state._isSnapped).toBe(true)
|
|
85
|
+
expect(state.vertecies[1]).toEqual([7, 8])
|
|
86
|
+
|
|
87
|
+
ctx.nudgeVertexByDelta(state, -1, 0, true) // already snapped → breaks out of the snap radius
|
|
88
|
+
expect(state._isSnapped).toBe(false)
|
|
89
|
+
|
|
90
|
+
map._snapInstance = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
|
|
91
|
+
ctx.nudgeVertexByDelta(state, 1, 0, true) // snap enabled but inactive → raw offset coord
|
|
92
|
+
expect(state._isSnapped).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('resolveSnapTarget is a no-op passthrough to the candidate coordinate when snap is disabled', () => {
|
|
96
|
+
const { ctx, state } = createHarness()
|
|
97
|
+
const candidate = { lng: 3, lat: 4 }
|
|
98
|
+
expect(ctx.resolveSnapTarget(state, 1, 0, [0, 0], () => candidate)).toEqual(candidate)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('insertVertex splits a midpoint into a new vertex, records undo and selects it', () => {
|
|
102
|
+
const { ctx, state, api } = createHarness()
|
|
103
|
+
ctx.insertVertex({ ...state, selectedVertexIndex: state.vertecies.length, selectedVertexType: 'midpoint' }, { key: 'ArrowRight', shiftKey: false })
|
|
104
|
+
expect(api.add).toHaveBeenCalled()
|
|
105
|
+
expect(state.map ?? ctx.map._undoStack.length).toBeGreaterThan(0)
|
|
106
|
+
expect(api.changeMode).toHaveBeenCalledWith('edit_vertex', expect.objectContaining({
|
|
107
|
+
selectedVertexType: 'vertex'
|
|
108
|
+
}))
|
|
109
|
+
|
|
110
|
+
// Also works for an open line segment
|
|
111
|
+
const line = createHarness(LINE())
|
|
112
|
+
line.ctx.insertVertex({ ...line.state, selectedVertexIndex: line.state.vertecies.length, selectedVertexType: 'midpoint' }, { key: 'ArrowRight', shiftKey: false })
|
|
113
|
+
expect(line.api.add).toHaveBeenCalled()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('insertVertex bails out when the midpoint maps to no segment', () => {
|
|
117
|
+
const { ctx, state, api } = createHarness()
|
|
118
|
+
// midIdx beyond the segment's midpoint count, but with a defined midpoint coord
|
|
119
|
+
const badState = { ...state, selectedVertexIndex: state.vertecies.length + 4, midpoints: [...state.midpoints, [5, 5]] }
|
|
120
|
+
api.add.mockClear()
|
|
121
|
+
ctx.insertVertex(badState, { key: 'ArrowRight', shiftKey: false })
|
|
122
|
+
expect(api.add).not.toHaveBeenCalled()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('moveVertex repositions a vertex, applies snap and guards an out-of-range index', () => {
|
|
126
|
+
const { ctx, state, map } = createHarness()
|
|
127
|
+
state.selectedVertexIndex = 0
|
|
128
|
+
ctx.moveVertex(state, { lng: 1, lat: 2 })
|
|
129
|
+
expect(state.vertecies[0]).toEqual([1, 2])
|
|
130
|
+
|
|
131
|
+
map._snapInstance = { snapStatus: true, snapCoords: [7, 8] }
|
|
132
|
+
ctx.moveVertex(state, { lng: 1, lat: 2 }, { checkSnap: true })
|
|
133
|
+
expect(state.vertecies[0]).toEqual([7, 8])
|
|
134
|
+
|
|
135
|
+
const before = [...state.vertecies]
|
|
136
|
+
ctx.moveVertex({ ...state, selectedVertexIndex: 99 }, { lng: 3, lat: 3 })
|
|
137
|
+
expect(ctx.getVerticies('feat-1')).toEqual(before)
|
|
138
|
+
|
|
139
|
+
// checkSnap requested but no active snap → coordinate used as-is
|
|
140
|
+
const h = createHarness()
|
|
141
|
+
h.ctx.moveVertex({ ...h.state, selectedVertexIndex: 0 }, { lng: 1, lat: 2 }, { checkSnap: true })
|
|
142
|
+
expect(h.ctx.getVerticies('feat-1')[0]).toEqual([1, 2])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('deleteVertex removes a vertex, or no-ops for missing feature / bad index / minimum size', () => {
|
|
146
|
+
const { ctx, state, api } = createHarness()
|
|
147
|
+
ctx.deleteVertex({ ...state, featureId: 'missing' })
|
|
148
|
+
ctx.deleteVertex({ ...state, selectedVertexIndex: 99 })
|
|
149
|
+
expect(api.changeMode).not.toHaveBeenCalled()
|
|
150
|
+
|
|
151
|
+
const triangle = createHarness({ type: 'Polygon', coordinates: [[[0, 0], [10, 0], [5, 10], [0, 0]]] })
|
|
152
|
+
triangle.state.selectedVertexIndex = 0
|
|
153
|
+
triangle.ctx.deleteVertex(triangle.state) // 3 vertices → at minimum, rejected
|
|
154
|
+
expect(triangle.api.changeMode).not.toHaveBeenCalled()
|
|
155
|
+
|
|
156
|
+
state.selectedVertexIndex = 1
|
|
157
|
+
ctx.deleteVertex(state)
|
|
158
|
+
expect(ctx.map._undoStack.pop()).toMatchObject({ type: 'delete_vertex', vertexIndex: 1 })
|
|
159
|
+
expect(api.changeMode).toHaveBeenCalledWith('edit_vertex', expect.objectContaining({ selectedVertexIndex: -1 }))
|
|
160
|
+
|
|
161
|
+
// Also works on an open line segment
|
|
162
|
+
const line = createHarness(LINE())
|
|
163
|
+
line.state.selectedVertexIndex = 1
|
|
164
|
+
line.ctx.deleteVertex(line.state)
|
|
165
|
+
expect(line.ctx.getVerticies('feat-1')).toHaveLength(2)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCoords,
|
|
3
|
+
getRingSegments,
|
|
4
|
+
getSegmentForIndex
|
|
5
|
+
} from './geometryHelpers.js'
|
|
6
|
+
import { spatialNavigate } from '../../../../utils/spatial.js'
|
|
7
|
+
|
|
8
|
+
export const vertexQueries = {
|
|
9
|
+
findVertexIndex (coords, targetCoord, currentIdx) {
|
|
10
|
+
// Search for vertex, preferring matches near currentIdx to handle duplicate coords (e.g., closing vertices)
|
|
11
|
+
const matches = []
|
|
12
|
+
coords.forEach((c, i) => {
|
|
13
|
+
if (c[0] === targetCoord[0] && c[1] === targetCoord[1]) {
|
|
14
|
+
matches.push(i)
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
if (matches.length === 0) { return -1 }
|
|
19
|
+
if (matches.length === 1) { return matches[0] }
|
|
20
|
+
|
|
21
|
+
// Multiple matches - pick closest to current selection
|
|
22
|
+
if (currentIdx >= 0) {
|
|
23
|
+
return matches.reduce((best, idx) =>
|
|
24
|
+
Math.abs(idx - currentIdx) < Math.abs(best - currentIdx) ? idx : best
|
|
25
|
+
, matches[0])
|
|
26
|
+
}
|
|
27
|
+
return matches[0]
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
getCoordPath (state, idx) {
|
|
31
|
+
const feature = this.getFeature(state.featureId)
|
|
32
|
+
if (!feature) { return '0' }
|
|
33
|
+
|
|
34
|
+
const segments = getRingSegments(feature)
|
|
35
|
+
const result = getSegmentForIndex(segments, idx)
|
|
36
|
+
if (!result) { return '0' }
|
|
37
|
+
|
|
38
|
+
const { segment, localIdx } = result
|
|
39
|
+
return [...segment.path, localIdx].join('.')
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
syncVertices (state) {
|
|
43
|
+
state.vertecies = this.getVerticies(state.featureId)
|
|
44
|
+
state.midpoints = this.getMidpoints(state.featureId)
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
getVerticies (featureId) {
|
|
48
|
+
return getCoords(this.getFeature(featureId))
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
getMidpoints (featureId) {
|
|
52
|
+
const feature = this.getFeature(featureId)
|
|
53
|
+
const coords = getCoords(feature)
|
|
54
|
+
const segments = getRingSegments(feature)
|
|
55
|
+
if (!coords?.length || !segments.length) {
|
|
56
|
+
return []
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const midpoints = []
|
|
60
|
+
// Create midpoints within each segment, respecting boundaries
|
|
61
|
+
for (const seg of segments) {
|
|
62
|
+
// For closed rings, create midpoint between every vertex including last→first
|
|
63
|
+
// For open lines, create midpoints only between consecutive vertices (no wrap-around)
|
|
64
|
+
const count = seg.closed ? seg.length : seg.length - 1
|
|
65
|
+
for (let i = 0; i < count; i++) {
|
|
66
|
+
const idx = seg.start + i
|
|
67
|
+
const nextIdx = seg.start + ((i + 1) % seg.length)
|
|
68
|
+
const [x1, y1] = coords[idx]
|
|
69
|
+
const [x2, y2] = coords[nextIdx]
|
|
70
|
+
midpoints.push([(x1 + x2) / 2, (y1 + y2) / 2])
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return midpoints
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
getVertexOrMidpoint (state, direction) {
|
|
77
|
+
// Ensure vertices and midpoints are populated
|
|
78
|
+
if (!state.vertecies?.length) {
|
|
79
|
+
state.vertecies = this.getVerticies(state.featureId)
|
|
80
|
+
state.midpoints = this.getMidpoints(state.featureId)
|
|
81
|
+
}
|
|
82
|
+
if (!state.vertecies?.length) {
|
|
83
|
+
return [-1, null]
|
|
84
|
+
}
|
|
85
|
+
const project = (p) => p ? Object.values(this.map.project(p)) : null
|
|
86
|
+
const pixels = [...state.vertecies.map(project), ...state.midpoints.map(project)].filter(Boolean)
|
|
87
|
+
if (!pixels.length) {
|
|
88
|
+
return [-1, null]
|
|
89
|
+
}
|
|
90
|
+
const start = pixels[state.selectedVertexIndex] || Object.values(this.map.project(this.map.getCenter()))
|
|
91
|
+
const idx = spatialNavigate(start, pixels, direction)
|
|
92
|
+
return [idx, idx < state.vertecies.length ? 'vertex' : 'midpoint']
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
getVertexIndexFromMidpoint (state, coordPath) {
|
|
96
|
+
const feature = this.getFeature(state.featureId)
|
|
97
|
+
const segments = getRingSegments(feature)
|
|
98
|
+
const parts = coordPath.split('.').map(Number)
|
|
99
|
+
|
|
100
|
+
// Find which segment this coord_path belongs to
|
|
101
|
+
let midpointOffset = 0
|
|
102
|
+
for (const seg of segments) {
|
|
103
|
+
const pathMatches = seg.path.every((val, idx) => val === parts[idx])
|
|
104
|
+
if (pathMatches && parts.length === seg.path.length + 1) {
|
|
105
|
+
// In DirectSelect, midpoint coord_path represents the insertion index
|
|
106
|
+
// The midpoint between vertex N and N+1 has coord_path ending in N+1
|
|
107
|
+
// So our flat midpoint index is one less than the coord_path index
|
|
108
|
+
const insertionIdx = parts[parts.length - 1]
|
|
109
|
+
const localMidpointIdx = insertionIdx > 0 ? insertionIdx - 1 : seg.length - 2
|
|
110
|
+
// Midpoints are indexed after all vertices
|
|
111
|
+
return state.vertecies.length + midpointOffset + localMidpointIdx
|
|
112
|
+
}
|
|
113
|
+
// Count midpoints in this segment (must match getMidpoints calculation)
|
|
114
|
+
const segMidpoints = seg.closed ? seg.length : seg.length - 1
|
|
115
|
+
midpointOffset += segMidpoints
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Fallback
|
|
119
|
+
return state.vertecies.length
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHarness, LINE } from './__helpers__/harness.js'
|
|
2
|
+
|
|
3
|
+
describe('vertexQueries', () => {
|
|
4
|
+
test('findVertexIndex handles no, single and duplicate matches', () => {
|
|
5
|
+
const { ctx } = createHarness()
|
|
6
|
+
expect(ctx.findVertexIndex([[0, 0], [1, 1]], [9, 9], -1)).toBe(-1)
|
|
7
|
+
expect(ctx.findVertexIndex([[0, 0], [1, 1]], [1, 1], -1)).toBe(1)
|
|
8
|
+
expect(ctx.findVertexIndex([[0, 0], [1, 1], [0, 0]], [0, 0], 2)).toBe(2)
|
|
9
|
+
expect(ctx.findVertexIndex([[0, 0], [1, 1], [0, 0]], [0, 0], -1)).toBe(0)
|
|
10
|
+
expect(ctx.findVertexIndex([[0, 0], [1, 1], [0, 0]], [0, 0], 0)).toBe(0)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('getCoordPath resolves an index, or returns "0" without a feature or segment', () => {
|
|
14
|
+
const { ctx, state } = createHarness()
|
|
15
|
+
expect(ctx.getCoordPath(state, 2)).toBe('0.2')
|
|
16
|
+
expect(ctx.getCoordPath(state, 99)).toBe('0')
|
|
17
|
+
expect(ctx.getCoordPath({ featureId: 'missing' }, 0)).toBe('0')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('getVerticies / getMidpoints for closed rings, open lines and missing features', () => {
|
|
21
|
+
const { ctx } = createHarness()
|
|
22
|
+
expect(ctx.getVerticies('feat-1')).toHaveLength(4)
|
|
23
|
+
expect(ctx.getMidpoints('feat-1')).toHaveLength(4)
|
|
24
|
+
expect(ctx.getMidpoints('missing')).toEqual([])
|
|
25
|
+
const line = createHarness(LINE())
|
|
26
|
+
expect(line.ctx.getMidpoints('feat-1')).toHaveLength(2)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('syncVertices refreshes the cached vertex and midpoint lists', () => {
|
|
30
|
+
const { ctx, state } = createHarness()
|
|
31
|
+
state.vertecies = []
|
|
32
|
+
state.midpoints = []
|
|
33
|
+
ctx.syncVertices(state)
|
|
34
|
+
expect(state.vertecies).toHaveLength(4)
|
|
35
|
+
expect(state.midpoints).toHaveLength(4)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('getVertexOrMidpoint repopulates empty lists, navigates, and guards empty features', () => {
|
|
39
|
+
const { ctx, state } = createHarness()
|
|
40
|
+
state.vertecies = []
|
|
41
|
+
state.selectedVertexIndex = 0
|
|
42
|
+
const [idx, type] = ctx.getVertexOrMidpoint(state, 'ArrowRight')
|
|
43
|
+
expect(idx).toBeGreaterThanOrEqual(0)
|
|
44
|
+
expect(['vertex', 'midpoint']).toContain(type)
|
|
45
|
+
|
|
46
|
+
expect(ctx.getVertexOrMidpoint({ featureId: 'missing', vertecies: [] }, 'ArrowRight')).toEqual([-1, null])
|
|
47
|
+
expect(ctx.getVertexOrMidpoint({ ...state, vertecies: [null], midpoints: [], selectedVertexIndex: 0 }, 'ArrowRight')).toEqual([-1, null])
|
|
48
|
+
|
|
49
|
+
// Falls back to the map centre when the current index has no pixel, and can resolve a vertex target
|
|
50
|
+
const fresh = createHarness()
|
|
51
|
+
expect(fresh.ctx.getVertexOrMidpoint({ ...fresh.state, selectedVertexIndex: -1 }, 'ArrowRight')[0]).toBeGreaterThanOrEqual(0)
|
|
52
|
+
const types = new Set()
|
|
53
|
+
for (let s = 0; s < fresh.state.vertecies.length + fresh.state.midpoints.length; s++) {
|
|
54
|
+
for (const d of ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown']) {
|
|
55
|
+
types.add(fresh.ctx.getVertexOrMidpoint({ ...fresh.state, selectedVertexIndex: s }, d)[1])
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
expect(types.has('vertex')).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('getVertexIndexFromMidpoint maps coord paths (insertion index, wrap-around and fallback)', () => {
|
|
62
|
+
const { ctx, state, features } = createHarness()
|
|
63
|
+
expect(ctx.getVertexIndexFromMidpoint(state, '0.2')).toBe(5)
|
|
64
|
+
expect(ctx.getVertexIndexFromMidpoint(state, '0.0')).toBe(6)
|
|
65
|
+
expect(ctx.getVertexIndexFromMidpoint(state, '5.0')).toBe(4)
|
|
66
|
+
|
|
67
|
+
// Counts earlier segments for multi-part geometry (closed and open)
|
|
68
|
+
features.set('multipoly', { type: 'MultiPolygon', coordinates: [[[[0, 0], [1, 0], [1, 1], [0, 0]]], [[[5, 5], [6, 5], [6, 6], [5, 5]]]] })
|
|
69
|
+
expect(ctx.getVertexIndexFromMidpoint({ ...state, featureId: 'multipoly', vertecies: new Array(8).fill([0, 0]) }, '1.0.1')).toBeGreaterThan(8)
|
|
70
|
+
features.set('multiline', { type: 'MultiLineString', coordinates: [[[0, 0], [1, 1]], [[5, 5], [6, 6]]] })
|
|
71
|
+
expect(ctx.getVertexIndexFromMidpoint({ ...state, featureId: 'multiline', vertecies: new Array(4).fill([0, 0]) }, '1.1')).toBeGreaterThan(4)
|
|
72
|
+
})
|
|
73
|
+
})
|