@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,431 @@
|
|
|
1
|
+
import { createDrawMode, buildCondition, buildCanPlaceVertex } from './DrawMode.js'
|
|
2
|
+
import { createDrawInput } from './drawInput.js'
|
|
3
|
+
import { createFeatureStore } from '../core/featureStore.js'
|
|
4
|
+
import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
|
|
5
|
+
import { STYLES_CHANGED_EVENT } from '../core/internalEvents.js'
|
|
6
|
+
import { createFakeMap, createFakeManager, polygonFeature, lineFeature } from '../__helpers__/harness.js'
|
|
7
|
+
|
|
8
|
+
jest.mock('./drawInput.js', () => ({
|
|
9
|
+
createDrawInput: jest.fn(() => ({
|
|
10
|
+
getInterfaceType: jest.fn(() => 'keyboard'),
|
|
11
|
+
setInterfaceType: jest.fn(),
|
|
12
|
+
destroy: jest.fn()
|
|
13
|
+
}))
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
const setup = (geometryType = 'Polygon') => {
|
|
17
|
+
const map = createFakeMap()
|
|
18
|
+
const manager = createFakeManager()
|
|
19
|
+
manager.store = createFeatureStore()
|
|
20
|
+
const emitted = () => manager.emit.mock.calls.map(([type, payload]) => ({ type, payload }))
|
|
21
|
+
const mode = createDrawMode({
|
|
22
|
+
map,
|
|
23
|
+
manager,
|
|
24
|
+
options: { geometryType, featureId: 'shape-1', properties: { label: 'field' }, container: null, snap: null }
|
|
25
|
+
})
|
|
26
|
+
const interaction = map.interactions[0]
|
|
27
|
+
const input = createDrawInput.mock.results.at(-1).value
|
|
28
|
+
const inputOptions = createDrawInput.mock.calls.at(-1)[0].options
|
|
29
|
+
return { map, manager, emitted, mode, interaction, input, inputOptions }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
afterEach(() => jest.clearAllMocks())
|
|
33
|
+
|
|
34
|
+
describe('buildCondition (duplicate-click suppression)', () => {
|
|
35
|
+
const map = createFakeMap()
|
|
36
|
+
const click = (x, y, originalEvent = {}) => ({ pixel: [x, y], originalEvent })
|
|
37
|
+
const conditionFor = (sketch) => buildCondition(map, 'LineString', () => sketch, () => true)
|
|
38
|
+
|
|
39
|
+
test('modifier-key clicks never draw', () => {
|
|
40
|
+
expect(conditionFor(null)(click(0, 0, { shiftKey: true }))).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('clicks always draw before the sketch exists or once the shape is finishable', () => {
|
|
44
|
+
expect(conditionFor(null)(click(0, 0))).toBe(true)
|
|
45
|
+
const finishable = lineFeature([[0, 0], [10, 0], [50, 50]]) // 2 placed ≥ line minimum
|
|
46
|
+
expect(conditionFor(finishable)(click(10, 1))).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('while unfinishable, clicks on the last placed vertex are suppressed', () => {
|
|
50
|
+
const oneVertex = lineFeature([[10, 10], [50, 50]]) // 1 placed + rubber
|
|
51
|
+
expect(conditionFor(oneVertex)(click(11, 11))).toBe(false)
|
|
52
|
+
expect(conditionFor(oneVertex)(click(20, 20))).toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('degenerate sketches or unprojectable vertices do not block drawing', () => {
|
|
56
|
+
expect(conditionFor(lineFeature([[10, 10]]))(click(10, 10))).toBe(true) // nothing placed yet
|
|
57
|
+
const blindMap = { getPixelFromCoordinate: () => null }
|
|
58
|
+
const condition = buildCondition(blindMap, 'LineString', () => lineFeature([[10, 10], [50, 50]]), () => true)
|
|
59
|
+
expect(condition(click(10, 10))).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('buildCondition (placement gate, polygon)', () => {
|
|
64
|
+
const map = createFakeMap()
|
|
65
|
+
// Polygon sketch: 3 placed vertices + 2 trailing (rubber-band + closing) coords.
|
|
66
|
+
const polyClick = (coordinate) => ({ pixel: [0, 0], originalEvent: {}, coordinate })
|
|
67
|
+
const condition = (ring, manager = createFakeManager()) => {
|
|
68
|
+
const getSketch = () => polygonFeature(ring)
|
|
69
|
+
const gate = buildCanPlaceVertex({ manager, geometryType: 'Polygon', getSketch })
|
|
70
|
+
return buildCondition(map, 'Polygon', getSketch, gate)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
test('rejects a click that would make the drawn path cross itself', () => {
|
|
74
|
+
const sketch = [[0, 0], [2, 2], [2, 0], [9, 9], [0, 0]] // placed: (0,0)(2,2)(2,0)
|
|
75
|
+
expect(condition(sketch)(polyClick([0, 2]))).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('allows a click that keeps the path simple', () => {
|
|
79
|
+
const sketch = [[0, 0], [2, 0], [2, 2], [9, 9], [0, 0]] // placed: (0,0)(2,0)(2,2)
|
|
80
|
+
expect(condition(sketch)(polyClick([0, 2]))).toBe(true)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('buildCanPlaceVertex', () => {
|
|
85
|
+
const gateFor = (ring, manager) =>
|
|
86
|
+
buildCanPlaceVertex({ manager, geometryType: 'Polygon', getSketch: () => (ring ? polygonFeature(ring) : null) })
|
|
87
|
+
|
|
88
|
+
test('a hard-rule veto emits PLACEMENT_BLOCKED with the candidate and reason', () => {
|
|
89
|
+
const manager = createFakeManager()
|
|
90
|
+
const gate = gateFor([[0, 0], [2, 2], [2, 0], [9, 9], [0, 0]], manager)
|
|
91
|
+
expect(gate([0, 2])).toBe(false)
|
|
92
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.PLACEMENT_BLOCKED, expect.objectContaining({
|
|
93
|
+
phase: 'place',
|
|
94
|
+
mode: 'draw_polygon',
|
|
95
|
+
vertexIndex: 3,
|
|
96
|
+
reason: expect.any(String),
|
|
97
|
+
feature: expect.objectContaining({ type: 'Feature' })
|
|
98
|
+
}))
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('the user callback can veto a placement (and receives phase "place")', () => {
|
|
102
|
+
const manager = createFakeManager()
|
|
103
|
+
manager._geometryValidator = jest.fn((event) =>
|
|
104
|
+
event.phase === 'place' ? { valid: false, reason: 'outside region' } : { valid: true })
|
|
105
|
+
const gate = gateFor([[0, 0], [2, 0], [9, 9], [0, 0]], manager)
|
|
106
|
+
expect(gate([5, 5])).toBe(false)
|
|
107
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.PLACEMENT_BLOCKED,
|
|
108
|
+
expect.objectContaining({ reason: 'outside region', phase: 'place' }))
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('the user callback can veto the very first vertex (no sketch yet)', () => {
|
|
112
|
+
const manager = createFakeManager()
|
|
113
|
+
manager._geometryValidator = () => ({ valid: false, reason: 'outside region' })
|
|
114
|
+
const gate = gateFor(null, manager)
|
|
115
|
+
expect(gate([5, 5])).toBe(false)
|
|
116
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.PLACEMENT_BLOCKED,
|
|
117
|
+
expect.objectContaining({ vertexIndex: 0 }))
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('a valid placement passes and emits nothing', () => {
|
|
121
|
+
const manager = createFakeManager()
|
|
122
|
+
manager._geometryValidator = jest.fn(() => true)
|
|
123
|
+
const gate = gateFor([[0, 0], [2, 0], [9, 9], [0, 0]], manager)
|
|
124
|
+
expect(gate([2, 2])).toBe(true)
|
|
125
|
+
expect(manager.emit).not.toHaveBeenCalled()
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('drawing lifecycle', () => {
|
|
130
|
+
test('the sketch reports its placed vertex count on every geometry change', () => {
|
|
131
|
+
const { emitted, interaction } = setup()
|
|
132
|
+
const sketch = polygonFeature([[0, 0], [5, 5], [0, 0]])
|
|
133
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
134
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [10, 0], [5, 5], [0, 0]]])
|
|
135
|
+
expect(emitted().at(-1)).toEqual({ type: ADAPTER_EVENTS.VERTEX_CHANGE, payload: { numVertices: 2 } })
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('finishCondition blocks finishing while the geometry is invalid', () => {
|
|
139
|
+
const { manager, interaction } = setup()
|
|
140
|
+
manager._geometryValid = false
|
|
141
|
+
expect(interaction.finishCondition_()).toBe(false)
|
|
142
|
+
manager._geometryValid = true
|
|
143
|
+
expect(interaction.finishCondition_()).toBe(true)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('the live check turns the sketch stroke dashed while the displayed ring self-intersects', () => {
|
|
147
|
+
const { manager, interaction } = setup('Polygon')
|
|
148
|
+
const sketch = polygonFeature([[0, 0], [5, 5], [0, 0]])
|
|
149
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
150
|
+
// Placed (0,0)(10,10)(10,0) + rubber-band (0,10) + closing — a bowtie.
|
|
151
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [10, 10], [10, 0], [0, 10], [0, 0]]])
|
|
152
|
+
expect(manager.styles.createSketchStyle).toHaveBeenLastCalledWith('Polygon', true)
|
|
153
|
+
// Rubber-band moves back to a simple ring → solid again.
|
|
154
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]])
|
|
155
|
+
expect(manager.styles.createSketchStyle).toHaveBeenLastCalledWith('Polygon', false)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('the live check only restyles when the invalid state flips, not on every move', () => {
|
|
159
|
+
const { manager, interaction } = setup('Polygon')
|
|
160
|
+
const sketch = polygonFeature([[0, 0], [5, 5], [0, 0]])
|
|
161
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
162
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [10, 10], [10, 0], [0, 10], [0, 0]]])
|
|
163
|
+
const styleCalls = manager.styles.createSketchStyle.mock.calls.length
|
|
164
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [10, 10], [10, 0], [1, 10], [0, 0]]]) // still crossing
|
|
165
|
+
expect(manager.styles.createSketchStyle.mock.calls.length).toBe(styleCalls)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('placing a valid vertex keeps the stroke solid while the rubber band still sits on it', () => {
|
|
169
|
+
const { manager, interaction } = setup('Polygon')
|
|
170
|
+
const sketch = polygonFeature([[0, 0], [5, 5], [0, 0]])
|
|
171
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
172
|
+
// 3 placed + rubber band duplicating the just-placed vertex + closing coord.
|
|
173
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [10, 0], [10, 10], [10, 10], [0, 0]]])
|
|
174
|
+
expect(manager.styles.createSketchStyle).not.toHaveBeenCalledWith('Polygon', true)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('2 placed vertices + cursor never go dashed (below the 4-point threshold)', () => {
|
|
178
|
+
const { manager, interaction } = setup('Polygon')
|
|
179
|
+
const sketch = polygonFeature([[0, 0], [5, 5], [0, 0]])
|
|
180
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
181
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [10, 10], [5, 0], [0, 0]]]) // 2 placed + rubber
|
|
182
|
+
expect(manager.styles.createSketchStyle).not.toHaveBeenCalledWith('Polygon', true)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('drawInput receives the placement gate for touch/keyboard placements', () => {
|
|
186
|
+
const { inputOptions } = setup('Polygon')
|
|
187
|
+
expect(typeof inputOptions.canPlace).toBe('function')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('a placement-vetoing path disables Add point; a legal one re-enables it', () => {
|
|
191
|
+
const { emitted, interaction } = setup('Polygon')
|
|
192
|
+
const sketch = polygonFeature([[0, 0], [5, 5], [0, 0]])
|
|
193
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
194
|
+
// Open drawn path crosses itself → placing at the crosshair would be vetoed.
|
|
195
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [2, 2], [2, 0], [0, 2], [0, 0]]])
|
|
196
|
+
expect(emitted().find((e) => e.type === ADAPTER_EVENTS.CAN_PLACE_CHANGE)?.payload)
|
|
197
|
+
.toEqual(expect.objectContaining({ canPlace: false, reason: expect.any(String) }))
|
|
198
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [2, 0], [2, 2], [0, 2], [0, 0]]])
|
|
199
|
+
expect(emitted().filter((e) => e.type === ADAPTER_EVENTS.CAN_PLACE_CHANGE).pop()?.payload)
|
|
200
|
+
.toEqual(expect.objectContaining({ canPlace: true }))
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('a red stroke via the closing edge alone keeps Add point enabled', () => {
|
|
204
|
+
const { manager, emitted, interaction } = setup('Polygon')
|
|
205
|
+
const sketch = polygonFeature([[0, 0], [5, 5], [0, 0]])
|
|
206
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
207
|
+
// Only the implicit closing edge crosses: stroke dashed, but the placement is legal.
|
|
208
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [2, 0], [0, 2], [2, 2], [0, 0]]])
|
|
209
|
+
expect(manager.styles.createSketchStyle).toHaveBeenLastCalledWith('Polygon', true)
|
|
210
|
+
expect(emitted().find((e) => e.type === ADAPTER_EVENTS.CAN_PLACE_CHANGE)).toBeUndefined()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('the user callback runs ONCE per sketch change, with phase preview, driving both the stroke and Add point from that single call', () => {
|
|
214
|
+
jest.useFakeTimers()
|
|
215
|
+
const { manager, emitted, interaction } = setup('Polygon')
|
|
216
|
+
manager._geometryValidator = jest.fn(() => ({ valid: false, reason: 'outside region' }))
|
|
217
|
+
const sketch = polygonFeature([[0, 0], [5, 5], [0, 0]])
|
|
218
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
219
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]])
|
|
220
|
+
expect(manager._geometryValidator).not.toHaveBeenCalled() // deferred to the frame
|
|
221
|
+
jest.runAllTimers()
|
|
222
|
+
expect(manager._geometryValidator).toHaveBeenCalledTimes(1) // not once per gate
|
|
223
|
+
expect(manager._geometryValidator).toHaveBeenCalledWith(expect.objectContaining({ phase: 'preview' }))
|
|
224
|
+
expect(manager.styles.createSketchStyle).toHaveBeenLastCalledWith('Polygon', true)
|
|
225
|
+
expect(emitted().find((e) => e.type === ADAPTER_EVENTS.CAN_PLACE_CHANGE)?.payload)
|
|
226
|
+
.toEqual(expect.objectContaining({ canPlace: false, reason: 'outside region' }))
|
|
227
|
+
jest.useRealTimers()
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('the user callback runs even before any vertex is placed, gating Add point from the very first candidate point', () => {
|
|
231
|
+
jest.useFakeTimers()
|
|
232
|
+
const { manager, emitted, interaction } = setup('Polygon')
|
|
233
|
+
manager._geometryValidator = jest.fn(() => ({ valid: false, reason: 'outside region' }))
|
|
234
|
+
const sketch = polygonFeature([[5, 5], [5, 5]]) // no committed vertices yet — rubber-band + closing dupe only
|
|
235
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
236
|
+
sketch.getGeometry().setCoordinates([[[5, 5], [5, 5]]])
|
|
237
|
+
jest.runAllTimers()
|
|
238
|
+
expect(manager._geometryValidator).toHaveBeenCalledWith(expect.objectContaining({ numVertices: 0, phase: 'preview' }))
|
|
239
|
+
expect(emitted().find((e) => e.type === ADAPTER_EVENTS.CAN_PLACE_CHANGE)?.payload)
|
|
240
|
+
.toEqual(expect.objectContaining({ canPlace: false, reason: 'outside region' }))
|
|
241
|
+
jest.useRealTimers()
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test('setInvalid rebuilds the sketch style in the invalid variant and re-renders', () => {
|
|
245
|
+
const { mode, manager, interaction } = setup('Polygon')
|
|
246
|
+
const changed = jest.spyOn(interaction.overlay_, 'changed')
|
|
247
|
+
mode.setInvalid(true)
|
|
248
|
+
expect(manager.styles.createSketchStyle).toHaveBeenLastCalledWith('Polygon', true)
|
|
249
|
+
expect(changed).toHaveBeenCalled()
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test('setInterfaceType forwards to the draw input (e.g. DrawInit syncing MoveControl-driven interface changes mid-session)', () => {
|
|
253
|
+
const { mode, input } = setup('Polygon')
|
|
254
|
+
mode.setInterfaceType('touch')
|
|
255
|
+
expect(input.setInterfaceType).toHaveBeenCalledWith('touch')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
test('setDrawingPreviewProperty tags the sketch feature and re-renders; safe with no sketch yet', () => {
|
|
259
|
+
const { mode, interaction } = setup('LineString')
|
|
260
|
+
const changed = jest.spyOn(interaction.overlay_, 'changed')
|
|
261
|
+
expect(() => mode.setDrawingPreviewProperty('splitter', 'valid')).not.toThrow()
|
|
262
|
+
expect(changed).not.toHaveBeenCalled()
|
|
263
|
+
|
|
264
|
+
const sketch = lineFeature([[0, 0], [1, 1]])
|
|
265
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
266
|
+
mode.setDrawingPreviewProperty('splitter', 'valid')
|
|
267
|
+
expect(sketch.get('splitter')).toBe('valid')
|
|
268
|
+
expect(changed).toHaveBeenCalled()
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
test('undo re-validates the committed shape (deferred, phase commit-delete)', () => {
|
|
272
|
+
jest.useFakeTimers()
|
|
273
|
+
const { mode, manager, emitted, interaction } = setup()
|
|
274
|
+
const sketch = polygonFeature([[0, 0], [10, 0], [5, 5], [0, 0]])
|
|
275
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
276
|
+
jest.spyOn(interaction, 'removeLastPoint').mockImplementation(() => {})
|
|
277
|
+
manager.emit.mockClear()
|
|
278
|
+
mode.undo()
|
|
279
|
+
jest.runAllTimers()
|
|
280
|
+
expect(emitted().filter((e) => e.type === ADAPTER_EVENTS.GEOMETRY_CHANGE).pop()?.payload).toEqual(
|
|
281
|
+
expect.objectContaining({ phase: 'commit-delete', feature: expect.any(Object) }))
|
|
282
|
+
jest.useRealTimers()
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('placing a vertex emits a deferred commit-level geometrychange for validation', () => {
|
|
286
|
+
jest.useFakeTimers()
|
|
287
|
+
const { emitted, interaction } = setup()
|
|
288
|
+
const sketch = polygonFeature([[0, 0], [5, 5], [0, 0]])
|
|
289
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
290
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [10, 0], [5, 5], [0, 0]]])
|
|
291
|
+
// Deferred a tick to avoid re-entrancy — not emitted synchronously.
|
|
292
|
+
expect(emitted().some((e) => e.type === ADAPTER_EVENTS.GEOMETRY_CHANGE)).toBe(false)
|
|
293
|
+
jest.runAllTimers()
|
|
294
|
+
const geom = emitted().find((e) => e.type === ADAPTER_EVENTS.GEOMETRY_CHANGE)
|
|
295
|
+
expect(geom.payload).toEqual(expect.objectContaining({ phase: 'commit-add' }))
|
|
296
|
+
jest.useRealTimers()
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
test('does not re-emit an add when the placed count is unchanged', () => {
|
|
300
|
+
jest.useFakeTimers()
|
|
301
|
+
const { emitted, interaction } = setup()
|
|
302
|
+
const sketch = polygonFeature([[0, 0], [5, 5], [0, 0]])
|
|
303
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
304
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [10, 0], [5, 5], [0, 0]]]) // placed grows to 2 → one add
|
|
305
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [10, 0], [7, 7], [0, 0]]]) // rubber moved, placed still 2
|
|
306
|
+
jest.runAllTimers()
|
|
307
|
+
expect(emitted().filter((e) => e.type === ADAPTER_EVENTS.GEOMETRY_CHANGE)).toHaveLength(1)
|
|
308
|
+
jest.useRealTimers()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test('emits the placed line vertices as a LineString geometrychange', () => {
|
|
312
|
+
jest.useFakeTimers()
|
|
313
|
+
const { emitted, interaction } = setup('LineString')
|
|
314
|
+
const sketch = lineFeature([[0, 0], [5, 5]])
|
|
315
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
316
|
+
sketch.getGeometry().setCoordinates([[0, 0], [10, 0], [5, 5]])
|
|
317
|
+
jest.runAllTimers()
|
|
318
|
+
const geom = emitted().find((e) => e.type === ADAPTER_EVENTS.GEOMETRY_CHANGE)
|
|
319
|
+
expect(geom.payload.feature.geometry.type).toBe('LineString')
|
|
320
|
+
jest.useRealTimers()
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test('vertex tracking becomes inert once the sketch is cleared', () => {
|
|
324
|
+
jest.useFakeTimers()
|
|
325
|
+
const { emitted, interaction, mode } = setup()
|
|
326
|
+
const sketch = polygonFeature([[0, 0], [5, 5], [0, 0]])
|
|
327
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: sketch })
|
|
328
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [10, 0], [5, 5], [0, 0]]]) // schedules a deferred add emit
|
|
329
|
+
mode.destroy() // clears the sketch reference
|
|
330
|
+
sketch.getGeometry().setCoordinates([[[0, 0], [12, 0], [6, 6], [0, 0]]]) // updateVertexCount now a no-op
|
|
331
|
+
jest.runAllTimers() // the earlier deferred emit sees no sketch
|
|
332
|
+
expect(emitted().some((e) => e.type === ADAPTER_EVENTS.GEOMETRY_CHANGE)).toBe(false)
|
|
333
|
+
jest.useRealTimers()
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
test('finishing hands the feature over with the requested id and properties', () => {
|
|
337
|
+
const { manager, emitted, interaction } = setup()
|
|
338
|
+
interaction.dispatchEvent({ type: 'drawend', feature: polygonFeature([[0, 0], [10, 0], [10, 10], [0, 0]], null) })
|
|
339
|
+
const created = emitted().find((e) => e.type === ADAPTER_EVENTS.CREATE)
|
|
340
|
+
expect(created.payload).toMatchObject({ id: 'shape-1', properties: { label: 'field' } })
|
|
341
|
+
expect(manager.store.getOL('shape-1')).not.toBeNull()
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test('aborting cancels', () => {
|
|
345
|
+
const { emitted, interaction } = setup()
|
|
346
|
+
interaction.dispatchEvent({ type: 'drawabort' })
|
|
347
|
+
expect(emitted()).toContainEqual({ type: ADAPTER_EVENTS.CANCEL, payload: undefined })
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
test('the interaction consults the duplicate-suppression condition against the live sketch', () => {
|
|
351
|
+
const { interaction } = setup('LineString')
|
|
352
|
+
expect(interaction.condition_({ pixel: [11, 11], originalEvent: {} })).toBe(true) // no sketch yet
|
|
353
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: lineFeature([[11, 11], [50, 50]]) })
|
|
354
|
+
expect(interaction.condition_({ pixel: [11, 11], originalEvent: {} })).toBe(false) // on last placed
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
test('done() finishes only when the shape has enough vertices', () => {
|
|
358
|
+
const { mode, interaction } = setup()
|
|
359
|
+
const finish = jest.spyOn(interaction, 'finishDrawing').mockImplementation(() => {})
|
|
360
|
+
mode.done() // no sketch at all
|
|
361
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: polygonFeature([[0, 0], [10, 0], [5, 5], [0, 0]]) })
|
|
362
|
+
mode.done() // 2 placed < polygon minimum of 3
|
|
363
|
+
expect(finish).not.toHaveBeenCalled()
|
|
364
|
+
|
|
365
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: polygonFeature([[0, 0], [10, 0], [10, 10], [5, 5], [0, 0]]) })
|
|
366
|
+
mode.done()
|
|
367
|
+
expect(finish).toHaveBeenCalled()
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
test('cancel() aborts the sketch; undo() removes the last point and re-reports the count', () => {
|
|
371
|
+
const { mode, emitted, interaction } = setup()
|
|
372
|
+
const abort = jest.spyOn(interaction, 'abortDrawing').mockImplementation(() => {})
|
|
373
|
+
const removeLast = jest.spyOn(interaction, 'removeLastPoint').mockImplementation(() => {})
|
|
374
|
+
mode.cancel()
|
|
375
|
+
expect(abort).toHaveBeenCalled()
|
|
376
|
+
|
|
377
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: polygonFeature([[0, 0], [10, 0], [5, 5], [0, 0]]) })
|
|
378
|
+
mode.undo()
|
|
379
|
+
expect(removeLast).toHaveBeenCalled()
|
|
380
|
+
expect(emitted().at(-1)).toEqual({ type: ADAPTER_EVENTS.VERTEX_CHANGE, payload: { numVertices: 2 } })
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
test('undo before any sketch exists re-reports nothing', () => {
|
|
384
|
+
const { mode, emitted, interaction } = setup()
|
|
385
|
+
jest.spyOn(interaction, 'removeLastPoint').mockImplementation(() => {})
|
|
386
|
+
mode.undo() // no sketch yet → updateVertexCount returns early
|
|
387
|
+
expect(emitted().some((e) => e.type === ADAPTER_EVENTS.VERTEX_CHANGE)).toBe(false)
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
describe('wiring', () => {
|
|
392
|
+
test('the drawInput gets working undo and canFinish callbacks', () => {
|
|
393
|
+
const { interaction, inputOptions } = setup('LineString')
|
|
394
|
+
const removeLast = jest.spyOn(interaction, 'removeLastPoint').mockImplementation(() => {})
|
|
395
|
+
expect(inputOptions.canFinish()).toBe(false) // no sketch yet
|
|
396
|
+
interaction.dispatchEvent({ type: 'drawstart', feature: lineFeature([[0, 0], [10, 0], [5, 5]]) })
|
|
397
|
+
expect(inputOptions.canFinish()).toBe(true)
|
|
398
|
+
inputOptions.onUndo()
|
|
399
|
+
expect(removeLast).toHaveBeenCalled()
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
test('properties default to an empty object when omitted from options', () => {
|
|
403
|
+
const map = createFakeMap()
|
|
404
|
+
const manager = createFakeManager()
|
|
405
|
+
manager.store = createFeatureStore()
|
|
406
|
+
createDrawMode({ map, manager, options: { geometryType: 'Polygon', featureId: 'shape-2', container: null, snap: null } })
|
|
407
|
+
map.interactions[0].dispatchEvent({ type: 'drawend', feature: polygonFeature([[0, 0], [10, 0], [10, 10], [0, 0]], null) })
|
|
408
|
+
const created = manager.store.getOL('shape-2')
|
|
409
|
+
expect(created).not.toBeNull()
|
|
410
|
+
expect(created.get('label')).toBeUndefined()
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test('map style changes rebuild the sketch style for the current geometry type', () => {
|
|
414
|
+
const { manager, interaction } = setup()
|
|
415
|
+
expect(manager.styles.createSketchStyle).toHaveBeenCalledWith('Polygon')
|
|
416
|
+
manager.emit(STYLES_CHANGED_EVENT, manager.styles)
|
|
417
|
+
expect(manager.styles.createSketchStyle).toHaveBeenCalledTimes(2)
|
|
418
|
+
expect(interaction.getOverlay().getStyleFunction()({ getGeometry: () => ({ getType: () => 'Point' }) })).toEqual([])
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
test('destroy reports the final interface type, tears down input and interaction', () => {
|
|
422
|
+
const { map, mode, emitted, input, interaction } = setup()
|
|
423
|
+
mode.destroy()
|
|
424
|
+
expect(emitted()).toContainEqual({
|
|
425
|
+
type: ADAPTER_EVENTS.INTERFACE_TYPE_CHANGE,
|
|
426
|
+
payload: { interfaceType: 'keyboard' }
|
|
427
|
+
})
|
|
428
|
+
expect(input.destroy).toHaveBeenCalled()
|
|
429
|
+
expect(map.removeInteraction).toHaveBeenCalledWith(interaction)
|
|
430
|
+
})
|
|
431
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createVertexPlacement } from './vertexPlacement.js'
|
|
2
|
+
|
|
3
|
+
const ARROW_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'])
|
|
4
|
+
|
|
5
|
+
const wireInputEvents = ({
|
|
6
|
+
container, addVertexButtonId, olView, onUndo,
|
|
7
|
+
getInterfaceType, setInterfaceType, clearLastCoord,
|
|
8
|
+
updateRubberbanding, placeVertex
|
|
9
|
+
}) => {
|
|
10
|
+
const onCenterChange = () => {
|
|
11
|
+
if (getInterfaceType() !== 'mouse') {
|
|
12
|
+
updateRubberbanding()
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
olView?.on('change:center', onCenterChange)
|
|
16
|
+
|
|
17
|
+
const onKeydown = (e) => {
|
|
18
|
+
if (!container.contains(document.activeElement)) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
if (ARROW_KEYS.has(e.key)) {
|
|
22
|
+
setInterfaceType('keyboard')
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
if (e.key === 'Enter') {
|
|
26
|
+
e.preventDefault()
|
|
27
|
+
setInterfaceType('keyboard')
|
|
28
|
+
placeVertex()
|
|
29
|
+
}
|
|
30
|
+
if (e.key === 'z' && (e.metaKey || e.ctrlKey)) {
|
|
31
|
+
e.preventDefault()
|
|
32
|
+
onUndo?.()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const onButtonClick = (e) => {
|
|
37
|
+
if (addVertexButtonId && e.target.closest(`#${addVertexButtonId}`)) {
|
|
38
|
+
placeVertex()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const onPointerdown = (e) => {
|
|
43
|
+
if (e.pointerType !== 'touch') {
|
|
44
|
+
setInterfaceType('mouse')
|
|
45
|
+
clearLastCoord()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const onTouchstart = () => {
|
|
50
|
+
setInterfaceType('touch')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const onPointerMove = () => {
|
|
54
|
+
if (getInterfaceType() === 'mouse') {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
updateRubberbanding()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
globalThis.addEventListener('keydown', onKeydown)
|
|
61
|
+
globalThis.addEventListener('click', onButtonClick)
|
|
62
|
+
container.addEventListener('pointerdown', onPointerdown)
|
|
63
|
+
container.addEventListener('touchstart', onTouchstart, { passive: true })
|
|
64
|
+
container.addEventListener('pointermove', onPointerMove)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
destroy () {
|
|
68
|
+
olView?.un('change:center', onCenterChange)
|
|
69
|
+
globalThis.removeEventListener('keydown', onKeydown)
|
|
70
|
+
globalThis.removeEventListener('click', onButtonClick)
|
|
71
|
+
container.removeEventListener('pointerdown', onPointerdown)
|
|
72
|
+
container.removeEventListener('touchstart', onTouchstart)
|
|
73
|
+
container.removeEventListener('pointermove', onPointerMove)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Touch/keyboard input wiring for draw mode: crosshair vertex placement via the
|
|
80
|
+
* add-vertex button or Enter, keyboard undo, interface-type tracking, and
|
|
81
|
+
* rubber-band updates while the map pans under the crosshair.
|
|
82
|
+
*
|
|
83
|
+
* @returns {{ getInterfaceType: () => string, destroy: () => void }}
|
|
84
|
+
*/
|
|
85
|
+
export const createDrawInput = ({ drawInteraction, options }) => {
|
|
86
|
+
const { container, addVertexButtonId, mapProvider, snap, onUndo, canFinish, canPlace } = options
|
|
87
|
+
let interfaceType = options.interfaceType ?? 'mouse'
|
|
88
|
+
const getInterfaceType = () => interfaceType
|
|
89
|
+
|
|
90
|
+
const placement = createVertexPlacement({
|
|
91
|
+
drawInteraction,
|
|
92
|
+
mapProvider,
|
|
93
|
+
snap,
|
|
94
|
+
canFinish,
|
|
95
|
+
canPlace,
|
|
96
|
+
getInterfaceType
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const map = drawInteraction.getMap()
|
|
100
|
+
const olView = map?.getView()
|
|
101
|
+
|
|
102
|
+
const events = wireInputEvents({
|
|
103
|
+
container,
|
|
104
|
+
addVertexButtonId,
|
|
105
|
+
olView,
|
|
106
|
+
onUndo,
|
|
107
|
+
getInterfaceType,
|
|
108
|
+
setInterfaceType: (t) => { interfaceType = t },
|
|
109
|
+
clearLastCoord: placement.clearLastCoord,
|
|
110
|
+
updateRubberbanding: placement.updateRubberbanding,
|
|
111
|
+
placeVertex: placement.placeVertex
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// change:center fires once when a keyboard pan animation starts; postrender tracks each frame.
|
|
115
|
+
const onMapRender = () => {
|
|
116
|
+
if (interfaceType !== 'mouse' && olView?.getAnimating()) {
|
|
117
|
+
placement.updateRubberbanding()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
map?.on('postrender', onMapRender)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
getInterfaceType,
|
|
124
|
+
// Called when the global interface type changes without any pointer/touch/key
|
|
125
|
+
// event landing on the map container (e.g. panning via MoveControl after
|
|
126
|
+
// switching to touch) — refresh the rubber band immediately rather than
|
|
127
|
+
// waiting for the next incidental change:center/postrender event.
|
|
128
|
+
setInterfaceType (type) {
|
|
129
|
+
interfaceType = type
|
|
130
|
+
if (type !== 'mouse') {
|
|
131
|
+
placement.updateRubberbanding()
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
destroy () {
|
|
135
|
+
events.destroy()
|
|
136
|
+
map?.un('postrender', onMapRender)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|