@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,109 @@
|
|
|
1
|
+
import { createSelectionState } from './selectionState.js'
|
|
2
|
+
import { createFakeManager, polygonFeature } from '../__helpers__/harness.js'
|
|
3
|
+
import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
|
|
4
|
+
|
|
5
|
+
const RING = [[0, 0], [10, 0], [10, 10], [0, 0]]
|
|
6
|
+
|
|
7
|
+
const setup = (interfaceType) => {
|
|
8
|
+
const manager = createFakeManager()
|
|
9
|
+
const olFeature = polygonFeature(RING)
|
|
10
|
+
const layers = {
|
|
11
|
+
vertexLayer: { update: jest.fn(), setSelected: jest.fn() },
|
|
12
|
+
midpointLayer: { update: jest.fn(), setSelected: jest.fn(), getCoords: jest.fn(() => [[5, 0], [10, 5], [5, 5]]) },
|
|
13
|
+
activeLayer: { update: jest.fn() }
|
|
14
|
+
}
|
|
15
|
+
const map = { render: jest.fn() }
|
|
16
|
+
const store = { toGeoJSON: jest.fn(() => ({ id: 'f1' })) }
|
|
17
|
+
const selection = createSelectionState({ map, manager, store, olFeature, interfaceType, layers })
|
|
18
|
+
return { manager, olFeature, layers, map, store, selection }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('defaults to mouse interface when none is given', () => {
|
|
22
|
+
expect(setup().selection.state.interfaceType).toBe('mouse')
|
|
23
|
+
expect(setup('touch').selection.state.interfaceType).toBe('touch')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('selecting a vertex highlights it, clears midpoint selection and reports the selection', () => {
|
|
27
|
+
const { selection, layers, manager } = setup()
|
|
28
|
+
selection.state.vertices = [[0, 0], [10, 0], [10, 10]]
|
|
29
|
+
selection.setState({ selectedVertexIndex: 1, selectedVertexType: 'vertex' })
|
|
30
|
+
expect(layers.vertexLayer.setSelected).toHaveBeenCalledWith(1)
|
|
31
|
+
expect(layers.midpointLayer.setSelected).toHaveBeenCalledWith(-1)
|
|
32
|
+
expect(layers.activeLayer.update).toHaveBeenCalledWith(selection.state)
|
|
33
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, { index: 1, numVertices: 3 })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('selecting a midpoint highlights it by local index and reports index -1', () => {
|
|
37
|
+
const { selection, layers, manager } = setup()
|
|
38
|
+
selection.state.vertices = [[0, 0], [10, 0], [10, 10]]
|
|
39
|
+
selection.setState({ selectedVertexIndex: 4, selectedVertexType: 'midpoint' })
|
|
40
|
+
expect(layers.midpointLayer.setSelected).toHaveBeenCalledWith(1)
|
|
41
|
+
expect(layers.vertexLayer.setSelected).toHaveBeenCalledWith(-1)
|
|
42
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, { index: -1, numVertices: 3 })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('deselecting fires the onDeselect hook', () => {
|
|
46
|
+
const { selection } = setup()
|
|
47
|
+
const onDeselect = jest.fn()
|
|
48
|
+
selection.setHooks({ onDeselect })
|
|
49
|
+
selection.setState({ selectedVertexIndex: -1, selectedVertexType: null })
|
|
50
|
+
expect(onDeselect).toHaveBeenCalled()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('a vertices update refreshes the handle layers, midpoints and the map', () => {
|
|
54
|
+
const { selection, layers, map } = setup()
|
|
55
|
+
const onUpdate = jest.fn()
|
|
56
|
+
selection.setHooks({ onUpdate })
|
|
57
|
+
selection.setState({ vertices: [[0, 0], [10, 0], [10, 10]] })
|
|
58
|
+
expect(layers.vertexLayer.update).toHaveBeenCalledWith({ type: 'Polygon', coordinates: [RING] })
|
|
59
|
+
expect(selection.state.midpoints).toEqual([[5, 0], [10, 5], [5, 5]])
|
|
60
|
+
expect(onUpdate).toHaveBeenCalled()
|
|
61
|
+
expect(map.render).toHaveBeenCalled()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('syncGeom derives state from the geometry and emits vertexchange + update', () => {
|
|
65
|
+
const { selection, manager, store } = setup()
|
|
66
|
+
selection.syncGeom()
|
|
67
|
+
expect(selection.state.vertices).toEqual([[0, 0], [10, 0], [10, 10]])
|
|
68
|
+
expect(selection.state.midpoints).toHaveLength(3)
|
|
69
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_CHANGE, { numVertices: 3 })
|
|
70
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.UPDATE, store.toGeoJSON())
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('emitGeometryValidation emits a deferred commit-level geometrychange with the change phase', () => {
|
|
74
|
+
jest.useFakeTimers()
|
|
75
|
+
const { selection, manager, store } = setup()
|
|
76
|
+
manager.emit.mockClear()
|
|
77
|
+
|
|
78
|
+
selection.emitGeometryValidation('commit-move', 2)
|
|
79
|
+
// Deferred a tick to avoid re-entrancy — nothing emitted synchronously.
|
|
80
|
+
expect(manager.emit).not.toHaveBeenCalledWith(ADAPTER_EVENTS.GEOMETRY_CHANGE, expect.anything())
|
|
81
|
+
|
|
82
|
+
jest.runAllTimers()
|
|
83
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.GEOMETRY_CHANGE, {
|
|
84
|
+
feature: store.toGeoJSON(),
|
|
85
|
+
phase: 'commit-move',
|
|
86
|
+
vertexIndex: 2
|
|
87
|
+
})
|
|
88
|
+
jest.useRealTimers()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('emitGeometryValidation is a no-op without a change phase', () => {
|
|
92
|
+
jest.useFakeTimers()
|
|
93
|
+
const { selection, manager } = setup()
|
|
94
|
+
manager.emit.mockClear()
|
|
95
|
+
selection.emitGeometryValidation(undefined, 0)
|
|
96
|
+
jest.runAllTimers()
|
|
97
|
+
expect(manager.emit).not.toHaveBeenCalledWith(ADAPTER_EVENTS.GEOMETRY_CHANGE, expect.anything())
|
|
98
|
+
jest.useRealTimers()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('geometry changes refresh the layers until destroy unbinds the listener', () => {
|
|
102
|
+
const { selection, olFeature, layers } = setup()
|
|
103
|
+
olFeature.getGeometry().setCoordinates([[[0, 0], [20, 0], [20, 20], [0, 0]]])
|
|
104
|
+
expect(selection.state.vertices).toEqual([[0, 0], [20, 0], [20, 20]])
|
|
105
|
+
const calls = layers.vertexLayer.update.mock.calls.length
|
|
106
|
+
selection.destroy()
|
|
107
|
+
olFeature.getGeometry().setCoordinates([[[0, 0], [30, 0], [30, 30], [0, 0]]])
|
|
108
|
+
expect(layers.vertexLayer.update.mock.calls.length).toBe(calls)
|
|
109
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { coordToPixel, pixelToCoord } from '../utils/olCoords.js'
|
|
2
|
+
import { createTouchTarget, applyTouchTargetColors, showTouchTarget, hideTouchTarget, isOnTouchTarget } from '../utils/touchTarget.js'
|
|
3
|
+
import { moveVertex } from './vertexOps.js'
|
|
4
|
+
import { findNearest } from './vertexHitTest.js'
|
|
5
|
+
|
|
6
|
+
const TAP_MOVE_THRESHOLD = 10
|
|
7
|
+
const TAP_TIME_THRESHOLD = 400
|
|
8
|
+
const TOUCH_TOLERANCE = 24
|
|
9
|
+
|
|
10
|
+
const handleTouchStart = (e, { map, targetEl, cssToOl, getState, drag }) => {
|
|
11
|
+
const touch = e.touches[0]
|
|
12
|
+
const onTarget = isOnTouchTarget(e.target)
|
|
13
|
+
drag.tapStart = { x: touch.clientX, y: touch.clientY, time: Date.now(), onTarget }
|
|
14
|
+
if (!onTarget) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
const { selectedVertexIndex, vertices } = getState()
|
|
18
|
+
const vertex = vertices[selectedVertexIndex]
|
|
19
|
+
if (!vertex) {
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
const tOl = map.getEventPixel({ clientX: touch.clientX, clientY: touch.clientY })
|
|
23
|
+
const vertexPx = coordToPixel(map, vertex)
|
|
24
|
+
const style = getComputedStyle(targetEl)
|
|
25
|
+
const svgOlPx = cssToOl({ x: Number.parseFloat(style.left), y: Number.parseFloat(style.top) })
|
|
26
|
+
drag.dragStartCoord = [...vertex]
|
|
27
|
+
drag.dragStartIndex = selectedVertexIndex
|
|
28
|
+
drag.vertexTouchDelta = { x: tOl[0] - vertexPx.x, y: tOl[1] - vertexPx.y }
|
|
29
|
+
drag.targetTouchDelta = { x: tOl[0] - svgOlPx.x, y: tOl[1] - svgOlPx.y }
|
|
30
|
+
e.preventDefault()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const handleTouchMove = (e, { map, targetEl, olToCSS, getState, setState, snap, drag }) => {
|
|
34
|
+
if (!isOnTouchTarget(e.target) || drag.dragStartIndex == null) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
e.preventDefault()
|
|
38
|
+
const tOl = map.getEventPixel({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY })
|
|
39
|
+
const rawCoord = pixelToCoord(map, { x: tOl[0] - drag.vertexTouchDelta.x, y: tOl[1] - drag.vertexTouchDelta.y })
|
|
40
|
+
const newCoord = snap ? snap.apply(rawCoord) : rawCoord
|
|
41
|
+
snap?.hideIndicator()
|
|
42
|
+
const { olFeature, vertices } = getState()
|
|
43
|
+
if (!olFeature) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
moveVertex(olFeature, drag.dragStartIndex, newCoord)
|
|
47
|
+
setState({ vertices: vertices.map((c, i) => i === drag.dragStartIndex ? newCoord : c) })
|
|
48
|
+
showTouchTarget(targetEl, olToCSS({ x: tOl[0] - drag.targetTouchDelta.x, y: tOl[1] - drag.targetTouchDelta.y }))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const handleTap = (e, { map, getState, onTap, drag }) => {
|
|
52
|
+
if (!drag.tapStart || drag.tapStart.onTarget || e.changedTouches.length === 0) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
const t = e.changedTouches[0]
|
|
56
|
+
const dt = Date.now() - drag.tapStart.time
|
|
57
|
+
if (Math.hypot(t.clientX - drag.tapStart.x, t.clientY - drag.tapStart.y) < TAP_MOVE_THRESHOLD && dt < TAP_TIME_THRESHOLD) {
|
|
58
|
+
const tOl = map.getEventPixel({ clientX: t.clientX, clientY: t.clientY })
|
|
59
|
+
const tapState = getState()
|
|
60
|
+
onTap?.(findNearest(map, tapState.vertices, tapState.midpoints, { x: tOl[0], y: tOl[1] }, TOUCH_TOLERANCE))
|
|
61
|
+
e.preventDefault()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handleTouchEnd = (e, ctx) => {
|
|
66
|
+
const { getState, onVertexMoved, snap, drag } = ctx
|
|
67
|
+
if (drag.dragStartIndex == null) {
|
|
68
|
+
handleTap(e, ctx)
|
|
69
|
+
drag.tapStart = null
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
drag.tapStart = null
|
|
73
|
+
const { vertices } = getState()
|
|
74
|
+
if (vertices[drag.dragStartIndex] && drag.dragStartCoord) {
|
|
75
|
+
onVertexMoved({ vertexIndex: drag.dragStartIndex, previousCoord: drag.dragStartCoord })
|
|
76
|
+
}
|
|
77
|
+
snap?.hideIndicator()
|
|
78
|
+
drag.dragStartCoord = null; drag.dragStartIndex = null; drag.vertexTouchDelta = null; drag.targetTouchDelta = null
|
|
79
|
+
e.preventDefault()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const wireTouchEvents = (deps) => {
|
|
83
|
+
const { container } = deps
|
|
84
|
+
const drag = {
|
|
85
|
+
dragStartCoord: null,
|
|
86
|
+
dragStartIndex: null,
|
|
87
|
+
vertexTouchDelta: null,
|
|
88
|
+
targetTouchDelta: null,
|
|
89
|
+
tapStart: null
|
|
90
|
+
}
|
|
91
|
+
const ctx = { ...deps, drag }
|
|
92
|
+
|
|
93
|
+
const onTouchstart = (e) => handleTouchStart(e, ctx)
|
|
94
|
+
const onTouchmove = (e) => handleTouchMove(e, ctx)
|
|
95
|
+
const onTouchend = (e) => handleTouchEnd(e, ctx)
|
|
96
|
+
|
|
97
|
+
container.addEventListener('touchstart', onTouchstart, { passive: false })
|
|
98
|
+
container.addEventListener('touchmove', onTouchmove, { passive: false })
|
|
99
|
+
container.addEventListener('touchend', onTouchend, { passive: false })
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
isDragging: () => drag.dragStartIndex != null,
|
|
103
|
+
destroy () {
|
|
104
|
+
container.removeEventListener('touchstart', onTouchstart)
|
|
105
|
+
container.removeEventListener('touchmove', onTouchmove)
|
|
106
|
+
container.removeEventListener('touchend', onTouchend)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Touch vertex drag handler for edit mode.
|
|
113
|
+
* Shows an SVG offset target below the finger so the vertex can be repositioned
|
|
114
|
+
* without finger occlusion. Tap on a vertex or midpoint selects it via onTap.
|
|
115
|
+
*
|
|
116
|
+
* @param {{ map, container, getState, setState, onVertexMoved, onTap, colors }} options
|
|
117
|
+
* @returns {{ updateTargetPosition, updateColors, hide, destroy }}
|
|
118
|
+
*/
|
|
119
|
+
export const createTouchHandler = ({ map, container, getState, setState, onVertexMoved, onTap, colors, snap }) => {
|
|
120
|
+
const targetEl = createTouchTarget(container)
|
|
121
|
+
applyTouchTargetColors(targetEl, colors)
|
|
122
|
+
|
|
123
|
+
// OL pixel space is relative to ol-viewport at its pre-scale CSS size.
|
|
124
|
+
// Container CSS space is larger when a CSS transform scales up ol-viewport
|
|
125
|
+
// (e.g. scale(1.5) at medium map size makes OL pixels 1.5× smaller than CSS pixels).
|
|
126
|
+
const cssTx = { scale: 1, ox: 0, oy: 0 }
|
|
127
|
+
const olToCSS = (p) => ({ x: p.x * cssTx.scale + cssTx.ox, y: p.y * cssTx.scale + cssTx.oy })
|
|
128
|
+
const cssToOl = (p) => ({ x: (p.x - cssTx.ox) / cssTx.scale, y: (p.y - cssTx.oy) / cssTx.scale })
|
|
129
|
+
|
|
130
|
+
const syncCssTx = () => {
|
|
131
|
+
const vpEl = map.getViewport()
|
|
132
|
+
const vpRect = vpEl.getBoundingClientRect()
|
|
133
|
+
const cRect = container.getBoundingClientRect()
|
|
134
|
+
const vpScale = vpEl.offsetWidth > 0 ? vpRect.width / vpEl.offsetWidth : 1
|
|
135
|
+
const cScale = container.offsetWidth > 0 ? cRect.width / container.offsetWidth : 1
|
|
136
|
+
Object.assign(cssTx, {
|
|
137
|
+
scale: vpScale / cScale,
|
|
138
|
+
ox: (vpRect.left - cRect.left) / cScale,
|
|
139
|
+
oy: (vpRect.top - cRect.top) / cScale
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const touchEvents = wireTouchEvents({ container, map, targetEl, olToCSS, cssToOl, getState, setState, onVertexMoved, onTap, snap })
|
|
144
|
+
|
|
145
|
+
const updateTargetPosition = () => {
|
|
146
|
+
const { selectedVertexIndex, vertices, interfaceType } = getState()
|
|
147
|
+
if (selectedVertexIndex < 0 || !vertices[selectedVertexIndex] || interfaceType !== 'touch') {
|
|
148
|
+
hideTouchTarget(targetEl)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
const px = coordToPixel(map, vertices[selectedVertexIndex])
|
|
152
|
+
if (!px) {
|
|
153
|
+
hideTouchTarget(targetEl)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
showTouchTarget(targetEl, olToCSS(px))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Reposition on every render — keeps target anchored during pinch-zoom and pan.
|
|
160
|
+
// Skipped during drag since touchmove handles position directly.
|
|
161
|
+
const onPostrender = () => {
|
|
162
|
+
const { selectedVertexIndex, interfaceType } = getState()
|
|
163
|
+
if (selectedVertexIndex >= 0 && !touchEvents.isDragging() && interfaceType === 'touch') {
|
|
164
|
+
updateTargetPosition()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
map.on('postrender', onPostrender)
|
|
168
|
+
|
|
169
|
+
const onSizeChange = () => { syncCssTx(); map.once('postrender', updateTargetPosition) }
|
|
170
|
+
map.on('change:size', onSizeChange)
|
|
171
|
+
syncCssTx()
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
updateTargetPosition,
|
|
175
|
+
updateColors (newColors) { applyTouchTargetColors(targetEl, newColors) },
|
|
176
|
+
hide () { hideTouchTarget(targetEl) },
|
|
177
|
+
destroy () {
|
|
178
|
+
map.un('change:size', onSizeChange)
|
|
179
|
+
map.un('postrender', onPostrender)
|
|
180
|
+
touchEvents.destroy()
|
|
181
|
+
hideTouchTarget(targetEl)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createTouchHandler } from './touchHandler.js'
|
|
2
|
+
import { createFakeMap, createContainer, polygonFeature, domEvent } from '../__helpers__/harness.js'
|
|
3
|
+
|
|
4
|
+
const RING = [[0, 0], [100, 0], [100, 100], [0, 100], [0, 0]]
|
|
5
|
+
|
|
6
|
+
// Guard-path tests only — the happy touch-drag and tap flows are covered through EditMode
|
|
7
|
+
const setup = (stateOverrides = {}, { snap = null } = {}) => {
|
|
8
|
+
const map = createFakeMap()
|
|
9
|
+
const container = createContainer()
|
|
10
|
+
const state = {
|
|
11
|
+
olFeature: polygonFeature(RING),
|
|
12
|
+
selectedVertexIndex: 1,
|
|
13
|
+
selectedVertexType: 'vertex',
|
|
14
|
+
vertices: [[0, 0], [100, 0], [100, 100], [0, 100]],
|
|
15
|
+
midpoints: [],
|
|
16
|
+
interfaceType: 'touch',
|
|
17
|
+
...stateOverrides
|
|
18
|
+
}
|
|
19
|
+
const setState = jest.fn((updates) => Object.assign(state, updates))
|
|
20
|
+
const onVertexMoved = jest.fn()
|
|
21
|
+
const onTap = jest.fn()
|
|
22
|
+
const handler = createTouchHandler({ map, container, getState: () => state, setState, onVertexMoved, onTap, colors: {}, snap })
|
|
23
|
+
const grip = container.querySelector('[data-im-draw-touch-target] circle')
|
|
24
|
+
const touch = (type, x, y) => grip.dispatchEvent(domEvent(type, {
|
|
25
|
+
touches: [{ clientX: x, clientY: y }],
|
|
26
|
+
changedTouches: [{ clientX: x, clientY: y }]
|
|
27
|
+
}))
|
|
28
|
+
return { map, container, state, handler, onVertexMoved, onTap, touch, grip }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
afterEach(() => { document.body.innerHTML = '' })
|
|
32
|
+
|
|
33
|
+
test('touching the target with nothing selected starts neither a drag nor a tap', () => {
|
|
34
|
+
const { state, handler, touch, onVertexMoved, onTap } = setup({ selectedVertexIndex: -1 })
|
|
35
|
+
touch('touchstart', 100, 0)
|
|
36
|
+
touch('touchmove', 120, 10)
|
|
37
|
+
touch('touchend', 120, 10)
|
|
38
|
+
expect(onVertexMoved).not.toHaveBeenCalled()
|
|
39
|
+
expect(onTap).not.toHaveBeenCalled() // target touches never count as taps
|
|
40
|
+
expect(state.olFeature.getGeometry().getCoordinates()[0]).toEqual(RING)
|
|
41
|
+
handler.destroy()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('a stray touchmove without an active drag is ignored', () => {
|
|
45
|
+
const { state, handler, touch } = setup()
|
|
46
|
+
touch('touchmove', 120, 10)
|
|
47
|
+
expect(state.olFeature.getGeometry().getCoordinates()[0]).toEqual(RING)
|
|
48
|
+
handler.destroy()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('a drag halts safely if the feature disappears mid-gesture', () => {
|
|
52
|
+
const { state, handler, touch } = setup()
|
|
53
|
+
handler.updateTargetPosition() // target visible over vertex 1
|
|
54
|
+
touch('touchstart', 100, 0)
|
|
55
|
+
const feature = state.olFeature
|
|
56
|
+
state.olFeature = null
|
|
57
|
+
touch('touchmove', 120, 10)
|
|
58
|
+
expect(feature.getGeometry().getCoordinates()[0]).toEqual(RING)
|
|
59
|
+
handler.destroy()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('the offset target hides when its vertex cannot be projected', () => {
|
|
63
|
+
const { map, container, handler } = setup()
|
|
64
|
+
handler.updateTargetPosition()
|
|
65
|
+
const target = container.querySelector('[data-im-draw-touch-target]')
|
|
66
|
+
expect(target.style.display).toBe('block')
|
|
67
|
+
map.getPixelFromCoordinate = () => null // e.g. mid view transition
|
|
68
|
+
handler.updateTargetPosition()
|
|
69
|
+
expect(target.style.display).toBe('none')
|
|
70
|
+
handler.destroy()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('a drag applies snapping to the moved coordinate when a snap manager is active', () => {
|
|
74
|
+
const snap = { apply: jest.fn((c) => [c[0] + 5, c[1]]), hideIndicator: jest.fn() }
|
|
75
|
+
const { state, handler, touch } = setup({}, { snap })
|
|
76
|
+
handler.updateTargetPosition()
|
|
77
|
+
touch('touchstart', 100, 0)
|
|
78
|
+
touch('touchmove', 120, 10)
|
|
79
|
+
expect(snap.apply).toHaveBeenCalled()
|
|
80
|
+
expect(snap.hideIndicator).toHaveBeenCalled()
|
|
81
|
+
expect(state.vertices[1]).toEqual([125, 10]) // dragged to [120,10] then snapped +5 on x
|
|
82
|
+
handler.destroy()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('a touch that drifts too far before lifting is not treated as a tap', () => {
|
|
86
|
+
const { container, handler, onTap } = setup({ selectedVertexIndex: -1 })
|
|
87
|
+
container.dispatchEvent(domEvent('touchstart', { touches: [{ clientX: 10, clientY: 10 }] }))
|
|
88
|
+
container.dispatchEvent(domEvent('touchend', { changedTouches: [{ clientX: 60, clientY: 60 }] })) // moved past the tap threshold
|
|
89
|
+
expect(onTap).not.toHaveBeenCalled()
|
|
90
|
+
handler.destroy()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('a drag that ends after its vertex has vanished commits nothing', () => {
|
|
94
|
+
const { state, handler, touch, onVertexMoved } = setup()
|
|
95
|
+
handler.updateTargetPosition()
|
|
96
|
+
touch('touchstart', 100, 0)
|
|
97
|
+
touch('touchmove', 120, 10)
|
|
98
|
+
state.vertices = [] // the vertex is gone by the time the finger lifts
|
|
99
|
+
touch('touchend', 120, 10)
|
|
100
|
+
expect(onVertexMoved).not.toHaveBeenCalled()
|
|
101
|
+
handler.destroy()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('a postrender with nothing selected leaves the target hidden', () => {
|
|
105
|
+
const { map, container, handler } = setup({ selectedVertexIndex: -1 })
|
|
106
|
+
const target = container.querySelector('[data-im-draw-touch-target]')
|
|
107
|
+
map.emit('postrender')
|
|
108
|
+
expect(target.style.display).toBe('none')
|
|
109
|
+
handler.destroy()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('the CSS transform reflects a scaled viewport when element widths are measurable', () => {
|
|
113
|
+
const map = createFakeMap()
|
|
114
|
+
const container = createContainer()
|
|
115
|
+
const vp = map.getViewport()
|
|
116
|
+
Object.defineProperty(vp, 'offsetWidth', { value: 200, configurable: true })
|
|
117
|
+
Object.defineProperty(container, 'offsetWidth', { value: 100, configurable: true })
|
|
118
|
+
vp.getBoundingClientRect = () => ({ width: 300, height: 300, left: 0, top: 0 })
|
|
119
|
+
container.getBoundingClientRect = () => ({ width: 100, height: 100, left: 0, top: 0 })
|
|
120
|
+
const state = {
|
|
121
|
+
olFeature: polygonFeature(RING),
|
|
122
|
+
selectedVertexIndex: 1,
|
|
123
|
+
selectedVertexType: 'vertex',
|
|
124
|
+
vertices: [[0, 0], [100, 0], [100, 100], [0, 100]],
|
|
125
|
+
midpoints: [],
|
|
126
|
+
interfaceType: 'touch'
|
|
127
|
+
}
|
|
128
|
+
const handler = createTouchHandler({ map, container, getState: () => state, setState: jest.fn(), onVertexMoved: jest.fn(), onTap: jest.fn(), colors: {}, snap: null })
|
|
129
|
+
handler.updateTargetPosition() // vertex [100,0] -> pixel {100,0} -> scale 1.5 -> css left 150px
|
|
130
|
+
const target = container.querySelector('[data-im-draw-touch-target]')
|
|
131
|
+
expect(target.style.left).toBe('150px')
|
|
132
|
+
handler.destroy()
|
|
133
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getRingSegments,
|
|
3
|
+
getSegmentForIndex,
|
|
4
|
+
getModifiableCoords
|
|
5
|
+
} from '../utils/geometryHelpers.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Undo handlers for edit mode.
|
|
9
|
+
* Each operation receives the OL feature and the saved op payload,
|
|
10
|
+
* mutates the geometry, and returns the vertex index to re-select (or -1).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const undoMoveVertex = (olFeature, op) => {
|
|
14
|
+
const { vertexIndex, previousCoord } = op
|
|
15
|
+
const geom = olFeature.getGeometry()
|
|
16
|
+
const geojsonGeom = { type: geom.getType(), coordinates: geom.getCoordinates() }
|
|
17
|
+
const segments = getRingSegments(geojsonGeom)
|
|
18
|
+
const result = getSegmentForIndex(segments, vertexIndex)
|
|
19
|
+
if (!result) {
|
|
20
|
+
return -1
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ring = getModifiableCoords(geojsonGeom, result.segment.path)
|
|
24
|
+
ring[result.localIdx] = [...previousCoord]
|
|
25
|
+
if (result.segment.closed && result.localIdx === 0) {
|
|
26
|
+
ring[ring.length - 1] = [...previousCoord]
|
|
27
|
+
}
|
|
28
|
+
geom.setCoordinates(geojsonGeom.coordinates)
|
|
29
|
+
return vertexIndex
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const undoInsertVertex = (olFeature, op) => {
|
|
33
|
+
const { vertexIndex } = op
|
|
34
|
+
const geom = olFeature.getGeometry()
|
|
35
|
+
const geojsonGeom = { type: geom.getType(), coordinates: geom.getCoordinates() }
|
|
36
|
+
const segments = getRingSegments(geojsonGeom)
|
|
37
|
+
const result = getSegmentForIndex(segments, vertexIndex)
|
|
38
|
+
if (result) {
|
|
39
|
+
const ring = getModifiableCoords(geojsonGeom, result.segment.path)
|
|
40
|
+
ring.splice(result.localIdx, 1)
|
|
41
|
+
if (result.segment.closed) {
|
|
42
|
+
ring[ring.length - 1] = [...ring[0]]
|
|
43
|
+
}
|
|
44
|
+
geom.setCoordinates(geojsonGeom.coordinates)
|
|
45
|
+
}
|
|
46
|
+
// Undoing an insert removes the vertex, so there is never one to re-select
|
|
47
|
+
return -1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const undoDeleteVertex = (olFeature, op) => {
|
|
51
|
+
const { vertexIndex, deletedCoord } = op
|
|
52
|
+
const geom = olFeature.getGeometry()
|
|
53
|
+
const geojsonGeom = { type: geom.getType(), coordinates: geom.getCoordinates() }
|
|
54
|
+
const segments = getRingSegments(geojsonGeom)
|
|
55
|
+
|
|
56
|
+
let result = getSegmentForIndex(segments, vertexIndex)
|
|
57
|
+
// Vertex might be at a segment boundary after deletion shifted indices
|
|
58
|
+
if (!result) {
|
|
59
|
+
for (const seg of segments) {
|
|
60
|
+
if (vertexIndex === seg.start + seg.length) {
|
|
61
|
+
result = { segment: seg, localIdx: seg.length }
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!result) {
|
|
67
|
+
return -1
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ring = getModifiableCoords(geojsonGeom, result.segment.path)
|
|
71
|
+
ring.splice(result.localIdx, 0, [...deletedCoord])
|
|
72
|
+
if (result.segment.closed) {
|
|
73
|
+
ring[ring.length - 1] = [...ring[0]]
|
|
74
|
+
}
|
|
75
|
+
geom.setCoordinates(geojsonGeom.coordinates)
|
|
76
|
+
return vertexIndex
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Dispatch the correct undo handler based on operation type.
|
|
81
|
+
* @returns {number} vertex index to re-select after undo, or -1 for none
|
|
82
|
+
*/
|
|
83
|
+
export const applyUndo = (olFeature, op) => {
|
|
84
|
+
switch (op.type) {
|
|
85
|
+
case 'move_vertex':
|
|
86
|
+
return undoMoveVertex(olFeature, op)
|
|
87
|
+
case 'insert_vertex':
|
|
88
|
+
return undoInsertVertex(olFeature, op)
|
|
89
|
+
case 'delete_vertex':
|
|
90
|
+
return undoDeleteVertex(olFeature, op)
|
|
91
|
+
default:
|
|
92
|
+
return -1
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { applyUndo, undoMoveVertex, undoInsertVertex, undoDeleteVertex } from './undoOps.js'
|
|
2
|
+
import { polygonFeature, lineFeature } from '../__helpers__/harness.js'
|
|
3
|
+
|
|
4
|
+
const SQUARE = [[0, 0], [100, 0], [100, 100], [0, 100], [0, 0]]
|
|
5
|
+
const ring = (feature) => feature.getGeometry().getCoordinates()[0]
|
|
6
|
+
|
|
7
|
+
describe('undoMoveVertex', () => {
|
|
8
|
+
test('restores the previous coordinate, syncing the closing coord for ring vertex 0', () => {
|
|
9
|
+
const feature = polygonFeature(SQUARE)
|
|
10
|
+
expect(undoMoveVertex(feature, { vertexIndex: 0, previousCoord: [5, 5] })).toBe(0)
|
|
11
|
+
expect(ring(feature)[0]).toEqual([5, 5])
|
|
12
|
+
expect(ring(feature).at(-1)).toEqual([5, 5])
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('returns -1 for an index outside the geometry', () => {
|
|
16
|
+
expect(undoMoveVertex(polygonFeature(SQUARE), { vertexIndex: 99, previousCoord: [0, 0] })).toBe(-1)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('undoInsertVertex', () => {
|
|
21
|
+
test('removes the inserted vertex and keeps the ring closed', () => {
|
|
22
|
+
const feature = polygonFeature([[0, 0], [50, 0], [100, 0], [100, 100], [0, 100], [0, 0]])
|
|
23
|
+
expect(undoInsertVertex(feature, { vertexIndex: 1 })).toBe(-1)
|
|
24
|
+
expect(ring(feature)).toEqual(SQUARE)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('returns -1 for an index outside the geometry', () => {
|
|
28
|
+
expect(undoInsertVertex(polygonFeature(SQUARE), { vertexIndex: 99 })).toBe(-1)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('removes the inserted vertex from an open line without a closing-coordinate sync', () => {
|
|
32
|
+
const feature = lineFeature([[0, 0], [50, 0], [100, 0]])
|
|
33
|
+
expect(undoInsertVertex(feature, { vertexIndex: 1 })).toBe(-1)
|
|
34
|
+
expect(feature.getGeometry().getCoordinates()).toEqual([[0, 0], [100, 0]])
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('undoDeleteVertex', () => {
|
|
39
|
+
test('re-inserts the deleted coordinate and re-selects it', () => {
|
|
40
|
+
const feature = polygonFeature([[0, 0], [100, 0], [0, 100], [0, 0]]) // square minus [100,100]
|
|
41
|
+
expect(undoDeleteVertex(feature, { vertexIndex: 2, deletedCoord: [100, 100] })).toBe(2)
|
|
42
|
+
expect(ring(feature)).toEqual([[0, 0], [100, 0], [100, 100], [0, 100], [0, 0]])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('re-inserts at a segment boundary (the deleted vertex was the last of its part)', () => {
|
|
46
|
+
const feature = lineFeature([[0, 0], [10, 0]]) // [20,0] was deleted from the end
|
|
47
|
+
expect(undoDeleteVertex(feature, { vertexIndex: 2, deletedCoord: [20, 0] })).toBe(2)
|
|
48
|
+
expect(feature.getGeometry().getCoordinates()).toEqual([[0, 0], [10, 0], [20, 0]])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('returns -1 when the index fits no segment at all', () => {
|
|
52
|
+
expect(undoDeleteVertex(lineFeature([[0, 0], [10, 0]]), { vertexIndex: 99, deletedCoord: [0, 0] })).toBe(-1)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('applyUndo dispatch', () => {
|
|
57
|
+
test('routes each operation type and ignores unknown ones', () => {
|
|
58
|
+
const feature = polygonFeature(SQUARE)
|
|
59
|
+
expect(applyUndo(feature, { type: 'move_vertex', vertexIndex: 1, previousCoord: [90, 0] })).toBe(1)
|
|
60
|
+
expect(applyUndo(feature, { type: 'delete_vertex', vertexIndex: 1, deletedCoord: [95, 0] })).toBe(1)
|
|
61
|
+
expect(applyUndo(feature, { type: 'insert_vertex', vertexIndex: 1 })).toBe(-1)
|
|
62
|
+
expect(applyUndo(feature, { type: 'resize' })).toBe(-1)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { coordToPixel, pixelDist } from '../utils/olCoords.js'
|
|
2
|
+
|
|
3
|
+
// Also used as the Modify interaction's pixelTolerance so pointer hit
|
|
4
|
+
// detection and OL's drag activation agree on what "on a handle" means
|
|
5
|
+
export const PIXEL_TOLERANCE = 12
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Find the nearest vertex to a screen pixel within tolerance.
|
|
9
|
+
*
|
|
10
|
+
* @param {import('ol/Map').default} map
|
|
11
|
+
* @param {number[][]} vertices - flat coordinate array [[e,n], ...]
|
|
12
|
+
* @param {{ x: number, y: number }} pixel
|
|
13
|
+
* @param {number} [tolerance]
|
|
14
|
+
* @returns {{ index: number, type: 'vertex' } | null}
|
|
15
|
+
*/
|
|
16
|
+
export const findNearestVertex = (map, vertices, pixel, tolerance = PIXEL_TOLERANCE) => {
|
|
17
|
+
let bestIdx = -1
|
|
18
|
+
let bestDist = tolerance
|
|
19
|
+
|
|
20
|
+
vertices.forEach((coord, i) => {
|
|
21
|
+
const px = coordToPixel(map, coord)
|
|
22
|
+
if (!px) {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
const d = pixelDist(px, pixel)
|
|
26
|
+
if (d < bestDist) {
|
|
27
|
+
bestDist = d
|
|
28
|
+
bestIdx = i
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return bestIdx >= 0 ? { index: bestIdx, type: 'vertex' } : null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Find the nearest midpoint to a screen pixel within tolerance.
|
|
37
|
+
*
|
|
38
|
+
* @param {import('ol/Map').default} map
|
|
39
|
+
* @param {number[][]} midpoints - midpoint coordinate array
|
|
40
|
+
* @param {{ x: number, y: number }} pixel
|
|
41
|
+
* @param {number} vertexCount - number of actual vertices (midpoint index offset)
|
|
42
|
+
* @param {number} [tolerance]
|
|
43
|
+
* @returns {{ index: number, type: 'midpoint' } | null}
|
|
44
|
+
*/
|
|
45
|
+
export const findNearestMidpoint = (map, midpoints, pixel, vertexCount, tolerance = PIXEL_TOLERANCE) => {
|
|
46
|
+
let bestIdx = -1
|
|
47
|
+
let bestDist = tolerance
|
|
48
|
+
|
|
49
|
+
midpoints.forEach((coord, i) => {
|
|
50
|
+
const px = coordToPixel(map, coord)
|
|
51
|
+
if (!px) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
const d = pixelDist(px, pixel)
|
|
55
|
+
if (d < bestDist) {
|
|
56
|
+
bestDist = d
|
|
57
|
+
bestIdx = i
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return bestIdx >= 0 ? { index: vertexCount + bestIdx, type: 'midpoint' } : null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find the nearest vertex or midpoint to a pixel.
|
|
66
|
+
* Vertices take priority when equidistant.
|
|
67
|
+
*
|
|
68
|
+
* @param {number} [tolerance]
|
|
69
|
+
* @returns {{ index: number, type: 'vertex'|'midpoint' } | null}
|
|
70
|
+
*/
|
|
71
|
+
export const findNearest = (map, vertices, midpoints, pixel, tolerance = PIXEL_TOLERANCE) => {
|
|
72
|
+
return findNearestVertex(map, vertices, pixel, tolerance) ??
|
|
73
|
+
findNearestMidpoint(map, midpoints, pixel, vertices.length, tolerance)
|
|
74
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { findNearest, findNearestVertex, findNearestMidpoint } from './vertexHitTest.js'
|
|
2
|
+
import { createFakeMap } from '../__helpers__/harness.js'
|
|
3
|
+
|
|
4
|
+
const map = createFakeMap()
|
|
5
|
+
const vertices = [[0, 0], [100, 0]]
|
|
6
|
+
const midpoints = [[50, 0]]
|
|
7
|
+
|
|
8
|
+
test('finds the nearest vertex within the pixel tolerance', () => {
|
|
9
|
+
expect(findNearestVertex(map, vertices, { x: 98, y: 5 })).toEqual({ index: 1, type: 'vertex' })
|
|
10
|
+
expect(findNearestVertex(map, vertices, { x: 50, y: 50 })).toBeNull()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('midpoint hits are offset by the vertex count', () => {
|
|
14
|
+
expect(findNearestMidpoint(map, midpoints, { x: 51, y: 2 }, 2)).toEqual({ index: 2, type: 'midpoint' })
|
|
15
|
+
expect(findNearestMidpoint(map, midpoints, { x: 200, y: 0 }, 2)).toBeNull()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('vertices take priority over midpoints; nothing in range gives null', () => {
|
|
19
|
+
expect(findNearest(map, [[50, 4]], midpoints, { x: 50, y: 2 })).toEqual({ index: 0, type: 'vertex' })
|
|
20
|
+
expect(findNearest(map, vertices, midpoints, { x: 51, y: 2 })).toEqual({ index: 2, type: 'midpoint' })
|
|
21
|
+
expect(findNearest(map, vertices, midpoints, { x: 500, y: 500 })).toBeNull()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('coordinates that cannot be projected are skipped', () => {
|
|
25
|
+
const blindMap = { getPixelFromCoordinate: () => null }
|
|
26
|
+
expect(findNearestVertex(blindMap, vertices, { x: 0, y: 0 })).toBeNull()
|
|
27
|
+
expect(findNearestMidpoint(blindMap, midpoints, { x: 50, y: 0 }, 2)).toBeNull()
|
|
28
|
+
})
|