@defra/interactive-map 0.0.37-alpha → 0.0.39-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/templates/draw-tools.njk +4 -3
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api.md +32 -0
- package/docs/examples/draw-tools.mdx +0 -4
- package/docs/plugins/draw.md +482 -0
- package/docs/plugins.md +2 -6
- package/govuk-prototype-kit.config.json +3 -2
- package/package.json +2 -4
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/css/index.css +1 -97
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/src/registry/dataset.js +3 -1
- package/plugins/datasets/src/registry/datasetRegistry.js +30 -24
- package/plugins/datasets/src/registry/datasetRegistry.test.js +25 -18
- 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,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCoords, getRingSegments, getSegmentForIndex, getModifiableCoords, coordPathToFlatIndex
|
|
3
|
+
} from './geometryHelpers.js'
|
|
4
|
+
|
|
5
|
+
const POLYGON = () => ({ type: 'Polygon', coordinates: [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]] })
|
|
6
|
+
const LINE = () => ({ type: 'LineString', coordinates: [[0, 0], [10, 0], [10, 10]] })
|
|
7
|
+
|
|
8
|
+
describe('geometryHelpers', () => {
|
|
9
|
+
test('getCoords flattens every geometry type and guards missing input', () => {
|
|
10
|
+
expect(getCoords(LINE())).toHaveLength(3)
|
|
11
|
+
expect(getCoords(POLYGON())).toHaveLength(5)
|
|
12
|
+
expect(getCoords({ type: 'MultiLineString', coordinates: [[[0, 0], [1, 1]], [[2, 2]]] })).toHaveLength(3)
|
|
13
|
+
expect(getCoords({ type: 'MultiPolygon', coordinates: [[[[0, 0], [1, 1]]], [[[2, 2]]]] })).toHaveLength(3)
|
|
14
|
+
expect(getCoords({ type: 'Point', coordinates: [0, 0] })).toEqual([])
|
|
15
|
+
expect(getCoords(null)).toEqual([])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('getRingSegments describes each geometry type and guards missing input', () => {
|
|
19
|
+
expect(getRingSegments(LINE())).toEqual([{ start: 0, length: 3, path: [], closed: false }])
|
|
20
|
+
expect(getRingSegments(POLYGON())).toEqual([{ start: 0, length: 5, path: [0], closed: true }])
|
|
21
|
+
expect(getRingSegments({ type: 'MultiLineString', coordinates: [[[0, 0], [1, 1]], [[2, 2]]] }))
|
|
22
|
+
.toEqual([{ start: 0, length: 2, path: [0], closed: false }, { start: 2, length: 1, path: [1], closed: false }])
|
|
23
|
+
expect(getRingSegments({ type: 'MultiPolygon', coordinates: [[[[0, 0], [1, 1]]], [[[2, 2]]]] }))
|
|
24
|
+
.toEqual([{ start: 0, length: 2, path: [0, 0], closed: true }, { start: 2, length: 1, path: [1, 0], closed: true }])
|
|
25
|
+
expect(getRingSegments({ type: 'Point', coordinates: [0, 0] })).toEqual([])
|
|
26
|
+
expect(getRingSegments(null)).toEqual([])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('getSegmentForIndex finds the owning segment or returns null', () => {
|
|
30
|
+
const segments = getRingSegments(POLYGON())
|
|
31
|
+
expect(getSegmentForIndex(segments, 2)).toEqual({ segment: segments[0], localIdx: 2 })
|
|
32
|
+
expect(getSegmentForIndex(segments, 99)).toBeNull()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('getModifiableCoords walks the hierarchical path', () => {
|
|
36
|
+
const geojson = { geometry: { type: 'MultiPolygon', coordinates: [[[[0, 0]]], [[[9, 9]]]] } }
|
|
37
|
+
expect(getModifiableCoords(geojson, [1, 0])).toEqual([[9, 9]])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('coordPathToFlatIndex resolves matched paths and falls back to the last index', () => {
|
|
41
|
+
const poly = POLYGON()
|
|
42
|
+
expect(coordPathToFlatIndex(poly, '0.2')).toBe(2)
|
|
43
|
+
// Unmatched path falls back to the trailing number
|
|
44
|
+
expect(coordPathToFlatIndex(poly, '5.7')).toBe(7)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { scalePoint, isOnSVG } from './helpers.js'
|
|
2
|
+
|
|
3
|
+
describe('helpers', () => {
|
|
4
|
+
test('scalePoint multiplies both axes by the scale', () => {
|
|
5
|
+
expect(scalePoint({ x: 2, y: 3 }, 4)).toEqual({ x: 8, y: 12 })
|
|
6
|
+
expect(scalePoint({ x: 5, y: 5 }, 1)).toEqual({ x: 5, y: 5 })
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test('isOnSVG detects SVG elements and their descendants, not plain DOM nodes', () => {
|
|
10
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
11
|
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
|
|
12
|
+
svg.appendChild(circle)
|
|
13
|
+
expect(isOnSVG(svg)).toBeTruthy() // instanceof SVGElement
|
|
14
|
+
expect(isOnSVG(circle)).toBeTruthy() // nested SVG element
|
|
15
|
+
expect(isOnSVG(document.createElement('div'))).toBeFalsy()
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { getSnapInstance, clearSnapIndicator } from '../../utils/snapHelpers.js'
|
|
2
|
+
import { getCoords } from './geometryHelpers.js'
|
|
3
|
+
|
|
4
|
+
const ARROW_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'])
|
|
5
|
+
const ARROW_OFFSETS = { ArrowUp: [0, -1], ArrowDown: [0, 1], ArrowLeft: [-1, 0], ArrowRight: [1, 0] }
|
|
6
|
+
const INTERACTIVE_TAGS = new Set(['INPUT', 'TEXTAREA', 'BUTTON', 'SELECT', 'A'])
|
|
7
|
+
|
|
8
|
+
// Keyboard shortcuts are ignored while a form control outside the map viewport has
|
|
9
|
+
// focus, but still work for elements inside the viewport (e.g. draw toolbar buttons).
|
|
10
|
+
const isInteractiveElementFocused = (state) => {
|
|
11
|
+
const el = document.activeElement
|
|
12
|
+
if (!el || el === document.body) { return false }
|
|
13
|
+
if (state.container?.contains(el)) { return false }
|
|
14
|
+
return INTERACTIVE_TAGS.has(el.tagName) || el.isContentEditable || el.hasAttribute('tabindex')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const isUndoShortcut = (e) => e.key === 'z' && (e.metaKey || e.ctrlKey) && !e.shiftKey
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Keyboard interaction for the vertex-edit mode: arrow-key vertex movement/insertion,
|
|
21
|
+
* space-to-select, Escape, and Cmd/Ctrl+Z undo. Mixed into EditVertexMode.
|
|
22
|
+
*/
|
|
23
|
+
export const keyboardHandlers = {
|
|
24
|
+
onKeydown (state, e) {
|
|
25
|
+
if (isInteractiveElementFocused(state)) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
state.interfaceType = 'keyboard'
|
|
30
|
+
this.hideTouchVertexIndicator(state)
|
|
31
|
+
|
|
32
|
+
if (e.key === ' ') {
|
|
33
|
+
this.handleSpace(state, e)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
if (ARROW_KEYS.has(e.key) && state.selectedVertexIndex >= 0) {
|
|
37
|
+
this.handleArrowKey(state, e)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
if (e.key === 'Escape') {
|
|
41
|
+
this.changeMode(state, { isPanEnabled: true, selectedVertexIndex: -1, selectedVertexType: null })
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
if (isUndoShortcut(e)) {
|
|
45
|
+
this.handleUndoShortcut(state, e)
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Space always cancels the default; with no active selection it starts keyboard editing.
|
|
50
|
+
handleSpace (state, e) {
|
|
51
|
+
e.preventDefault()
|
|
52
|
+
if (state.selectedVertexIndex < 0) {
|
|
53
|
+
this.startKeyboardSelection(state)
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Alt+arrow steps to the next vertex/midpoint; a plain arrow nudges the selected vertex.
|
|
58
|
+
handleArrowKey (state, e) {
|
|
59
|
+
e.preventDefault()
|
|
60
|
+
e.stopPropagation()
|
|
61
|
+
if (e.altKey) {
|
|
62
|
+
this.updateVertex(state, e.key)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
this.moveVertexByKey(state, e)
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Space with no active selection: select the first vertex for keyboard editing.
|
|
69
|
+
startKeyboardSelection (state) {
|
|
70
|
+
const snap = getSnapInstance(this.map)
|
|
71
|
+
if (snap) {
|
|
72
|
+
clearSnapIndicator(snap, this.map)
|
|
73
|
+
}
|
|
74
|
+
if (!state.vertecies?.length) {
|
|
75
|
+
state.vertecies = this.getVerticies(state.featureId)
|
|
76
|
+
state.midpoints = this.getMidpoints(state.featureId)
|
|
77
|
+
}
|
|
78
|
+
if (!state.vertecies?.length) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
state.isPanEnabled = false
|
|
82
|
+
this.updateVertex(state)
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Arrow key with a selected vertex: insert (midpoint) or nudge the vertex, honouring snap.
|
|
86
|
+
moveVertexByKey (state, e) {
|
|
87
|
+
if (state.selectedVertexType === 'midpoint') {
|
|
88
|
+
this.insertVertex(state, e)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const feature = this.getFeature(state.featureId)
|
|
93
|
+
const currentCoord = feature && getCoords(feature)?.[state.selectedVertexIndex]
|
|
94
|
+
if (!currentCoord) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Save starting position for undo (only on first move of sequence)
|
|
99
|
+
if (!state._keyboardMoveStartPosition) {
|
|
100
|
+
state._keyboardMoveStartPosition = [...currentCoord]
|
|
101
|
+
state._keyboardMoveStartIndex = state.selectedVertexIndex
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.moveVertex(state, this._keyboardMoveTarget(state, e, currentCoord))
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// Resolve the destination coordinate for a keyboard nudge, applying or breaking
|
|
108
|
+
// snap — delegates to the shared resolver (vertexOperations.js) also used by
|
|
109
|
+
// MoveControl's nudgeVertexByDelta, so both snap identically.
|
|
110
|
+
_keyboardMoveTarget (state, e, currentCoord) {
|
|
111
|
+
const [dx, dy] = ARROW_OFFSETS[e.key]
|
|
112
|
+
return this.resolveSnapTarget(state, dx, dy, currentCoord, () => this.getNewCoord(state, e))
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// Cmd/Ctrl+Z: undo the last edit, unless the user is typing in a text field.
|
|
116
|
+
handleUndoShortcut (state, e) {
|
|
117
|
+
const tag = document.activeElement?.tagName
|
|
118
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
e.preventDefault()
|
|
122
|
+
e.stopPropagation()
|
|
123
|
+
this.handleUndo(state)
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
onKeyup (state, e) {
|
|
127
|
+
if (isInteractiveElementFocused(state)) {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
state.interfaceType = 'keyboard'
|
|
132
|
+
if (ARROW_KEYS.has(e.key) && state.selectedVertexIndex >= 0) {
|
|
133
|
+
e.stopPropagation()
|
|
134
|
+
|
|
135
|
+
// Push undo for keyboard move sequence
|
|
136
|
+
if (state._keyboardMoveStartPosition && state._keyboardMoveStartIndex != null) {
|
|
137
|
+
this.pushUndo({
|
|
138
|
+
type: 'move_vertex',
|
|
139
|
+
featureId: state.featureId,
|
|
140
|
+
vertexIndex: state._keyboardMoveStartIndex,
|
|
141
|
+
previousPosition: state._keyboardMoveStartPosition
|
|
142
|
+
})
|
|
143
|
+
state._keyboardMoveStartPosition = null
|
|
144
|
+
state._keyboardMoveStartIndex = null
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (e.key === 'Delete') {
|
|
148
|
+
this.deleteVertex(state)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createHarness } from './__helpers__/harness.js'
|
|
2
|
+
|
|
3
|
+
describe('keyboardHandlers', () => {
|
|
4
|
+
const keydown = (ctx, state, key, extra = {}) => ctx.onKeydown(state, { key, preventDefault: jest.fn(), stopPropagation: jest.fn(), ...extra })
|
|
5
|
+
const keyup = (ctx, state, key) => ctx.onKeyup(state, { key, stopPropagation: jest.fn() })
|
|
6
|
+
|
|
7
|
+
test('shortcuts are ignored outside the viewport (input, focusable non-input) and when the container is absent', () => {
|
|
8
|
+
const { ctx, state } = createHarness()
|
|
9
|
+
const input = document.createElement('input')
|
|
10
|
+
document.body.appendChild(input)
|
|
11
|
+
input.focus()
|
|
12
|
+
keydown(ctx, state, 'ArrowRight')
|
|
13
|
+
keyup(ctx, state, 'ArrowRight')
|
|
14
|
+
expect(state.interfaceType).not.toBe('keyboard')
|
|
15
|
+
|
|
16
|
+
const focusable = document.createElement('div')
|
|
17
|
+
focusable.tabIndex = 0
|
|
18
|
+
document.body.appendChild(focusable)
|
|
19
|
+
focusable.focus()
|
|
20
|
+
keydown(ctx, state, 'ArrowRight')
|
|
21
|
+
expect(state.interfaceType).not.toBe('keyboard')
|
|
22
|
+
|
|
23
|
+
input.focus()
|
|
24
|
+
const s = { ...state, container: undefined, interfaceType: 'mouse' }
|
|
25
|
+
keydown(ctx, s, 'ArrowRight')
|
|
26
|
+
expect(s.interfaceType).toBe('mouse')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('space selects the first vertex, or does nothing when a vertex is selected or there are none', () => {
|
|
30
|
+
const { ctx, state, map } = createHarness()
|
|
31
|
+
map._snapInstance = { status: true, snapStatus: true, snapCoords: [1, 1] }
|
|
32
|
+
const updateSpy = jest.spyOn(ctx, 'updateVertex').mockImplementation(() => {})
|
|
33
|
+
keydown(ctx, state, ' ')
|
|
34
|
+
expect(state.isPanEnabled).toBe(false)
|
|
35
|
+
expect(updateSpy).toHaveBeenCalledTimes(1)
|
|
36
|
+
|
|
37
|
+
keydown(ctx, { ...state, selectedVertexIndex: 0 }, ' ') // already selected → just cancels default
|
|
38
|
+
expect(updateSpy).toHaveBeenCalledTimes(1)
|
|
39
|
+
|
|
40
|
+
map._snapInstance = null // no snap indicator to clear
|
|
41
|
+
const s = { ...state, featureId: 'missing', vertecies: [], selectedVertexIndex: -1, isPanEnabled: true }
|
|
42
|
+
keydown(ctx, s, ' ')
|
|
43
|
+
expect(s.isPanEnabled).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('an arrow key moves the selected vertex, snapping when active and using the raw coord otherwise', () => {
|
|
47
|
+
const { ctx, state, map } = createHarness()
|
|
48
|
+
state.selectedVertexIndex = 1
|
|
49
|
+
keydown(ctx, state, 'ArrowRight')
|
|
50
|
+
expect(state._keyboardMoveStartIndex).toBe(1)
|
|
51
|
+
|
|
52
|
+
state.getSnapEnabled = () => true
|
|
53
|
+
map._snapInstance = { status: true, snapStatus: true, snapCoords: [7, 8], snapToClosestPoint: jest.fn() }
|
|
54
|
+
keydown(ctx, state, 'ArrowUp')
|
|
55
|
+
expect(state._isSnapped).toBe(true)
|
|
56
|
+
expect(state.vertecies[1]).toEqual([7, 8])
|
|
57
|
+
|
|
58
|
+
keydown(ctx, state, 'ArrowLeft') // snapped already → break out of the snap radius
|
|
59
|
+
expect(state._isSnapped).toBe(false)
|
|
60
|
+
|
|
61
|
+
map._snapInstance = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
|
|
62
|
+
keydown(ctx, state, 'ArrowRight') // snap enabled but inactive → raw new coord
|
|
63
|
+
expect(state._isSnapped).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('an arrow key on a midpoint inserts a vertex; guards a missing feature or out-of-range vertex', () => {
|
|
67
|
+
const { ctx, state } = createHarness()
|
|
68
|
+
const insertSpy = jest.spyOn(ctx, 'insertVertex').mockImplementation(() => {})
|
|
69
|
+
keydown(ctx, { ...state, selectedVertexIndex: state.vertecies.length, selectedVertexType: 'midpoint' }, 'ArrowRight')
|
|
70
|
+
expect(insertSpy).toHaveBeenCalled()
|
|
71
|
+
|
|
72
|
+
keydown(ctx, { ...state, featureId: 'missing', selectedVertexIndex: 0 }, 'ArrowRight')
|
|
73
|
+
keydown(ctx, { ...state, selectedVertexIndex: 99 }, 'ArrowRight')
|
|
74
|
+
expect(ctx.map._undoStack.length).toBe(0)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('alt+arrow steps the selection, Escape clears it, and Cmd/Ctrl+Z undoes (ignoring shift and text fields)', () => {
|
|
78
|
+
const { ctx, state, api } = createHarness()
|
|
79
|
+
state.selectedVertexIndex = 0
|
|
80
|
+
const updateSpy = jest.spyOn(ctx, 'updateVertex').mockImplementation(() => {})
|
|
81
|
+
keydown(ctx, state, 'ArrowRight', { altKey: true })
|
|
82
|
+
expect(updateSpy).toHaveBeenCalledWith(state, 'ArrowRight')
|
|
83
|
+
|
|
84
|
+
keydown(ctx, state, 'Escape')
|
|
85
|
+
expect(api.changeMode).toHaveBeenCalledWith('edit_vertex', expect.objectContaining({ isPanEnabled: true }))
|
|
86
|
+
|
|
87
|
+
const undoSpy = jest.spyOn(ctx, 'handleUndo').mockImplementation(() => {})
|
|
88
|
+
keydown(ctx, state, 'z', { metaKey: true })
|
|
89
|
+
keydown(ctx, state, 'z', { ctrlKey: true })
|
|
90
|
+
expect(undoSpy).toHaveBeenCalledTimes(2)
|
|
91
|
+
keydown(ctx, state, 'z', { metaKey: true, shiftKey: true }) // shift → not undo
|
|
92
|
+
expect(undoSpy).toHaveBeenCalledTimes(2)
|
|
93
|
+
|
|
94
|
+
// Ignored while typing in a text field inside the viewport
|
|
95
|
+
const input = document.createElement('input')
|
|
96
|
+
state.container.appendChild(input)
|
|
97
|
+
input.focus()
|
|
98
|
+
keydown(ctx, state, 'z', { metaKey: true })
|
|
99
|
+
expect(undoSpy).toHaveBeenCalledTimes(2)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('onKeyup pushes a move undo after a sequence, deletes on Delete, no-ops otherwise, and allows viewport focus', () => {
|
|
103
|
+
const { ctx, state, map } = createHarness()
|
|
104
|
+
state.selectedVertexIndex = 1
|
|
105
|
+
state._keyboardMoveStartPosition = [10, 0]
|
|
106
|
+
state._keyboardMoveStartIndex = 1
|
|
107
|
+
keyup(ctx, state, 'ArrowRight')
|
|
108
|
+
expect(map._undoStack.pop()).toMatchObject({ type: 'move_vertex', vertexIndex: 1 })
|
|
109
|
+
|
|
110
|
+
keyup(ctx, { ...state, selectedVertexIndex: 1 }, 'ArrowRight') // no active sequence → no undo
|
|
111
|
+
expect(map._undoStack.length).toBe(0)
|
|
112
|
+
|
|
113
|
+
const deleteSpy = jest.spyOn(ctx, 'deleteVertex').mockImplementation(() => {})
|
|
114
|
+
keyup(ctx, state, 'Delete')
|
|
115
|
+
expect(deleteSpy).toHaveBeenCalled()
|
|
116
|
+
|
|
117
|
+
const child = document.createElement('button') // focus inside the viewport is non-blocking
|
|
118
|
+
state.container.appendChild(child)
|
|
119
|
+
child.focus()
|
|
120
|
+
keyup(ctx, state, 'ArrowRight')
|
|
121
|
+
expect(state.interfaceType).toBe('keyboard')
|
|
122
|
+
})
|
|
123
|
+
})
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import DirectSelect from '../../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/modes/direct_select.js' // NOSONAR
|
|
2
|
+
import {
|
|
3
|
+
getSnapInstance, isSnapEnabled, getSnapLngLat, triggerSnapAtPoint, clearSnapState
|
|
4
|
+
} from '../../utils/snapHelpers.js'
|
|
5
|
+
import { getCoords, coordPathToFlatIndex } from './geometryHelpers.js'
|
|
6
|
+
|
|
7
|
+
const EVENT_VERTEX_SELECTION = 'draw.vertexselection'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mouse/pointer interaction for the vertex-edit mode: vertex/midpoint mouse-down,
|
|
11
|
+
* click-to-commit insertions, drag (with snapping) and mouse-up undo bookkeeping.
|
|
12
|
+
* Wraps the parent DirectSelect handlers. Mixed into EditVertexMode.
|
|
13
|
+
*/
|
|
14
|
+
export const pointerHandlers = {
|
|
15
|
+
onMouseDown (state, e) {
|
|
16
|
+
clearSnapState(getSnapInstance(this.map))
|
|
17
|
+
const meta = e.featureTarget?.properties.meta
|
|
18
|
+
const coordPath = e.featureTarget?.properties.coord_path
|
|
19
|
+
|
|
20
|
+
if (['vertex', 'midpoint'].includes(meta)) {
|
|
21
|
+
state.dragMoveLocation = e.lngLat
|
|
22
|
+
state.dragMoving = false
|
|
23
|
+
DirectSelect.onMouseDown.call(this, state, e)
|
|
24
|
+
|
|
25
|
+
// Update selection state for vertex clicks (so onSelectionChange has correct context)
|
|
26
|
+
if (meta === 'vertex' && coordPath) {
|
|
27
|
+
const feature = this.getFeature(state.featureId)
|
|
28
|
+
const vertexIndex = coordPathToFlatIndex(feature, coordPath)
|
|
29
|
+
state.selectedVertexIndex = vertexIndex
|
|
30
|
+
state.selectedVertexType = 'vertex'
|
|
31
|
+
state.coordPath = coordPath
|
|
32
|
+
const vertex = state.vertecies?.[vertexIndex]
|
|
33
|
+
if (vertex) {
|
|
34
|
+
state._moveStartPosition = [...vertex]
|
|
35
|
+
state._moveStartIndex = vertexIndex
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (meta === 'midpoint') {
|
|
40
|
+
// DirectSelect converts midpoint to vertex - track this as an insert
|
|
41
|
+
const feature = this.getFeature(state.featureId)
|
|
42
|
+
const insertedIndex = coordPathToFlatIndex(feature, coordPath)
|
|
43
|
+
|
|
44
|
+
// Track this insertion for undo (will be pushed on mouseUp if drag occurred)
|
|
45
|
+
state._insertedVertexIndex = insertedIndex
|
|
46
|
+
state._isInsertingVertex = true
|
|
47
|
+
|
|
48
|
+
state.selectedVertexIndex = this.getVertexIndexFromMidpoint(state, coordPath)
|
|
49
|
+
state.selectedVertexType = 'vertex'
|
|
50
|
+
state.coordPath = null // Clear coordPath for midpoints
|
|
51
|
+
this.map.fire(EVENT_VERTEX_SELECTION, { index: state.selectedVertexIndex, numVertecies: state.vertecies.length })
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
onClick (state, e) { // NOSONAR — complexity accumulated from object-level context; single guard clause, see feedback_mgl_click_vs_mouseup.md
|
|
56
|
+
if (state._isInsertingVertex && state._insertedVertexIndex != null) {
|
|
57
|
+
const insertedIndex = state._insertedVertexIndex
|
|
58
|
+
this.syncVertices(state)
|
|
59
|
+
this.pushUndo({ type: 'insert_vertex', featureId: state.featureId, vertexIndex: insertedIndex })
|
|
60
|
+
state.selectedVertexIndex = insertedIndex
|
|
61
|
+
state.selectedVertexType = 'vertex'
|
|
62
|
+
state._isInsertingVertex = false
|
|
63
|
+
state._insertedVertexIndex = null
|
|
64
|
+
this.map.fire(EVENT_VERTEX_SELECTION, { index: insertedIndex, numVertecies: state.vertecies.length })
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
DirectSelect.onClick.call(this, state, e)
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
onMouseUp (state, e) {
|
|
71
|
+
clearSnapState(getSnapInstance(this.map))
|
|
72
|
+
|
|
73
|
+
const wasInsertion = state._isInsertingVertex && state._insertedVertexIndex != null
|
|
74
|
+
const vertexMoved = this._didVertexMove(state)
|
|
75
|
+
|
|
76
|
+
if (state.dragMoving || vertexMoved || wasInsertion) {
|
|
77
|
+
this.syncVertices(state)
|
|
78
|
+
if (wasInsertion) {
|
|
79
|
+
this._recordInsertionUndo(state)
|
|
80
|
+
} else if (vertexMoved) {
|
|
81
|
+
this._recordMoveUndo(state)
|
|
82
|
+
} else {
|
|
83
|
+
// dragMoving without an actual move or insertion: nothing to record
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Clean up move state
|
|
88
|
+
state._moveStartPosition = null
|
|
89
|
+
state._moveStartIndex = null
|
|
90
|
+
|
|
91
|
+
DirectSelect.onMouseUp.call(this, state, e)
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// Did the selected vertex actually change position during this interaction?
|
|
95
|
+
// Reads the live feature (not the cached state.vertecies) for reliability.
|
|
96
|
+
_didVertexMove (state) {
|
|
97
|
+
if (!state._moveStartPosition || state._moveStartIndex == null) {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
const feature = this.getFeature(state.featureId)
|
|
101
|
+
const currentVertex = feature && getCoords(feature)?.[state._moveStartIndex]
|
|
102
|
+
if (!currentVertex) {
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
return currentVertex[0] !== state._moveStartPosition[0] ||
|
|
106
|
+
currentVertex[1] !== state._moveStartPosition[1]
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Commit a midpoint-drag insertion: record undo, reselect the new vertex, broadcast the count.
|
|
110
|
+
_recordInsertionUndo (state) {
|
|
111
|
+
const insertedIndex = state._insertedVertexIndex
|
|
112
|
+
this.pushUndo({ type: 'insert_vertex', featureId: state.featureId, vertexIndex: insertedIndex })
|
|
113
|
+
// selectedVertexIndex pointed at the old midpoint-range index; use the real flat index
|
|
114
|
+
state.selectedVertexIndex = insertedIndex
|
|
115
|
+
state.selectedVertexType = 'vertex'
|
|
116
|
+
state._isInsertingVertex = false
|
|
117
|
+
state._insertedVertexIndex = null
|
|
118
|
+
// DirectSelect.onMouseUp fires draw.update but not draw.selectionchange, so broadcast the count here
|
|
119
|
+
this.map.fire(EVENT_VERTEX_SELECTION, { index: insertedIndex, numVertecies: state.vertecies.length })
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
_recordMoveUndo (state) {
|
|
123
|
+
this.pushUndo({
|
|
124
|
+
type: 'move_vertex',
|
|
125
|
+
featureId: state.featureId,
|
|
126
|
+
vertexIndex: state._moveStartIndex,
|
|
127
|
+
previousPosition: state._moveStartPosition
|
|
128
|
+
})
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
onDrag (state, e) {
|
|
132
|
+
if (state.interfaceType === 'touch') {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.map.fire('draw.geometrychange', state.feature)
|
|
137
|
+
|
|
138
|
+
const snap = getSnapInstance(this.map)
|
|
139
|
+
if (snap) {
|
|
140
|
+
snap.snapStatus = false
|
|
141
|
+
snap.snapCoords = null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!isSnapEnabled(state) || !snap?.status) {
|
|
145
|
+
DirectSelect.onDrag.call(this, state, e)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!state.selectedCoordPaths?.length || !state.canDragMove) {
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
state.dragMoving = true
|
|
154
|
+
e.originalEvent.stopPropagation()
|
|
155
|
+
triggerSnapAtPoint(snap, this.map, e.point)
|
|
156
|
+
|
|
157
|
+
const finalLngLat = getSnapLngLat(snap) || e.lngLat
|
|
158
|
+
state.feature.updateCoordinate(state.selectedCoordPaths[0], finalLngLat.lng, finalLngLat.lat)
|
|
159
|
+
state.dragMoveLocation = e.lngLat
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createHarness, dragEvt } from './__helpers__/harness.js'
|
|
2
|
+
|
|
3
|
+
describe('pointerHandlers', () => {
|
|
4
|
+
test('onMouseDown selects a vertex and records the move start, ignoring empty/no-coord targets', () => {
|
|
5
|
+
const { ctx, state } = createHarness()
|
|
6
|
+
ctx.onMouseDown(state, { lngLat: { lng: 10, lat: 0 }, featureTarget: { properties: { meta: 'vertex', coord_path: '0.1' } } })
|
|
7
|
+
expect(state.selectedVertexIndex).toBe(1)
|
|
8
|
+
expect(state._moveStartIndex).toBe(1)
|
|
9
|
+
|
|
10
|
+
const h = createHarness()
|
|
11
|
+
h.ctx.onMouseDown(h.state, { lngLat: { lng: 0, lat: 0 }, featureTarget: undefined })
|
|
12
|
+
h.ctx.onMouseDown(h.state, { lngLat: { lng: 0, lat: 0 }, featureTarget: { properties: { meta: 'vertex', coord_path: '0.9' } } })
|
|
13
|
+
expect(h.state._moveStartIndex).toBeUndefined()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('onMouseDown on a midpoint marks an insertion in progress', () => {
|
|
17
|
+
const { ctx, state, map } = createHarness()
|
|
18
|
+
ctx.onMouseDown(state, { lngLat: { lng: 5, lat: 0 }, featureTarget: { properties: { meta: 'midpoint', coord_path: '0.1', lng: 5, lat: 0 } } })
|
|
19
|
+
expect(state._isInsertingVertex).toBe(true)
|
|
20
|
+
expect(map.fire).toHaveBeenCalledWith('draw.vertexselection', expect.any(Object))
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('onClick commits an in-progress insertion, otherwise delegates to DirectSelect', () => {
|
|
24
|
+
const { ctx, state, api } = createHarness()
|
|
25
|
+
state._isInsertingVertex = true
|
|
26
|
+
state._insertedVertexIndex = 2
|
|
27
|
+
ctx.onClick(state, {})
|
|
28
|
+
expect(ctx.map._undoStack.pop()).toMatchObject({ type: 'insert_vertex', vertexIndex: 2 })
|
|
29
|
+
expect(state._isInsertingVertex).toBe(false)
|
|
30
|
+
|
|
31
|
+
ctx.onClick(state, { featureTarget: undefined }) // noTarget → clickNoTarget → changeMode
|
|
32
|
+
expect(api.changeMode).toHaveBeenCalledWith('edit_vertex', expect.objectContaining({ selectedVertexIndex: -1 }))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('onMouseUp records a move undo, an insertion undo, or nothing', () => {
|
|
36
|
+
const { ctx, state, map } = createHarness()
|
|
37
|
+
// nothing
|
|
38
|
+
ctx.onMouseUp(state, {})
|
|
39
|
+
expect(map._undoStack.length).toBe(0)
|
|
40
|
+
|
|
41
|
+
// move
|
|
42
|
+
state._moveStartIndex = 1
|
|
43
|
+
state._moveStartPosition = [10, 0]
|
|
44
|
+
ctx.moveVertex({ ...state, selectedVertexIndex: 1 }, { lng: 3, lat: 3 })
|
|
45
|
+
ctx.onMouseUp(state, {})
|
|
46
|
+
expect(map._undoStack.pop()).toMatchObject({ type: 'move_vertex', vertexIndex: 1 })
|
|
47
|
+
|
|
48
|
+
// insertion
|
|
49
|
+
state.dragMoving = true
|
|
50
|
+
state._isInsertingVertex = true
|
|
51
|
+
state._insertedVertexIndex = 2
|
|
52
|
+
ctx.onMouseUp(state, {})
|
|
53
|
+
expect(map._undoStack.pop()).toMatchObject({ type: 'insert_vertex', vertexIndex: 2 })
|
|
54
|
+
|
|
55
|
+
// In-progress drag with no move/insertion records nothing; guards missing feature/stale index; detects a vertical-only move
|
|
56
|
+
const h = createHarness()
|
|
57
|
+
h.ctx.onMouseUp({ ...h.state, dragMoving: true }, {})
|
|
58
|
+
h.ctx.onMouseUp({ ...h.state, _moveStartPosition: [0, 0], _moveStartIndex: 0, featureId: 'missing' }, {})
|
|
59
|
+
h.ctx.onMouseUp({ ...h.state, _moveStartPosition: [0, 0], _moveStartIndex: 99 }, {})
|
|
60
|
+
expect(h.map._undoStack.length).toBe(0)
|
|
61
|
+
h.ctx.moveVertex({ ...h.state, selectedVertexIndex: 1 }, { lng: 10, lat: 5 }) // same lng, new lat
|
|
62
|
+
h.ctx.onMouseUp({ ...h.state, _moveStartPosition: [10, 0], _moveStartIndex: 1 }, {})
|
|
63
|
+
expect(h.map._undoStack.pop()).toMatchObject({ type: 'move_vertex', vertexIndex: 1 })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('onDrag skips touch, delegates without snap, and snaps when enabled', () => {
|
|
67
|
+
const { ctx, state, map } = createHarness()
|
|
68
|
+
ctx.onDrag({ ...state, interfaceType: 'touch' }, { originalEvent: { stopPropagation: jest.fn() }, lngLat: { lng: 1, lat: 1 }, point: { x: 10, y: 10 } })
|
|
69
|
+
|
|
70
|
+
// no snap → DirectSelect.onDrag moves the vertex
|
|
71
|
+
Object.assign(state, { canDragMove: true, selectedCoordPaths: ['0.1'], dragMoveLocation: { lng: 0, lat: 0 } })
|
|
72
|
+
ctx.onDrag(state, { originalEvent: { stopPropagation: jest.fn() }, lngLat: { lng: 2, lat: 2 }, point: { x: 20, y: 20 } })
|
|
73
|
+
expect(map.fire).toHaveBeenCalledWith('draw.geometrychange', expect.anything())
|
|
74
|
+
|
|
75
|
+
// snap enabled
|
|
76
|
+
state.getSnapEnabled = () => true
|
|
77
|
+
map._snapInstance = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
|
|
78
|
+
ctx.onDrag(state, { originalEvent: { stopPropagation: jest.fn() }, lngLat: { lng: 3, lat: 3 }, point: { x: 30, y: 30 } })
|
|
79
|
+
expect(state.dragMoving).toBe(true)
|
|
80
|
+
|
|
81
|
+
// Snapping enabled but nothing selected → bail out
|
|
82
|
+
const nosel = createHarness()
|
|
83
|
+
nosel.map._snapInstance = { status: true, snapStatus: true, snapCoords: [1, 1] }
|
|
84
|
+
const ns = { ...nosel.state, getSnapEnabled: () => true, selectedCoordPaths: [], canDragMove: false }
|
|
85
|
+
nosel.ctx.onDrag(ns, dragEvt(1, 1))
|
|
86
|
+
expect(ns.dragMoving).not.toBe(true)
|
|
87
|
+
|
|
88
|
+
// Snapping active → snapped point; inactive → raw point
|
|
89
|
+
const snapped = createHarness()
|
|
90
|
+
snapped.map._snapInstance = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn(() => { snapped.map._snapInstance.snapStatus = true; snapped.map._snapInstance.snapCoords = [7, 8] }) }
|
|
91
|
+
snapped.ctx.onDrag({ ...snapped.state, getSnapEnabled: () => true, selectedCoordPaths: ['0.1'], canDragMove: true }, dragEvt(2, 2))
|
|
92
|
+
expect(snapped.ctx.getFeature('feat-1').getCoordinate('0.1')).toEqual([7, 8])
|
|
93
|
+
const raw = createHarness()
|
|
94
|
+
raw.map._snapInstance = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
|
|
95
|
+
raw.ctx.onDrag({ ...raw.state, getSnapEnabled: () => true, selectedCoordPaths: ['0.1'], canDragMove: true }, dragEvt(3, 3))
|
|
96
|
+
expect(raw.ctx.getFeature('feat-1').getCoordinate('0.1')).toEqual([3, 3])
|
|
97
|
+
})
|
|
98
|
+
})
|