@defra/interactive-map 0.0.37-alpha → 0.0.38-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/templates/draw-tools.njk +4 -3
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api.md +32 -0
- package/docs/examples/draw-tools.mdx +0 -4
- package/docs/plugins/draw.md +482 -0
- package/docs/plugins.md +2 -6
- package/govuk-prototype-kit.config.json +3 -2
- package/package.json +2 -4
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/css/index.css +1 -97
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -0
- package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
- package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
- package/plugins/draw/dist/esm/index.js +1 -0
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
- package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
- package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
- package/plugins/draw/dist/umd/index.js +2 -0
- package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
- package/plugins/draw/src/DrawInit.jsx +89 -0
- package/plugins/draw/src/DrawInit.test.jsx +184 -0
- package/plugins/draw/src/adapterEvents.js +127 -0
- package/plugins/draw/src/adapterEvents.test.js +20 -0
- package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
- package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
- package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
- package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
- package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
- package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
- package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
- package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
- package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
- package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
- package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
- package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
- package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
- package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
- package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
- package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
- package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
- package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
- package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
- package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
- package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
- package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
- package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
- package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
- package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
- package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
- package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
- package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
- package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
- package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
- package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
- package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
- package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
- package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
- package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
- package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
- package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
- package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
- package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
- package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
- package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
- package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
- package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
- package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
- package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
- package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
- package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
- package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
- package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
- package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
- package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
- package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
- package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
- package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
- package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
- package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
- package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
- package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
- package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
- package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
- package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
- package/plugins/draw/src/api/addFeature.js +22 -0
- package/plugins/draw/src/api/addFeature.test.js +41 -0
- package/plugins/draw/src/api/deleteFeature.js +11 -0
- package/plugins/draw/src/api/deleteFeature.test.js +24 -0
- package/plugins/draw/src/api/editFeature.js +54 -0
- package/plugins/draw/src/api/editFeature.test.js +134 -0
- package/plugins/draw/src/api/merge.js +30 -0
- package/plugins/draw/src/api/merge.test.js +57 -0
- package/plugins/draw/src/api/newLine.js +41 -0
- package/plugins/draw/src/api/newLine.test.js +93 -0
- package/plugins/draw/src/api/newPolygon.js +41 -0
- package/plugins/draw/src/api/newPolygon.test.js +93 -0
- package/plugins/draw/src/api/split.js +117 -0
- package/plugins/draw/src/api/split.test.js +204 -0
- package/plugins/draw/src/defaults.js +52 -0
- package/plugins/draw/src/defaults.test.js +18 -0
- package/plugins/draw/src/draw.scss +43 -0
- package/plugins/draw/src/events.js +237 -0
- package/plugins/draw/src/events.test.js +369 -0
- package/plugins/draw/src/index.js +10 -0
- package/plugins/draw/src/index.test.js +24 -0
- package/plugins/draw/src/manifest.js +170 -0
- package/plugins/draw/src/manifest.test.js +147 -0
- package/plugins/draw/src/reducer.js +69 -0
- package/plugins/draw/src/reducer.test.js +85 -0
- package/plugins/draw/src/utils/debounce.js +16 -0
- package/plugins/draw/src/utils/debounce.test.js +40 -0
- package/plugins/draw/src/utils/eventBus.js +36 -0
- package/plugins/draw/src/utils/eventBus.test.js +47 -0
- package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
- package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
- package/plugins/draw/src/utils/getValueForStyle.js +33 -0
- package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
- package/plugins/draw/src/utils/resolveColors.js +39 -0
- package/plugins/draw/src/utils/resolveColors.test.js +34 -0
- package/plugins/draw/src/utils/spatial.js +243 -0
- package/plugins/draw/src/utils/spatial.test.js +243 -0
- package/plugins/draw/src/utils/touchTarget.js +95 -0
- package/plugins/draw/src/utils/touchTarget.test.js +107 -0
- package/plugins/draw/src/utils/undoStack.js +31 -0
- package/plugins/draw/src/utils/undoStack.test.js +49 -0
- package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
- package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
- package/plugins/draw/src/validation/liveStroke.js +85 -0
- package/plugins/draw/src/validation/liveStroke.test.js +150 -0
- package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
- package/plugins/draw/src/validation/rules.js +186 -0
- package/plugins/draw/src/validation/rules.test.js +120 -0
- package/plugins/draw/src/validation/validateGeometry.js +119 -0
- package/plugins/draw/src/validation/validateGeometry.test.js +200 -0
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/src/hooks/useInteractionHandlers.js +1 -1
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
- package/plugins/interact/src/utils/buildStylesMap.js +1 -0
- package/plugins/search/dist/css/index.css +1 -317
- package/rollup.esm.mjs +35 -28
- package/sonar-project.properties +2 -2
- package/src/App/components/Attributions/Attributions.module.scss +1 -0
- package/src/App/components/Hints/Hints.module.scss +5 -4
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
- package/src/App/components/MoveControl/MoveControl.jsx +73 -9
- package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
- package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
- package/src/App/hooks/useHintsAPI.js +67 -0
- package/src/App/hooks/useHintsAPI.test.js +145 -0
- package/src/App/renderer/PluginInits.jsx +4 -0
- package/src/App/renderer/PluginInits.test.jsx +7 -1
- package/src/InteractiveMap/InteractiveMap.js +20 -0
- package/src/InteractiveMap/InteractiveMap.test.js +7 -0
- package/src/config/events.js +4 -0
- package/src/scss/settings/_dimensions.scss +3 -0
- package/src/services/announcer.js +38 -3
- package/src/services/announcer.test.js +133 -1
- package/src/services/hints.js +3 -0
- package/src/services/hints.test.js +19 -0
- package/src/types.js +12 -0
- package/src/utils/detectInterfaceType.js +5 -1
- package/src/utils/isMac.js +15 -0
- package/src/utils/isMac.test.js +37 -0
- package/webpack.umd.mjs +1 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import VectorSource from 'ol/source/Vector.js'
|
|
2
|
+
import VectorLayer from 'ol/layer/Vector.js'
|
|
3
|
+
import Feature from 'ol/Feature.js'
|
|
4
|
+
import Point from 'ol/geom/Point.js'
|
|
5
|
+
|
|
6
|
+
// Above the vertex (102) and midpoint (101) handle layers
|
|
7
|
+
const ACTIVE_LAYER_Z_INDEX = 103
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Overlay layer rendering the currently selected vertex or midpoint handle.
|
|
11
|
+
* The vertex/midpoint layers hide their own circle at the selected index and
|
|
12
|
+
* this layer draws the highlighted version on top.
|
|
13
|
+
*
|
|
14
|
+
* @param {import('ol/Map').default} map
|
|
15
|
+
* @param {() => object} getStyles - returns the manager's current style set
|
|
16
|
+
* (read at render time so style hot-swaps apply without rewiring)
|
|
17
|
+
* @returns {{ update: (state) => void, remove: () => void }}
|
|
18
|
+
*/
|
|
19
|
+
export const createActiveVertexLayer = (map, getStyles) => {
|
|
20
|
+
const source = new VectorSource()
|
|
21
|
+
const layer = new VectorLayer({ source, zIndex: ACTIVE_LAYER_Z_INDEX })
|
|
22
|
+
map.addLayer(layer)
|
|
23
|
+
|
|
24
|
+
const selectedCoordAndStyle = ({ selectedVertexIndex, selectedVertexType, vertices, midpoints }) => {
|
|
25
|
+
const styles = getStyles()
|
|
26
|
+
if (selectedVertexType === 'vertex') {
|
|
27
|
+
return { coord: vertices[selectedVertexIndex], style: styles.selectedVertexStyle }
|
|
28
|
+
}
|
|
29
|
+
if (selectedVertexType === 'midpoint') {
|
|
30
|
+
return { coord: midpoints[selectedVertexIndex - vertices.length], style: styles.selectedMidpointStyle }
|
|
31
|
+
}
|
|
32
|
+
return { coord: null, style: null }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
update (state) {
|
|
37
|
+
source.clear()
|
|
38
|
+
if (state.selectedVertexIndex < 0) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
const { coord, style } = selectedCoordAndStyle(state)
|
|
42
|
+
if (!coord) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
const feature = new Feature({ geometry: new Point(coord) })
|
|
46
|
+
feature.setStyle(style)
|
|
47
|
+
source.addFeature(feature)
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
remove () {
|
|
51
|
+
source.clear()
|
|
52
|
+
map.removeLayer(layer)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createActiveVertexLayer } from './activeVertexLayer.js'
|
|
2
|
+
import { createFakeMap, createFakeManager } from '../__helpers__/harness.js'
|
|
3
|
+
|
|
4
|
+
const setup = () => {
|
|
5
|
+
const map = createFakeMap()
|
|
6
|
+
const manager = createFakeManager()
|
|
7
|
+
const activeLayer = createActiveVertexLayer(map, () => manager.styles)
|
|
8
|
+
const source = map.layers[0].getSource()
|
|
9
|
+
const stateWith = (overrides) => ({
|
|
10
|
+
selectedVertexIndex: -1,
|
|
11
|
+
selectedVertexType: null,
|
|
12
|
+
vertices: [[0, 0], [10, 0], [10, 10]],
|
|
13
|
+
midpoints: [[5, 0], [10, 5], [5, 5]],
|
|
14
|
+
...overrides
|
|
15
|
+
})
|
|
16
|
+
return { map, manager, activeLayer, source, stateWith }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test('renders the selected vertex with the selected-vertex style', () => {
|
|
20
|
+
const { activeLayer, source, manager, stateWith } = setup()
|
|
21
|
+
activeLayer.update(stateWith({ selectedVertexIndex: 1, selectedVertexType: 'vertex' }))
|
|
22
|
+
const features = source.getFeatures()
|
|
23
|
+
expect(features).toHaveLength(1)
|
|
24
|
+
expect(features[0].getGeometry().getCoordinates()).toEqual([10, 0])
|
|
25
|
+
expect(features[0].getStyle()).toBe(manager.styles.selectedVertexStyle)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('renders a selected midpoint by its local index with the midpoint style', () => {
|
|
29
|
+
const { activeLayer, source, manager, stateWith } = setup()
|
|
30
|
+
activeLayer.update(stateWith({ selectedVertexIndex: 4, selectedVertexType: 'midpoint' }))
|
|
31
|
+
const features = source.getFeatures()
|
|
32
|
+
expect(features[0].getGeometry().getCoordinates()).toEqual([10, 5])
|
|
33
|
+
expect(features[0].getStyle()).toBe(manager.styles.selectedMidpointStyle)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('renders nothing without a selection, for an unknown type, or for a missing coordinate', () => {
|
|
37
|
+
const { activeLayer, source, stateWith } = setup()
|
|
38
|
+
activeLayer.update(stateWith({ selectedVertexIndex: 1, selectedVertexType: 'vertex' }))
|
|
39
|
+
activeLayer.update(stateWith({}))
|
|
40
|
+
expect(source.getFeatures()).toHaveLength(0)
|
|
41
|
+
activeLayer.update(stateWith({ selectedVertexIndex: 0, selectedVertexType: 'segment' }))
|
|
42
|
+
expect(source.getFeatures()).toHaveLength(0)
|
|
43
|
+
activeLayer.update(stateWith({ selectedVertexIndex: 99, selectedVertexType: 'vertex' }))
|
|
44
|
+
expect(source.getFeatures()).toHaveLength(0)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('remove clears the source and detaches the layer', () => {
|
|
48
|
+
const { map, activeLayer, source, stateWith } = setup()
|
|
49
|
+
activeLayer.update(stateWith({ selectedVertexIndex: 0, selectedVertexType: 'vertex' }))
|
|
50
|
+
activeLayer.remove()
|
|
51
|
+
expect(source.getFeatures()).toHaveLength(0)
|
|
52
|
+
expect(map.removeLayer).toHaveBeenCalled()
|
|
53
|
+
})
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { coordToPixel } from '../utils/olCoords.js'
|
|
2
|
+
import { spatialNavigate } from '../../../utils/spatial.js'
|
|
3
|
+
import { wireNudge } from './nudge.js'
|
|
4
|
+
|
|
5
|
+
const ARROW_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'])
|
|
6
|
+
const INTERACTIVE_TAGS = new Set(['INPUT', 'TEXTAREA', 'BUTTON', 'SELECT', 'A'])
|
|
7
|
+
|
|
8
|
+
const selectNearest = (map, getState, setState) => {
|
|
9
|
+
const { vertices, midpoints } = getState()
|
|
10
|
+
if (!vertices.length) {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
const centerPx = coordToPixel(map, map.getView().getCenter())
|
|
14
|
+
if (!centerPx) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
const allPixels = [
|
|
18
|
+
...vertices.map(c => coordToPixel(map, c)),
|
|
19
|
+
...midpoints.map(c => coordToPixel(map, c))
|
|
20
|
+
].filter(Boolean).map(p => [p.x, p.y])
|
|
21
|
+
const idx = spatialNavigate([centerPx.x, centerPx.y], allPixels, undefined)
|
|
22
|
+
setState({ selectedVertexIndex: idx, selectedVertexType: idx < vertices.length ? 'vertex' : 'midpoint' })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const navigateTo = (direction, map, getState, setState) => {
|
|
26
|
+
const { selectedVertexIndex, vertices, midpoints } = getState()
|
|
27
|
+
if (!vertices.length) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
const allCoords = [...vertices, ...midpoints]
|
|
31
|
+
const allPixels = allCoords.map(c => coordToPixel(map, c)).filter(Boolean).map(p => [p.x, p.y])
|
|
32
|
+
const startPx = selectedVertexIndex >= 0
|
|
33
|
+
? allPixels[selectedVertexIndex]
|
|
34
|
+
: (() => {
|
|
35
|
+
const c = coordToPixel(map, map.getView().getCenter())
|
|
36
|
+
return c ? [c.x, c.y] : null
|
|
37
|
+
})()
|
|
38
|
+
if (!startPx) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
const idx = spatialNavigate(startPx, allPixels, direction)
|
|
42
|
+
setState({ selectedVertexIndex: idx, selectedVertexType: idx < vertices.length ? 'vertex' : 'midpoint' })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const isInteractiveElementFocused = (appViewport) => {
|
|
46
|
+
const el = document.activeElement
|
|
47
|
+
if (!el || el === document.body) {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
if (appViewport.contains(el)) {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
return INTERACTIVE_TAGS.has(el.tagName) || el.isContentEditable || el.hasAttribute('tabindex')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const buildKeydownHandler = ({ map, getState, setState, nudge, keyMove, onUndo, onKeyboardActive, isFocused }) => {
|
|
57
|
+
const handleArrowKey = (e) => {
|
|
58
|
+
if (e.altKey) {
|
|
59
|
+
e.preventDefault()
|
|
60
|
+
e.stopPropagation()
|
|
61
|
+
navigateTo(e.key, map, getState, setState)
|
|
62
|
+
} else if (getState().selectedVertexIndex >= 0) {
|
|
63
|
+
e.preventDefault()
|
|
64
|
+
e.stopPropagation()
|
|
65
|
+
nudge(e)
|
|
66
|
+
} else {
|
|
67
|
+
// No action: arrow with no selection and no alt modifier
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const handleKey = (e) => {
|
|
72
|
+
onKeyboardActive?.()
|
|
73
|
+
if (e.key === ' ') {
|
|
74
|
+
e.preventDefault()
|
|
75
|
+
if (getState().selectedVertexIndex < 0) {
|
|
76
|
+
selectNearest(map, getState, setState)
|
|
77
|
+
}
|
|
78
|
+
} else if (ARROW_KEYS.has(e.key)) {
|
|
79
|
+
handleArrowKey(e)
|
|
80
|
+
} else if (e.key === 'z' && (e.metaKey || e.ctrlKey)) {
|
|
81
|
+
const tag = document.activeElement?.tagName
|
|
82
|
+
if (!INTERACTIVE_TAGS.has(tag)) {
|
|
83
|
+
e.preventDefault()
|
|
84
|
+
e.stopPropagation()
|
|
85
|
+
onUndo()
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
// No action
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (e) => {
|
|
93
|
+
if (isFocused()) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (e.key === 'Escape' && getState().selectedVertexIndex >= 0) {
|
|
97
|
+
e.preventDefault()
|
|
98
|
+
keyMove.start = null
|
|
99
|
+
keyMove.index = null
|
|
100
|
+
setState({ selectedVertexIndex: -1, selectedVertexType: null })
|
|
101
|
+
} else {
|
|
102
|
+
handleKey(e)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const buildKeyupHandler = ({ snap, keyMove, onVertexMoved, onDeleted, isFocused }) => (e) => {
|
|
108
|
+
if (isFocused()) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
if (ARROW_KEYS.has(e.key) && keyMove.start && keyMove.index != null) {
|
|
112
|
+
snap?.hideIndicator()
|
|
113
|
+
onVertexMoved({ vertexIndex: keyMove.index, previousCoord: keyMove.start })
|
|
114
|
+
keyMove.start = null
|
|
115
|
+
keyMove.index = null
|
|
116
|
+
}
|
|
117
|
+
if (e.key === 'Delete') {
|
|
118
|
+
onDeleted()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Keyboard handler for edit mode.
|
|
124
|
+
*
|
|
125
|
+
* Space — select nearest vertex or midpoint to crosshair (only when nothing selected)
|
|
126
|
+
* Alt+Arrow — navigate to next vertex/midpoint in that direction (requires selection)
|
|
127
|
+
* Arrow — move selected vertex; if midpoint selected, inserts it as a vertex and moves it
|
|
128
|
+
* Shift+Arrow — same but fine nudge (1px vs 5px)
|
|
129
|
+
* Delete — delete selected vertex (no-op on midpoints)
|
|
130
|
+
* Ctrl/Cmd+Z — undo
|
|
131
|
+
*
|
|
132
|
+
* Midpoints remain midpoints until moved — navigating to a midpoint (Space/Alt+Arrow) does not
|
|
133
|
+
* convert it. Only pressing a plain/Shift arrow converts it.
|
|
134
|
+
*
|
|
135
|
+
* @param {{ map, getState, setState, snap, onVertexMoved, onInserted, onDeleted, onUndo, onKeyboardActive }} options
|
|
136
|
+
* @returns {{ nudgeByDelta: (dx: number, dy: number, isLargeStep: boolean) => void, destroy: () => void }}
|
|
137
|
+
*/
|
|
138
|
+
export const createKeyboardHandler = (options) => {
|
|
139
|
+
const { map, snap, getState, setState, onVertexMoved, onInserted, onDeleted, onUndo, onKeyboardActive } = options
|
|
140
|
+
const { nudge, keyMove, nudgeByDelta } = wireNudge({ map, snap, getState, setState, onInserted, onVertexMoved })
|
|
141
|
+
const appViewport = map.getViewport().closest('[role="application"]') ?? map.getViewport()
|
|
142
|
+
const isFocused = () => isInteractiveElementFocused(appViewport)
|
|
143
|
+
|
|
144
|
+
const onKeydown = buildKeydownHandler({ map, getState, setState, nudge, keyMove, onUndo, onKeyboardActive, isFocused })
|
|
145
|
+
const onKeyup = buildKeyupHandler({ snap, keyMove, onVertexMoved, onDeleted, isFocused })
|
|
146
|
+
|
|
147
|
+
globalThis.addEventListener('keydown', onKeydown, { capture: true })
|
|
148
|
+
globalThis.addEventListener('keyup', onKeyup, { capture: true })
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
nudgeByDelta,
|
|
152
|
+
destroy () {
|
|
153
|
+
globalThis.removeEventListener('keydown', onKeydown, { capture: true })
|
|
154
|
+
globalThis.removeEventListener('keyup', onKeyup, { capture: true })
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { createKeyboardHandler } from './keyboardHandler.js'
|
|
2
|
+
import { createFakeMap, polygonFeature } from '../__helpers__/harness.js'
|
|
3
|
+
|
|
4
|
+
const RING = [[0, 0], [100, 0], [100, 100], [0, 0]]
|
|
5
|
+
|
|
6
|
+
const setup = () => {
|
|
7
|
+
const map = createFakeMap({ center: [98, 98] }) // crosshair near vertex [100, 100]
|
|
8
|
+
const state = {
|
|
9
|
+
olFeature: polygonFeature(RING),
|
|
10
|
+
selectedVertexIndex: -1,
|
|
11
|
+
selectedVertexType: null,
|
|
12
|
+
vertices: [[0, 0], [100, 0], [100, 100]],
|
|
13
|
+
midpoints: [[50, 0], [100, 50], [50, 50]]
|
|
14
|
+
}
|
|
15
|
+
const setState = jest.fn((updates) => Object.assign(state, updates))
|
|
16
|
+
const callbacks = {
|
|
17
|
+
onVertexMoved: jest.fn(),
|
|
18
|
+
onInserted: jest.fn(),
|
|
19
|
+
onDeleted: jest.fn(),
|
|
20
|
+
onUndo: jest.fn(),
|
|
21
|
+
onKeyboardActive: jest.fn()
|
|
22
|
+
}
|
|
23
|
+
const handler = createKeyboardHandler({ map, getState: () => state, setState, snap: null, ...callbacks })
|
|
24
|
+
liveHandlers.push(handler)
|
|
25
|
+
return { map, state, setState, handler, ...callbacks }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const liveHandlers = []
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
liveHandlers.splice(0).forEach((h) => h.destroy())
|
|
31
|
+
document.body.innerHTML = ''
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const key = (type, props) => window.dispatchEvent(new KeyboardEvent(type, { cancelable: true, ...props }))
|
|
35
|
+
|
|
36
|
+
test('nudgeByDelta (exposed for MoveControl) moves the selected vertex and reports it via onVertexMoved', () => {
|
|
37
|
+
const { state, handler, onVertexMoved } = setup()
|
|
38
|
+
Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
|
|
39
|
+
handler.nudgeByDelta(1, 0, true)
|
|
40
|
+
expect(state.vertices[1]).not.toEqual([100, 0])
|
|
41
|
+
expect(onVertexMoved).toHaveBeenCalledWith(expect.objectContaining({ vertexIndex: 1 }))
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('Space selects the vertex or midpoint nearest the crosshair, only when nothing is selected', () => {
|
|
45
|
+
const { state, setState, onKeyboardActive } = setup()
|
|
46
|
+
key('keydown', { key: ' ' })
|
|
47
|
+
expect(state.selectedVertexIndex).toBe(2) // [100, 100] is nearest [98, 98]
|
|
48
|
+
expect(state.selectedVertexType).toBe('vertex')
|
|
49
|
+
expect(onKeyboardActive).toHaveBeenCalled()
|
|
50
|
+
setState.mockClear()
|
|
51
|
+
key('keydown', { key: ' ' })
|
|
52
|
+
expect(setState).not.toHaveBeenCalled()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('Space selects a midpoint when it is the handle nearest the crosshair', () => {
|
|
56
|
+
const map = createFakeMap({ center: [100, 48] }) // crosshair beside midpoint [100, 50]
|
|
57
|
+
const state = {
|
|
58
|
+
olFeature: polygonFeature(RING),
|
|
59
|
+
selectedVertexIndex: -1,
|
|
60
|
+
selectedVertexType: null,
|
|
61
|
+
vertices: [[0, 0], [100, 0], [100, 100]],
|
|
62
|
+
midpoints: [[50, 0], [100, 50], [50, 50]]
|
|
63
|
+
}
|
|
64
|
+
const setState = jest.fn((updates) => Object.assign(state, updates))
|
|
65
|
+
const handler = createKeyboardHandler({
|
|
66
|
+
map,
|
|
67
|
+
getState: () => state,
|
|
68
|
+
setState,
|
|
69
|
+
snap: null,
|
|
70
|
+
onVertexMoved: jest.fn(),
|
|
71
|
+
onInserted: jest.fn(),
|
|
72
|
+
onDeleted: jest.fn(),
|
|
73
|
+
onUndo: jest.fn(),
|
|
74
|
+
onKeyboardActive: jest.fn()
|
|
75
|
+
})
|
|
76
|
+
liveHandlers.push(handler)
|
|
77
|
+
key('keydown', { key: ' ' })
|
|
78
|
+
expect(state.selectedVertexIndex).toBe(4) // midpoint [100, 50]
|
|
79
|
+
expect(state.selectedVertexType).toBe('midpoint')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('Alt+Arrow navigates the selection to the nearest handle in that direction', () => {
|
|
83
|
+
const { state } = setup()
|
|
84
|
+
Object.assign(state, { selectedVertexIndex: 2, selectedVertexType: 'vertex' })
|
|
85
|
+
key('keydown', { key: 'ArrowUp', altKey: true })
|
|
86
|
+
expect(state.selectedVertexIndex).toBe(4) // midpoint [100, 50] is nearest above [100, 100]
|
|
87
|
+
expect(state.selectedVertexType).toBe('midpoint')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('Alt+Arrow with nothing selected navigates relative to the crosshair', () => {
|
|
91
|
+
const { state } = setup()
|
|
92
|
+
key('keydown', { key: 'ArrowUp', altKey: true }) // no selection → start from crosshair [98, 98]
|
|
93
|
+
expect(state.selectedVertexIndex).toBe(4) // midpoint [100, 50] above the crosshair
|
|
94
|
+
expect(state.selectedVertexType).toBe('midpoint')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('Alt+Arrow can land the selection on a vertex', () => {
|
|
98
|
+
const { state } = setup()
|
|
99
|
+
Object.assign(state, { selectedVertexIndex: 4, selectedVertexType: 'midpoint' }) // midpoint [100, 50]
|
|
100
|
+
key('keydown', { key: 'ArrowUp', altKey: true })
|
|
101
|
+
expect(state.selectedVertexIndex).toBe(1) // vertex [100, 0] directly above
|
|
102
|
+
expect(state.selectedVertexType).toBe('vertex')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('a plain arrow nudges the selected vertex, and keyup commits a single move op', () => {
|
|
106
|
+
const { state, onVertexMoved } = setup()
|
|
107
|
+
Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
|
|
108
|
+
key('keydown', { key: 'ArrowRight' })
|
|
109
|
+
key('keydown', { key: 'ArrowRight' })
|
|
110
|
+
expect(state.vertices[1]).toEqual([110, 0])
|
|
111
|
+
|
|
112
|
+
key('keyup', { key: 'ArrowRight' })
|
|
113
|
+
expect(onVertexMoved).toHaveBeenCalledWith({ vertexIndex: 1, previousCoord: [100, 0] })
|
|
114
|
+
key('keyup', { key: 'ArrowRight' }) // no pending move — not committed twice
|
|
115
|
+
expect(onVertexMoved).toHaveBeenCalledTimes(1)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('arrows without a selection do nothing', () => {
|
|
119
|
+
const { setState } = setup()
|
|
120
|
+
key('keydown', { key: 'ArrowRight' })
|
|
121
|
+
expect(setState).not.toHaveBeenCalled()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('Escape clears the selection and any pending nudge', () => {
|
|
125
|
+
const { state, setState, onVertexMoved } = setup()
|
|
126
|
+
Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
|
|
127
|
+
key('keydown', { key: 'ArrowRight' })
|
|
128
|
+
key('keydown', { key: 'Escape' })
|
|
129
|
+
expect(setState).toHaveBeenLastCalledWith({ selectedVertexIndex: -1, selectedVertexType: null })
|
|
130
|
+
key('keyup', { key: 'ArrowRight' })
|
|
131
|
+
expect(onVertexMoved).not.toHaveBeenCalled()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('Delete deletes, ctrl/cmd+z undoes — but not while typing in an input inside the viewport', () => {
|
|
135
|
+
const { map, onDeleted, onUndo } = setup()
|
|
136
|
+
key('keyup', { key: 'Delete' })
|
|
137
|
+
expect(onDeleted).toHaveBeenCalled()
|
|
138
|
+
key('keydown', { key: 'z', ctrlKey: true })
|
|
139
|
+
expect(onUndo).toHaveBeenCalledTimes(1)
|
|
140
|
+
|
|
141
|
+
const input = document.createElement('input')
|
|
142
|
+
map.getViewport().appendChild(input)
|
|
143
|
+
input.focus()
|
|
144
|
+
key('keydown', { key: 'z', ctrlKey: true })
|
|
145
|
+
expect(onUndo).toHaveBeenCalledTimes(1)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('keys are ignored while an interactive element outside the viewport has focus', () => {
|
|
149
|
+
const { onDeleted, onKeyboardActive } = setup()
|
|
150
|
+
const button = document.createElement('button')
|
|
151
|
+
document.body.appendChild(button)
|
|
152
|
+
button.focus()
|
|
153
|
+
key('keyup', { key: 'Delete' })
|
|
154
|
+
key('keydown', { key: ' ' })
|
|
155
|
+
expect(onDeleted).not.toHaveBeenCalled()
|
|
156
|
+
expect(onKeyboardActive).not.toHaveBeenCalled()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('keys are ignored while a tabindex-focusable non-interactive element outside the viewport has focus', () => {
|
|
160
|
+
const { onDeleted } = setup()
|
|
161
|
+
const div = document.createElement('div')
|
|
162
|
+
div.tabIndex = 0 // focusable via tabindex, not one of the interactive tags
|
|
163
|
+
document.body.appendChild(div)
|
|
164
|
+
div.focus()
|
|
165
|
+
key('keyup', { key: 'Delete' })
|
|
166
|
+
expect(onDeleted).not.toHaveBeenCalled()
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test('unhandled keys do nothing', () => {
|
|
170
|
+
const { setState } = setup()
|
|
171
|
+
key('keydown', { key: 'a' })
|
|
172
|
+
expect(setState).not.toHaveBeenCalled()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('destroy removes the window listeners', () => {
|
|
176
|
+
const { handler, onDeleted } = setup()
|
|
177
|
+
handler.destroy()
|
|
178
|
+
key('keyup', { key: 'Delete' })
|
|
179
|
+
expect(onDeleted).not.toHaveBeenCalled()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('selection and navigation are no-ops without handles or without projectable pixels', () => {
|
|
183
|
+
const { state, setState } = setup()
|
|
184
|
+
state.vertices = []
|
|
185
|
+
key('keydown', { key: ' ' })
|
|
186
|
+
key('keydown', { key: 'ArrowUp', altKey: true })
|
|
187
|
+
expect(setState).not.toHaveBeenCalled()
|
|
188
|
+
|
|
189
|
+
const { state: s2, setState: set2, map } = setup()
|
|
190
|
+
map.getPixelFromCoordinate = () => null // e.g. mid view transition
|
|
191
|
+
key('keydown', { key: ' ' })
|
|
192
|
+
key('keydown', { key: 'ArrowUp', altKey: true }) // no selection → start px unprojectable
|
|
193
|
+
expect(set2).not.toHaveBeenCalled()
|
|
194
|
+
expect(s2.selectedVertexIndex).toBe(-1)
|
|
195
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import VectorSource from 'ol/source/Vector.js'
|
|
2
|
+
import VectorLayer from 'ol/layer/Vector.js'
|
|
3
|
+
import Feature from 'ol/Feature.js'
|
|
4
|
+
import Point from 'ol/geom/Point.js'
|
|
5
|
+
import { getMidpoints } from '../utils/geometryHelpers.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Manages a dedicated overlay layer for midpoint handles in edit mode.
|
|
9
|
+
* Midpoints are always visible (unlike OL Modify's native midpoints which
|
|
10
|
+
* only appear when the pointer is near a segment). The selected midpoint is
|
|
11
|
+
* rendered by the separate active-selection layer in EditMode (zIndex 103).
|
|
12
|
+
*/
|
|
13
|
+
export const createMidpointLayer = (map, midpointStyle) => {
|
|
14
|
+
let currentStyle = midpointStyle
|
|
15
|
+
let selectedIndex = -1
|
|
16
|
+
const source = new VectorSource()
|
|
17
|
+
const layer = new VectorLayer({
|
|
18
|
+
source,
|
|
19
|
+
style: (feature) => feature.get('midpointIndex') === selectedIndex ? null : [currentStyle],
|
|
20
|
+
zIndex: 101
|
|
21
|
+
})
|
|
22
|
+
map.addLayer(layer)
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
update (geom) {
|
|
26
|
+
source.clear()
|
|
27
|
+
const midpoints = getMidpoints(geom)
|
|
28
|
+
const features = midpoints.map((coord, i) => {
|
|
29
|
+
const f = new Feature({ geometry: new Point(coord) })
|
|
30
|
+
f.set('midpointIndex', i)
|
|
31
|
+
return f
|
|
32
|
+
})
|
|
33
|
+
source.addFeatures(features)
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
setSelected (index) {
|
|
37
|
+
selectedIndex = index
|
|
38
|
+
source.changed()
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
updateStyle (newMidpointStyle) {
|
|
42
|
+
currentStyle = newMidpointStyle
|
|
43
|
+
source.changed()
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
getCoords () {
|
|
47
|
+
return source.getFeatures()
|
|
48
|
+
.sort((a, b) => a.get('midpointIndex') - b.get('midpointIndex'))
|
|
49
|
+
.map(f => f.getGeometry().getCoordinates())
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
remove () {
|
|
53
|
+
source.clear()
|
|
54
|
+
map.removeLayer(layer)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Style from 'ol/style/Style.js'
|
|
2
|
+
import { createMidpointLayer } from './midpointLayer.js'
|
|
3
|
+
import { createFakeMap } from '../__helpers__/harness.js'
|
|
4
|
+
|
|
5
|
+
const GEOM = { type: 'Polygon', coordinates: [[[0, 0], [10, 0], [10, 10], [0, 0]]] }
|
|
6
|
+
|
|
7
|
+
const setup = () => {
|
|
8
|
+
const map = createFakeMap()
|
|
9
|
+
const style = new Style({})
|
|
10
|
+
const handles = createMidpointLayer(map, style)
|
|
11
|
+
const layer = map.layers[0]
|
|
12
|
+
return { map, style, handles, layer, source: layer.getSource() }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('update renders one handle per segment midpoint, retrievable in index order', () => {
|
|
16
|
+
const { handles, source } = setup()
|
|
17
|
+
handles.update(GEOM)
|
|
18
|
+
expect(source.getFeatures()).toHaveLength(3)
|
|
19
|
+
expect(handles.getCoords()).toEqual([[5, 0], [10, 5], [5, 5]])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('the selected midpoint is hidden here and styles hot-swap', () => {
|
|
23
|
+
const { handles, layer, style, source } = setup()
|
|
24
|
+
handles.update(GEOM)
|
|
25
|
+
handles.setSelected(0)
|
|
26
|
+
const styleFn = layer.getStyle()
|
|
27
|
+
const featureAt = (i) => source.getFeatures().find(f => f.get('midpointIndex') === i)
|
|
28
|
+
expect(styleFn(featureAt(0))).toBeNull()
|
|
29
|
+
expect(styleFn(featureAt(1))).toEqual([style])
|
|
30
|
+
|
|
31
|
+
const newStyle = new Style({})
|
|
32
|
+
handles.updateStyle(newStyle)
|
|
33
|
+
expect(styleFn(featureAt(1))).toEqual([newStyle])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('remove clears and detaches the layer', () => {
|
|
37
|
+
const { map, handles, source } = setup()
|
|
38
|
+
handles.update(GEOM)
|
|
39
|
+
handles.remove()
|
|
40
|
+
expect(source.getFeatures()).toHaveLength(0)
|
|
41
|
+
expect(map.layers).toHaveLength(0)
|
|
42
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import Modify from 'ol/interaction/Modify.js'
|
|
2
|
+
import Collection from 'ol/Collection.js'
|
|
3
|
+
import { findNearest, PIXEL_TOLERANCE } from './vertexHitTest.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Derive the undo operation from the vertex arrays before and after an OL
|
|
7
|
+
* Modify drag. A grown array means a midpoint drag inserted a vertex; an
|
|
8
|
+
* equal-length array with a changed coordinate means a vertex move.
|
|
9
|
+
*
|
|
10
|
+
* @param {number[][]} prevCoords - vertices snapshotted at modifystart
|
|
11
|
+
* @param {number[][]} newCoords - vertices after modifyend
|
|
12
|
+
* @returns {{ type: string, vertexIndex: number, previousCoord?: number[] } | null}
|
|
13
|
+
*/
|
|
14
|
+
export const deriveModifyOp = (prevCoords, newCoords) => {
|
|
15
|
+
if (newCoords.length > prevCoords.length) {
|
|
16
|
+
const insertedIdx = newCoords.findIndex((c, i) => c[0] !== prevCoords[i]?.[0])
|
|
17
|
+
return { type: 'insert_vertex', vertexIndex: Math.max(0, insertedIdx) }
|
|
18
|
+
}
|
|
19
|
+
if (newCoords.length === prevCoords.length) {
|
|
20
|
+
const movedIdx = newCoords.findIndex((c, i) => c[0] !== prevCoords[i][0] || c[1] !== prevCoords[i][1])
|
|
21
|
+
if (movedIdx >= 0) {
|
|
22
|
+
return { type: 'move_vertex', vertexIndex: movedIdx, previousCoord: prevCoords[movedIdx] }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Only activate Modify when clicking on a vertex/midpoint handle, never for the
|
|
29
|
+
// touch interface (touchHandler covers touch drags via the SVG offset target)
|
|
30
|
+
export const buildModifyCondition = ({ map, getState }) => (mapBrowserEvent) => {
|
|
31
|
+
const { interfaceType, vertices, midpoints } = getState()
|
|
32
|
+
if (interfaceType === 'touch') {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
const olPixel = map.getEventPixel(mapBrowserEvent.originalEvent)
|
|
36
|
+
return findNearest(map, vertices, midpoints, { x: olPixel[0], y: olPixel[1] }) !== null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* OL Modify interaction wiring for edit mode.
|
|
41
|
+
*
|
|
42
|
+
* Modify handles pointer vertex dragging and midpoint insertion natively; this
|
|
43
|
+
* wrapper restricts it to clicks on a handle (buildModifyCondition) and reports
|
|
44
|
+
* each completed drag via `onModifyEnd(prevCoords)` with the vertices
|
|
45
|
+
* snapshotted at drag start.
|
|
46
|
+
*
|
|
47
|
+
* @returns {{ destroy: () => void }}
|
|
48
|
+
*/
|
|
49
|
+
export const createModifyInteraction = ({ map, olFeature, getState, onModifyEnd }) => {
|
|
50
|
+
const condition = buildModifyCondition({ map, getState })
|
|
51
|
+
|
|
52
|
+
const modifyInteraction = new Modify({
|
|
53
|
+
features: new Collection([olFeature]),
|
|
54
|
+
style: () => [], // vertex circles rendered by vertexLayer instead
|
|
55
|
+
pixelTolerance: PIXEL_TOLERANCE,
|
|
56
|
+
condition
|
|
57
|
+
})
|
|
58
|
+
map.addInteraction(modifyInteraction)
|
|
59
|
+
|
|
60
|
+
let startCoords = null
|
|
61
|
+
|
|
62
|
+
modifyInteraction.on('modifystart', () => {
|
|
63
|
+
if (getState().interfaceType === 'touch') {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
startCoords = getState().vertices.map(c => [...c])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
modifyInteraction.on('modifyend', () => {
|
|
70
|
+
if (getState().interfaceType === 'touch') {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
const prevCoords = startCoords
|
|
74
|
+
startCoords = null
|
|
75
|
+
onModifyEnd(prevCoords)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
destroy () {
|
|
80
|
+
map.removeInteraction(modifyInteraction)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|