@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,72 @@
|
|
|
1
|
+
import { createSnapIndicator } from './snapIndicator.js'
|
|
2
|
+
import { createFakeMap } from '../__helpers__/harness.js'
|
|
3
|
+
|
|
4
|
+
const colors = { snapVertex: '#sv', snapEdge: '#se' }
|
|
5
|
+
|
|
6
|
+
const setup = () => {
|
|
7
|
+
const map = createFakeMap()
|
|
8
|
+
const indicator = createSnapIndicator(map, colors)
|
|
9
|
+
const layer = map.layers[0]
|
|
10
|
+
return { map, indicator, layer, source: layer.getSource() }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const renderedColor = (layer, feature) => {
|
|
14
|
+
const ctx = { beginPath: jest.fn(), arc: jest.fn(), fill: jest.fn(), fillStyle: null }
|
|
15
|
+
layer.getStyle()(feature).getRenderer()([5, 6], { context: ctx, pixelRatio: 2 })
|
|
16
|
+
return { color: ctx.fillStyle, arc: ctx.arc.mock.calls[0] }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test('show places a single reused feature; repeated shows move it rather than duplicate', () => {
|
|
20
|
+
const { indicator, source } = setup()
|
|
21
|
+
indicator.show([10, 10], 'vertex')
|
|
22
|
+
indicator.show([20, 20], 'edge')
|
|
23
|
+
expect(source.getFeatures()).toHaveLength(1)
|
|
24
|
+
expect(source.getFeatures()[0].getGeometry().getCoordinates()).toEqual([20, 20])
|
|
25
|
+
expect(source.getFeatures()[0].get('snapType')).toBe('edge')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('vertex and edge snaps render circles in their own colours, scaled by pixelRatio', () => {
|
|
29
|
+
const { indicator, layer, source } = setup()
|
|
30
|
+
indicator.show([10, 10], 'vertex')
|
|
31
|
+
const feature = source.getFeatures()[0]
|
|
32
|
+
expect(renderedColor(layer, feature)).toEqual({ color: colors.snapVertex, arc: [5, 6, 20, 0, Math.PI * 2] })
|
|
33
|
+
|
|
34
|
+
feature.set('snapType', 'edge', true)
|
|
35
|
+
expect(renderedColor(layer, feature).color).toBe(colors.snapEdge)
|
|
36
|
+
|
|
37
|
+
feature.set('snapType', 'mystery', true)
|
|
38
|
+
expect(layer.getStyle()(feature)).toBeNull()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('hide clears the circle and is safe to call when nothing is showing', () => {
|
|
42
|
+
const { indicator, source } = setup()
|
|
43
|
+
indicator.hide() // nothing showing — no-op
|
|
44
|
+
indicator.show([10, 10], 'vertex')
|
|
45
|
+
indicator.hide()
|
|
46
|
+
expect(source.getFeatures()).toHaveLength(0)
|
|
47
|
+
indicator.show([1, 1], 'vertex') // can show again after hiding
|
|
48
|
+
expect(source.getFeatures()).toHaveLength(1)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('colour updates apply to subsequent renders, refreshing a visible circle', () => {
|
|
52
|
+
const { indicator, layer, source } = setup()
|
|
53
|
+
indicator.show([10, 10], 'vertex')
|
|
54
|
+
indicator.updateColors({ snapVertex: '#new', snapEdge: '#se' })
|
|
55
|
+
expect(renderedColor(layer, source.getFeatures()[0]).color).toBe('#new')
|
|
56
|
+
indicator.updateColors(colors) // also fine while hidden
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('colour updates while hidden skip the redraw', () => {
|
|
60
|
+
const { indicator, source } = setup()
|
|
61
|
+
const changed = jest.spyOn(source, 'changed')
|
|
62
|
+
indicator.updateColors({ snapVertex: '#x', snapEdge: '#y' }) // nothing showing yet
|
|
63
|
+
expect(changed).not.toHaveBeenCalled()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('remove clears the source and detaches the layer', () => {
|
|
67
|
+
const { map, indicator, source } = setup()
|
|
68
|
+
indicator.show([10, 10], 'vertex')
|
|
69
|
+
indicator.remove()
|
|
70
|
+
expect(source.getFeatures()).toHaveLength(0)
|
|
71
|
+
expect(map.removeLayer).toHaveBeenCalled()
|
|
72
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom OL Interaction that intercepts pointer events and rewrites
|
|
3
|
+
* mapBrowserEvent.coordinate to the nearest snap candidate before any draw
|
|
4
|
+
* or modify interaction sees it.
|
|
5
|
+
*
|
|
6
|
+
* Coordinate snapping applies to all pointer events (pointermove, pointerdown,
|
|
7
|
+
* pointerup, singleclick) so that both rubberbanding and vertex placement are snapped.
|
|
8
|
+
* The visual indicator is only updated on pointermove.
|
|
9
|
+
*
|
|
10
|
+
* Must be added to the map AFTER the Draw/Modify interaction so it is processed
|
|
11
|
+
* first (OL iterates interactions in reverse-add order).
|
|
12
|
+
* snapManager.reattach() handles this after each mode change.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Interaction from 'ol/interaction/Interaction.js'
|
|
16
|
+
|
|
17
|
+
const SNAP_EVENTS = new Set(['pointermove', 'pointerdrag', 'pointerdown', 'pointerup', 'singleclick', 'click'])
|
|
18
|
+
|
|
19
|
+
const processSnapEvent = (mapBrowserEvent, engine, indicator, snapRadius, isIndicatorActive) => {
|
|
20
|
+
const { type } = mapBrowserEvent
|
|
21
|
+
|
|
22
|
+
if (type === 'pointerout' || type === 'pointerleave') {
|
|
23
|
+
indicator.hide()
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!SNAP_EVENTS.has(type)) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = engine.query(mapBrowserEvent.coordinate, snapRadius)
|
|
32
|
+
if (result) {
|
|
33
|
+
mapBrowserEvent.coordinate = result.coord.slice()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Only update indicator during free mouse movement — hide during drag, no-op for clicks
|
|
37
|
+
if (type === 'pointermove' && isIndicatorActive()) {
|
|
38
|
+
result ? indicator.show(result.coord, result.type) : indicator.hide()
|
|
39
|
+
} else if (type === 'pointerdrag') {
|
|
40
|
+
indicator.hide()
|
|
41
|
+
} else {
|
|
42
|
+
// no indicator update for click/down/up events
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const createSnapInteraction = (engine, indicator, snapRadius, isIndicatorActive) => {
|
|
47
|
+
const interaction = new Interaction({
|
|
48
|
+
handleEvent (mapBrowserEvent) {
|
|
49
|
+
if (interaction.getActive()) {
|
|
50
|
+
processSnapEvent(mapBrowserEvent, engine, indicator, snapRadius, isIndicatorActive)
|
|
51
|
+
}
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return interaction
|
|
57
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createSnapInteraction } from './snapInteraction.js'
|
|
2
|
+
|
|
3
|
+
const setup = ({ indicatorActive = true } = {}) => {
|
|
4
|
+
const engine = { query: jest.fn(() => ({ type: 'vertex', coord: [9, 9] })) }
|
|
5
|
+
const indicator = { show: jest.fn(), hide: jest.fn() }
|
|
6
|
+
const interaction = createSnapInteraction(engine, indicator, 12, () => indicatorActive)
|
|
7
|
+
const fire = (type, coordinate = [1, 1]) => {
|
|
8
|
+
const event = { type, coordinate }
|
|
9
|
+
const result = interaction.handleEvent(event)
|
|
10
|
+
return { event, result }
|
|
11
|
+
}
|
|
12
|
+
return { engine, indicator, interaction, fire }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('always lets the event continue to other interactions', () => {
|
|
16
|
+
const { fire } = setup()
|
|
17
|
+
expect(fire('pointermove').result).toBe(true)
|
|
18
|
+
expect(fire('wheel').result).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('does nothing while inactive', () => {
|
|
22
|
+
const { engine, interaction, fire } = setup()
|
|
23
|
+
interaction.setActive(false)
|
|
24
|
+
const { event } = fire('pointermove')
|
|
25
|
+
expect(engine.query).not.toHaveBeenCalled()
|
|
26
|
+
expect(event.coordinate).toEqual([1, 1])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('rewrites the event coordinate to a copy of the snap candidate for all pointer gestures', () => {
|
|
30
|
+
const { engine, fire } = setup()
|
|
31
|
+
for (const type of ['pointermove', 'pointerdrag', 'pointerdown', 'pointerup', 'singleclick', 'click']) {
|
|
32
|
+
const { event } = fire(type)
|
|
33
|
+
expect(event.coordinate).toEqual([9, 9])
|
|
34
|
+
expect(event.coordinate).not.toBe(engine.query.mock.results.at(-1).value.coord) // copy, not shared
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('the indicator follows free mouse movement only', () => {
|
|
39
|
+
const { engine, indicator, fire } = setup()
|
|
40
|
+
fire('pointermove')
|
|
41
|
+
expect(indicator.show).toHaveBeenCalledWith([9, 9], 'vertex')
|
|
42
|
+
|
|
43
|
+
engine.query.mockReturnValue(null)
|
|
44
|
+
fire('pointermove')
|
|
45
|
+
expect(indicator.hide).toHaveBeenCalledTimes(1)
|
|
46
|
+
|
|
47
|
+
engine.query.mockReturnValue({ type: 'edge', coord: [5, 5] })
|
|
48
|
+
fire('pointerdrag')
|
|
49
|
+
expect(indicator.hide).toHaveBeenCalledTimes(2) // hidden during drags
|
|
50
|
+
fire('singleclick')
|
|
51
|
+
expect(indicator.show).toHaveBeenCalledTimes(1) // clicks never touch the indicator
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('the indicator stays untouched during pointermove when the indicator is gated off', () => {
|
|
55
|
+
const { indicator, fire } = setup({ indicatorActive: false })
|
|
56
|
+
const { event } = fire('pointermove')
|
|
57
|
+
expect(event.coordinate).toEqual([9, 9]) // snapping still applies
|
|
58
|
+
expect(indicator.show).not.toHaveBeenCalled()
|
|
59
|
+
expect(indicator.hide).not.toHaveBeenCalled()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('leaving the map hides the indicator without querying; unrelated events are ignored', () => {
|
|
63
|
+
const { engine, indicator, fire } = setup()
|
|
64
|
+
fire('pointerout')
|
|
65
|
+
expect(indicator.hide).toHaveBeenCalled()
|
|
66
|
+
fire('wheel')
|
|
67
|
+
expect(engine.query).not.toHaveBeenCalled()
|
|
68
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snap manager — orchestrates the snap engine, indicator, and OL interaction.
|
|
3
|
+
*
|
|
4
|
+
* Returned as manager.snap; null when no snapLayers are configured.
|
|
5
|
+
*
|
|
6
|
+
* Interface used by draw and edit modes:
|
|
7
|
+
* snap.apply(coord) — query + show/hide indicator; returns snapped coord or original
|
|
8
|
+
* snap.hideIndicator() — explicit hide (e.g. on touch/keyboard commit)
|
|
9
|
+
* snap.setActive(bool) — enable / disable (wired to the UI snap toggle)
|
|
10
|
+
* snap.reattach() — re-add the OL interaction after a mode change so it stays
|
|
11
|
+
* last-added (= first to process pointermove events)
|
|
12
|
+
* snap.destroy() — full cleanup
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createSnapEngine } from './snapEngine.js'
|
|
16
|
+
import { createSnapIndicator } from './snapIndicator.js'
|
|
17
|
+
import { createSnapInteraction } from './snapInteraction.js'
|
|
18
|
+
|
|
19
|
+
export const createSnapManager = (map, snapLayers, colors, snapRadius) => {
|
|
20
|
+
if (!snapLayers?.length) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const engine = createSnapEngine(map, snapLayers)
|
|
25
|
+
const indicator = createSnapIndicator(map, colors)
|
|
26
|
+
|
|
27
|
+
let indicatorActive = false
|
|
28
|
+
const interaction = createSnapInteraction(engine, indicator, snapRadius, () => indicatorActive)
|
|
29
|
+
|
|
30
|
+
map.addInteraction(interaction)
|
|
31
|
+
interaction.setActive(false) // matches reducer initial state: snap: false
|
|
32
|
+
|
|
33
|
+
let active = false
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
/**
|
|
37
|
+
* Apply snap at coord. Updates the indicator and returns the snapped
|
|
38
|
+
* coordinate, or the original coordinate when no snap candidate is found.
|
|
39
|
+
* Returns original coord unchanged when snap is disabled.
|
|
40
|
+
*/
|
|
41
|
+
snapRadius,
|
|
42
|
+
|
|
43
|
+
apply (coord) {
|
|
44
|
+
if (!active) {
|
|
45
|
+
return coord
|
|
46
|
+
}
|
|
47
|
+
const result = engine.query(coord, snapRadius)
|
|
48
|
+
if (result) {
|
|
49
|
+
indicator.show(result.coord, result.type)
|
|
50
|
+
return result.coord
|
|
51
|
+
}
|
|
52
|
+
indicator.hide()
|
|
53
|
+
return coord
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
hideIndicator () {
|
|
57
|
+
indicator.hide()
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
setIndicatorActive (value) {
|
|
61
|
+
indicatorActive = value
|
|
62
|
+
if (!value) {
|
|
63
|
+
indicator.hide()
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
setActive (value) {
|
|
68
|
+
active = value
|
|
69
|
+
interaction.setActive(value)
|
|
70
|
+
if (!value) {
|
|
71
|
+
indicator.hide()
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Remove and re-add the OL interaction so it sits at the top of the
|
|
77
|
+
* interaction stack (last-added = first to handle pointermove).
|
|
78
|
+
* Call after each changeMode() so the interaction always runs before
|
|
79
|
+
* the newly added Draw or Modify interaction.
|
|
80
|
+
*/
|
|
81
|
+
setSnapLayers (layers) {
|
|
82
|
+
engine.setLayers(layers === null || layers === undefined ? snapLayers : layers)
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
reattach () {
|
|
86
|
+
map.removeInteraction(interaction)
|
|
87
|
+
map.addInteraction(interaction)
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
updateColors (newColors) {
|
|
91
|
+
indicator.updateColors(newColors)
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
destroy () {
|
|
95
|
+
map.removeInteraction(interaction)
|
|
96
|
+
indicator.remove()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createSnapManager } from './snapManager.js'
|
|
2
|
+
import { createSnapEngine } from './snapEngine.js'
|
|
3
|
+
import { createSnapIndicator } from './snapIndicator.js'
|
|
4
|
+
import { createSnapInteraction } from './snapInteraction.js'
|
|
5
|
+
import { createFakeMap } from '../__helpers__/harness.js'
|
|
6
|
+
|
|
7
|
+
jest.mock('./snapEngine.js', () => ({
|
|
8
|
+
createSnapEngine: jest.fn(() => ({ query: jest.fn(() => null), setLayers: jest.fn() }))
|
|
9
|
+
}))
|
|
10
|
+
jest.mock('./snapIndicator.js', () => ({
|
|
11
|
+
createSnapIndicator: jest.fn(() => ({ show: jest.fn(), hide: jest.fn(), updateColors: jest.fn(), remove: jest.fn() }))
|
|
12
|
+
}))
|
|
13
|
+
jest.mock('./snapInteraction.js', () => ({
|
|
14
|
+
createSnapInteraction: jest.fn(() => ({ setActive: jest.fn() }))
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
const setup = () => {
|
|
18
|
+
const map = createFakeMap()
|
|
19
|
+
const snap = createSnapManager(map, ['boundaries'], { snapVertex: '#sv' }, 12)
|
|
20
|
+
const engine = createSnapEngine.mock.results.at(-1).value
|
|
21
|
+
const indicator = createSnapIndicator.mock.results.at(-1).value
|
|
22
|
+
const interaction = createSnapInteraction.mock.results.at(-1).value
|
|
23
|
+
return { map, snap, engine, indicator, interaction }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
afterEach(() => jest.clearAllMocks())
|
|
27
|
+
|
|
28
|
+
test('no snap layers configured means no snap manager at all', () => {
|
|
29
|
+
expect(createSnapManager(createFakeMap(), null, {}, 12)).toBeNull()
|
|
30
|
+
expect(createSnapManager(createFakeMap(), [], {}, 12)).toBeNull()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('starts attached to the map but inactive, matching the initial snap-off UI state', () => {
|
|
34
|
+
const { map, snap, interaction } = setup()
|
|
35
|
+
expect(map.interactions).toContain(interaction)
|
|
36
|
+
expect(interaction.setActive).toHaveBeenCalledWith(false)
|
|
37
|
+
expect(snap.snapRadius).toBe(12)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('apply', () => {
|
|
41
|
+
test('returns the coordinate untouched while snap is off', () => {
|
|
42
|
+
const { snap, engine } = setup()
|
|
43
|
+
expect(snap.apply([1, 2])).toEqual([1, 2])
|
|
44
|
+
expect(engine.query).not.toHaveBeenCalled()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('snaps to a candidate and shows the indicator, or hides it on a miss', () => {
|
|
48
|
+
const { snap, engine, indicator } = setup()
|
|
49
|
+
snap.setActive(true)
|
|
50
|
+
engine.query.mockReturnValue({ type: 'vertex', coord: [9, 9] })
|
|
51
|
+
expect(snap.apply([8, 8])).toEqual([9, 9])
|
|
52
|
+
expect(indicator.show).toHaveBeenCalledWith([9, 9], 'vertex')
|
|
53
|
+
|
|
54
|
+
engine.query.mockReturnValue(null)
|
|
55
|
+
expect(snap.apply([1, 2])).toEqual([1, 2])
|
|
56
|
+
expect(indicator.hide).toHaveBeenCalled()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('deactivating snap or the indicator hides the circle; the interaction reads indicator state live', () => {
|
|
61
|
+
const { snap, indicator, interaction } = setup()
|
|
62
|
+
const isIndicatorActive = createSnapInteraction.mock.calls.at(-1)[3]
|
|
63
|
+
snap.setIndicatorActive(true)
|
|
64
|
+
expect(isIndicatorActive()).toBe(true)
|
|
65
|
+
snap.setIndicatorActive(false)
|
|
66
|
+
expect(isIndicatorActive()).toBe(false)
|
|
67
|
+
expect(indicator.hide).toHaveBeenCalledTimes(1)
|
|
68
|
+
|
|
69
|
+
snap.setActive(false)
|
|
70
|
+
expect(interaction.setActive).toHaveBeenLastCalledWith(false)
|
|
71
|
+
expect(indicator.hide).toHaveBeenCalledTimes(2)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('setSnapLayers forwards new layers, falling back to the configured set when cleared', () => {
|
|
75
|
+
const { snap, engine } = setup()
|
|
76
|
+
snap.setSnapLayers(['other'])
|
|
77
|
+
expect(engine.setLayers).toHaveBeenCalledWith(['other'])
|
|
78
|
+
snap.setSnapLayers(null)
|
|
79
|
+
expect(engine.setLayers).toHaveBeenCalledWith(['boundaries'])
|
|
80
|
+
snap.setSnapLayers(undefined)
|
|
81
|
+
expect(engine.setLayers).toHaveBeenLastCalledWith(['boundaries'])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('reattach re-adds the interaction so it processes pointer events first', () => {
|
|
85
|
+
const { map, snap, interaction } = setup()
|
|
86
|
+
map.interactions.push({ id: 'draw' }) // a mode was just added
|
|
87
|
+
snap.reattach()
|
|
88
|
+
expect(map.removeInteraction).toHaveBeenCalledWith(interaction)
|
|
89
|
+
expect(map.interactions.at(-1)).toBe(interaction)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('colour updates and explicit hides reach the indicator; destroy removes interaction and indicator', () => {
|
|
93
|
+
const { map, snap, indicator, interaction } = setup()
|
|
94
|
+
snap.hideIndicator()
|
|
95
|
+
expect(indicator.hide).toHaveBeenCalled()
|
|
96
|
+
snap.updateColors({ snapVertex: '#new' })
|
|
97
|
+
expect(indicator.updateColors).toHaveBeenCalledWith({ snapVertex: '#new' })
|
|
98
|
+
snap.destroy()
|
|
99
|
+
expect(map.removeInteraction).toHaveBeenCalledWith(interaction)
|
|
100
|
+
expect(indicator.remove).toHaveBeenCalled()
|
|
101
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure geometry helpers for multi-ring/multi-part geometry support.
|
|
3
|
+
* Handles coordinate transformations between flat arrays and hierarchical GeoJSON.
|
|
4
|
+
* Supports Polygon, MultiPolygon, LineString, MultiLineString.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const getCoords = (geom) => {
|
|
8
|
+
if (!geom?.coordinates) {
|
|
9
|
+
return []
|
|
10
|
+
}
|
|
11
|
+
switch (geom.type) {
|
|
12
|
+
case 'LineString': return geom.coordinates
|
|
13
|
+
case 'Polygon': return geom.coordinates.flatMap(ring => ring.slice(0, -1))
|
|
14
|
+
case 'MultiLineString': return geom.coordinates.flat(1)
|
|
15
|
+
case 'MultiPolygon': return geom.coordinates.flatMap(poly => poly.flatMap(ring => ring.slice(0, -1)))
|
|
16
|
+
default: return []
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Segment metadata for each ring or part.
|
|
22
|
+
* { start, length, path, closed }
|
|
23
|
+
*/
|
|
24
|
+
export const getRingSegments = (geom) => {
|
|
25
|
+
if (!geom?.coordinates) {
|
|
26
|
+
return []
|
|
27
|
+
}
|
|
28
|
+
const segments = []
|
|
29
|
+
let start = 0
|
|
30
|
+
|
|
31
|
+
switch (geom.type) {
|
|
32
|
+
case 'LineString':
|
|
33
|
+
segments.push({ start: 0, length: geom.coordinates.length, path: [], closed: false })
|
|
34
|
+
break
|
|
35
|
+
case 'Polygon':
|
|
36
|
+
geom.coordinates.forEach((ring, i) => {
|
|
37
|
+
const len = ring.length - 1
|
|
38
|
+
segments.push({ start, length: len, path: [i], closed: true })
|
|
39
|
+
start += len
|
|
40
|
+
})
|
|
41
|
+
break
|
|
42
|
+
case 'MultiLineString':
|
|
43
|
+
geom.coordinates.forEach((line, i) => {
|
|
44
|
+
segments.push({ start, length: line.length, path: [i], closed: false })
|
|
45
|
+
start += line.length
|
|
46
|
+
})
|
|
47
|
+
break
|
|
48
|
+
case 'MultiPolygon':
|
|
49
|
+
geom.coordinates.forEach((polygon, pi) => {
|
|
50
|
+
polygon.forEach((ring, ri) => {
|
|
51
|
+
const len = ring.length - 1
|
|
52
|
+
segments.push({ start, length: len, path: [pi, ri], closed: true })
|
|
53
|
+
start += len
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
break
|
|
57
|
+
default:
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return segments
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Find which segment a flat vertex index belongs to. */
|
|
65
|
+
export const getSegmentForIndex = (segments, flatIdx) => {
|
|
66
|
+
for (const seg of segments) {
|
|
67
|
+
if (flatIdx >= seg.start && flatIdx < seg.start + seg.length) {
|
|
68
|
+
return { segment: seg, localIdx: flatIdx - seg.start }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Return a reference to the coordinate array at a hierarchical path. */
|
|
75
|
+
export const getModifiableCoords = (geojsonGeometry, path) => {
|
|
76
|
+
let coords = geojsonGeometry.coordinates
|
|
77
|
+
for (const idx of path) {
|
|
78
|
+
coords = coords[idx]
|
|
79
|
+
}
|
|
80
|
+
return coords
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Compute midpoints for all segments, respecting open/closed ring boundaries. */
|
|
84
|
+
export const getMidpoints = (geom) => {
|
|
85
|
+
const coords = getCoords(geom)
|
|
86
|
+
const segments = getRingSegments(geom)
|
|
87
|
+
if (!coords.length || !segments.length) {
|
|
88
|
+
return []
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const midpoints = []
|
|
92
|
+
for (const seg of segments) {
|
|
93
|
+
const count = seg.closed ? seg.length : seg.length - 1
|
|
94
|
+
for (let i = 0; i < count; i++) {
|
|
95
|
+
const idx = seg.start + i
|
|
96
|
+
const nextIdx = seg.start + ((i + 1) % seg.length)
|
|
97
|
+
const [x1, y1] = coords[idx]
|
|
98
|
+
const [x2, y2] = coords[nextIdx]
|
|
99
|
+
midpoints.push([(x1 + x2) / 2, (y1 + y2) / 2])
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return midpoints
|
|
103
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getCoords, getRingSegments, getSegmentForIndex, getModifiableCoords, getMidpoints } from './geometryHelpers.js'
|
|
2
|
+
|
|
3
|
+
const SQUARE = [[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]
|
|
4
|
+
const HOLE = [[2, 2], [4, 2], [4, 4], [2, 2]]
|
|
5
|
+
const LINE = [[0, 0], [10, 0], [20, 0]]
|
|
6
|
+
|
|
7
|
+
describe('getCoords flattens editable vertices (closing coords stripped)', () => {
|
|
8
|
+
test.each([
|
|
9
|
+
['LineString', { type: 'LineString', coordinates: LINE }, 3],
|
|
10
|
+
['Polygon with hole', { type: 'Polygon', coordinates: [SQUARE, HOLE] }, 7],
|
|
11
|
+
['MultiLineString', { type: 'MultiLineString', coordinates: [LINE, LINE] }, 6],
|
|
12
|
+
['MultiPolygon', { type: 'MultiPolygon', coordinates: [[SQUARE], [SQUARE, HOLE]] }, 11]
|
|
13
|
+
])('%s', (_, geom, expected) => {
|
|
14
|
+
expect(getCoords(geom)).toHaveLength(expected)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('missing or unsupported geometry yields an empty array', () => {
|
|
18
|
+
expect(getCoords(null)).toEqual([])
|
|
19
|
+
expect(getCoords({ type: 'Point' })).toEqual([])
|
|
20
|
+
expect(getCoords({ type: 'Point', coordinates: [0, 0] })).toEqual([])
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('getRingSegments maps flat indices back to parts', () => {
|
|
25
|
+
test('describes each ring/part with its offset, length, path and closure', () => {
|
|
26
|
+
expect(getRingSegments({ type: 'LineString', coordinates: LINE }))
|
|
27
|
+
.toEqual([{ start: 0, length: 3, path: [], closed: false }])
|
|
28
|
+
expect(getRingSegments({ type: 'Polygon', coordinates: [SQUARE, HOLE] }))
|
|
29
|
+
.toEqual([
|
|
30
|
+
{ start: 0, length: 4, path: [0], closed: true },
|
|
31
|
+
{ start: 4, length: 3, path: [1], closed: true }
|
|
32
|
+
])
|
|
33
|
+
expect(getRingSegments({ type: 'MultiLineString', coordinates: [LINE, LINE] })[1])
|
|
34
|
+
.toEqual({ start: 3, length: 3, path: [1], closed: false })
|
|
35
|
+
expect(getRingSegments({ type: 'MultiPolygon', coordinates: [[SQUARE], [HOLE]] })[1])
|
|
36
|
+
.toEqual({ start: 4, length: 3, path: [1, 0], closed: true })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('missing or unsupported geometry yields no segments', () => {
|
|
40
|
+
expect(getRingSegments(null)).toEqual([])
|
|
41
|
+
expect(getRingSegments({ type: 'Point', coordinates: [0, 0] })).toEqual([])
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('getSegmentForIndex', () => {
|
|
46
|
+
const segments = getRingSegments({ type: 'Polygon', coordinates: [SQUARE, HOLE] })
|
|
47
|
+
|
|
48
|
+
test('finds the owning segment and the local index within it', () => {
|
|
49
|
+
expect(getSegmentForIndex(segments, 5)).toEqual({ segment: segments[1], localIdx: 1 })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('returns null beyond the last segment', () => {
|
|
53
|
+
expect(getSegmentForIndex(segments, 7)).toBeNull()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('getModifiableCoords returns a live reference at a hierarchical path', () => {
|
|
58
|
+
const geom = { type: 'MultiPolygon', coordinates: [[SQUARE], [HOLE]] }
|
|
59
|
+
const ring = getModifiableCoords(geom, [1, 0])
|
|
60
|
+
ring[0] = [3, 3]
|
|
61
|
+
expect(geom.coordinates[1][0][0]).toEqual([3, 3])
|
|
62
|
+
expect(getModifiableCoords(geom, [])).toBe(geom.coordinates)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('getMidpoints', () => {
|
|
66
|
+
test('closed rings wrap (one midpoint per vertex); open lines do not', () => {
|
|
67
|
+
expect(getMidpoints({ type: 'Polygon', coordinates: [SQUARE] }))
|
|
68
|
+
.toEqual([[5, 0], [10, 5], [5, 10], [0, 5]])
|
|
69
|
+
expect(getMidpoints({ type: 'LineString', coordinates: LINE }))
|
|
70
|
+
.toEqual([[5, 0], [15, 0]])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('empty geometry yields no midpoints', () => {
|
|
74
|
+
expect(getMidpoints({ type: 'LineString', coordinates: [] })).toEqual([])
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin helpers bridging OL's array-based pixel/coordinate API
|
|
3
|
+
* and the {x, y} object convention used in touch/keyboard handlers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** OL coordinate [e, n] → screen pixel { x, y } */
|
|
7
|
+
export const coordToPixel = (map, coord) => {
|
|
8
|
+
const px = map.getPixelFromCoordinate(coord)
|
|
9
|
+
if (!px) { return null }
|
|
10
|
+
return { x: px[0], y: px[1] }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Screen pixel { x, y } → OL coordinate [e, n] */
|
|
14
|
+
export const pixelToCoord = (map, pixel) => {
|
|
15
|
+
return map.getCoordinateFromPixel([pixel.x, pixel.y])
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Pixel distance between two { x, y } points */
|
|
19
|
+
export const pixelDist = (a, b) => Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2)
|
|
20
|
+
|
|
21
|
+
/** OL pixel array [x, y] → { x, y } */
|
|
22
|
+
export const arrayToPixel = ([x, y]) => ({ x, y })
|
|
23
|
+
|
|
24
|
+
/** { x, y } → OL pixel array [x, y] */
|
|
25
|
+
export const pixelToArray = ({ x, y }) => [x, y]
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Nudge a coordinate by (dx, dy) screen pixels.
|
|
29
|
+
* Converts pixel offset to map coordinate delta using the current resolution.
|
|
30
|
+
*/
|
|
31
|
+
export const nudgeCoord = (map, coord, dx, dy) => {
|
|
32
|
+
const px = map.getPixelFromCoordinate(coord)
|
|
33
|
+
if (!px) { return coord }
|
|
34
|
+
return map.getCoordinateFromPixel([px[0] + dx, px[1] + dy])
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { coordToPixel, pixelToCoord, pixelDist, arrayToPixel, pixelToArray, nudgeCoord } from './olCoords.js'
|
|
2
|
+
import { createFakeMap } from '../__helpers__/harness.js'
|
|
3
|
+
|
|
4
|
+
const map = createFakeMap()
|
|
5
|
+
|
|
6
|
+
test('converts between OL arrays and {x, y} pixel objects', () => {
|
|
7
|
+
expect(coordToPixel(map, [10, 20])).toEqual({ x: 10, y: 20 })
|
|
8
|
+
expect(pixelToCoord(map, { x: 10, y: 20 })).toEqual([10, 20])
|
|
9
|
+
expect(arrayToPixel([1, 2])).toEqual({ x: 1, y: 2 })
|
|
10
|
+
expect(pixelToArray({ x: 1, y: 2 })).toEqual([1, 2])
|
|
11
|
+
expect(pixelDist({ x: 0, y: 0 }, { x: 3, y: 4 })).toBe(5)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('nudgeCoord offsets a coordinate by screen pixels', () => {
|
|
15
|
+
expect(nudgeCoord(map, [10, 20], 5, -3)).toEqual([15, 17])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('unprojectable coordinates return null / stay put', () => {
|
|
19
|
+
const blindMap = { getPixelFromCoordinate: () => null }
|
|
20
|
+
expect(coordToPixel(blindMap, [1, 1])).toBeNull()
|
|
21
|
+
expect(nudgeCoord(blindMap, [1, 1], 5, 5)).toEqual([1, 1])
|
|
22
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordinate-layout helpers for ol/interaction/Draw sketch geometries.
|
|
3
|
+
*
|
|
4
|
+
* During drawing OL keeps a trailing rubber-band coordinate, and for Polygons
|
|
5
|
+
* additionally appends a copy of the first coordinate to close the ring:
|
|
6
|
+
* LineString: [...placed, rubber_band]
|
|
7
|
+
* Polygon ring: [...placed, rubber_band, v0_closing]
|
|
8
|
+
*
|
|
9
|
+
* This is the single place that layout is encoded — if an OL upgrade changes
|
|
10
|
+
* the Draw interaction's sketch bookkeeping, update it here.
|
|
11
|
+
*/
|
|
12
|
+
const TRAILING_COORDS = { Polygon: 2, LineString: 1 }
|
|
13
|
+
|
|
14
|
+
/** Placed vertex coordinates of an in-progress sketch geometry. */
|
|
15
|
+
export const getPlacedSketchCoords = (geom) => {
|
|
16
|
+
const type = geom.getType()
|
|
17
|
+
const coords = type === 'Polygon' ? (geom.getCoordinates()[0] ?? []) : geom.getCoordinates()
|
|
18
|
+
return coords.slice(0, -TRAILING_COORDS[type])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Last vertex committed by OL's Draw interaction, or null if none placed yet. */
|
|
22
|
+
export const getLastPlacedSketchCoord = (geom) => getPlacedSketchCoords(geom).at(-1) ?? null
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Polygon from 'ol/geom/Polygon.js'
|
|
2
|
+
import LineString from 'ol/geom/LineString.js'
|
|
3
|
+
import { getPlacedSketchCoords, getLastPlacedSketchCoord } from './sketchHelpers.js'
|
|
4
|
+
|
|
5
|
+
test('strips the rubber band (and the closing coord for polygons) to expose placed vertices', () => {
|
|
6
|
+
const poly = new Polygon([[[0, 0], [10, 0], [5, 5], [0, 0]]]) // 2 placed + rubber + closing
|
|
7
|
+
expect(getPlacedSketchCoords(poly)).toEqual([[0, 0], [10, 0]])
|
|
8
|
+
expect(getLastPlacedSketchCoord(poly)).toEqual([10, 0])
|
|
9
|
+
|
|
10
|
+
const line = new LineString([[0, 0], [10, 0], [5, 5]]) // 2 placed + rubber
|
|
11
|
+
expect(getPlacedSketchCoords(line)).toEqual([[0, 0], [10, 0]])
|
|
12
|
+
expect(getLastPlacedSketchCoord(line)).toEqual([10, 0])
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('empty or rubber-band-only sketches have no placed vertices', () => {
|
|
16
|
+
expect(getPlacedSketchCoords(new Polygon([]))).toEqual([])
|
|
17
|
+
expect(getLastPlacedSketchCoord(new LineString([]))).toBeNull()
|
|
18
|
+
})
|