@defra/interactive-map 0.0.37-alpha → 0.0.39-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/assets/templates/draw-tools.njk +4 -3
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/im-core.js +1 -1
  4. package/dist/esm/im-shell.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api.md +32 -0
  8. package/docs/examples/draw-tools.mdx +0 -4
  9. package/docs/plugins/draw.md +482 -0
  10. package/docs/plugins.md +2 -6
  11. package/govuk-prototype-kit.config.json +3 -2
  12. package/package.json +2 -4
  13. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  14. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  15. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  16. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  17. package/plugins/beta/map-styles/dist/css/index.css +1 -97
  18. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  19. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  20. package/plugins/datasets/src/registry/dataset.js +3 -1
  21. package/plugins/datasets/src/registry/datasetRegistry.js +30 -24
  22. package/plugins/datasets/src/registry/datasetRegistry.test.js +25 -18
  23. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -0
  24. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
  25. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
  26. package/plugins/draw/dist/esm/index.js +1 -0
  27. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
  28. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
  29. package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
  30. package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
  31. package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
  32. package/plugins/draw/dist/umd/index.js +2 -0
  33. package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
  34. package/plugins/draw/src/DrawInit.jsx +89 -0
  35. package/plugins/draw/src/DrawInit.test.jsx +184 -0
  36. package/plugins/draw/src/adapterEvents.js +127 -0
  37. package/plugins/draw/src/adapterEvents.test.js +20 -0
  38. package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
  39. package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
  40. package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
  41. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
  42. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
  43. package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
  44. package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
  45. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
  46. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
  47. package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
  48. package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
  49. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
  50. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
  51. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
  52. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
  53. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
  54. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
  55. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
  56. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
  57. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
  58. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
  59. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
  60. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
  61. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
  62. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
  63. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
  64. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
  65. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
  66. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
  67. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
  68. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
  69. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
  70. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
  71. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
  72. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
  74. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
  75. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
  89. package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
  90. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
  91. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
  92. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
  93. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
  94. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
  95. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
  96. package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
  97. package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
  98. package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
  99. package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
  100. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
  101. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
  102. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
  103. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
  104. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
  105. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
  106. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
  107. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
  108. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
  109. package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
  110. package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
  111. package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
  112. package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
  113. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
  114. package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
  115. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
  116. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
  117. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
  118. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
  119. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
  120. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
  121. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
  122. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
  123. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
  124. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
  125. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
  126. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
  127. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
  128. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
  129. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
  130. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
  132. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
  134. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
  136. package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
  137. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
  138. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
  139. package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
  140. package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
  142. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
  144. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
  145. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
  146. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
  147. package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
  148. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
  149. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
  150. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
  151. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
  153. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
  154. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
  155. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
  156. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
  157. package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
  158. package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
  159. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
  160. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
  161. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
  162. package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
  163. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
  164. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
  165. package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
  166. package/plugins/draw/src/api/addFeature.js +22 -0
  167. package/plugins/draw/src/api/addFeature.test.js +41 -0
  168. package/plugins/draw/src/api/deleteFeature.js +11 -0
  169. package/plugins/draw/src/api/deleteFeature.test.js +24 -0
  170. package/plugins/draw/src/api/editFeature.js +54 -0
  171. package/plugins/draw/src/api/editFeature.test.js +134 -0
  172. package/plugins/draw/src/api/merge.js +30 -0
  173. package/plugins/draw/src/api/merge.test.js +57 -0
  174. package/plugins/draw/src/api/newLine.js +41 -0
  175. package/plugins/draw/src/api/newLine.test.js +93 -0
  176. package/plugins/draw/src/api/newPolygon.js +41 -0
  177. package/plugins/draw/src/api/newPolygon.test.js +93 -0
  178. package/plugins/draw/src/api/split.js +117 -0
  179. package/plugins/draw/src/api/split.test.js +204 -0
  180. package/plugins/draw/src/defaults.js +52 -0
  181. package/plugins/draw/src/defaults.test.js +18 -0
  182. package/plugins/draw/src/draw.scss +43 -0
  183. package/plugins/draw/src/events.js +237 -0
  184. package/plugins/draw/src/events.test.js +369 -0
  185. package/plugins/draw/src/index.js +10 -0
  186. package/plugins/draw/src/index.test.js +24 -0
  187. package/plugins/draw/src/manifest.js +170 -0
  188. package/plugins/draw/src/manifest.test.js +147 -0
  189. package/plugins/draw/src/reducer.js +69 -0
  190. package/plugins/draw/src/reducer.test.js +85 -0
  191. package/plugins/draw/src/utils/debounce.js +16 -0
  192. package/plugins/draw/src/utils/debounce.test.js +40 -0
  193. package/plugins/draw/src/utils/eventBus.js +36 -0
  194. package/plugins/draw/src/utils/eventBus.test.js +47 -0
  195. package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
  196. package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
  197. package/plugins/draw/src/utils/getValueForStyle.js +33 -0
  198. package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
  199. package/plugins/draw/src/utils/resolveColors.js +39 -0
  200. package/plugins/draw/src/utils/resolveColors.test.js +34 -0
  201. package/plugins/draw/src/utils/spatial.js +243 -0
  202. package/plugins/draw/src/utils/spatial.test.js +243 -0
  203. package/plugins/draw/src/utils/touchTarget.js +95 -0
  204. package/plugins/draw/src/utils/touchTarget.test.js +107 -0
  205. package/plugins/draw/src/utils/undoStack.js +31 -0
  206. package/plugins/draw/src/utils/undoStack.test.js +49 -0
  207. package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
  208. package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
  209. package/plugins/draw/src/validation/liveStroke.js +85 -0
  210. package/plugins/draw/src/validation/liveStroke.test.js +150 -0
  211. package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
  212. package/plugins/draw/src/validation/rules.js +186 -0
  213. package/plugins/draw/src/validation/rules.test.js +120 -0
  214. package/plugins/draw/src/validation/validateGeometry.js +119 -0
  215. package/plugins/draw/src/validation/validateGeometry.test.js +200 -0
  216. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  217. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  218. package/plugins/interact/src/hooks/useInteractionHandlers.js +1 -1
  219. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
  220. package/plugins/interact/src/utils/buildStylesMap.js +1 -0
  221. package/plugins/search/dist/css/index.css +1 -317
  222. package/rollup.esm.mjs +35 -28
  223. package/sonar-project.properties +2 -2
  224. package/src/App/components/Attributions/Attributions.module.scss +1 -0
  225. package/src/App/components/Hints/Hints.module.scss +5 -4
  226. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
  227. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
  228. package/src/App/components/MoveControl/MoveControl.jsx +73 -9
  229. package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
  230. package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
  231. package/src/App/hooks/useHintsAPI.js +67 -0
  232. package/src/App/hooks/useHintsAPI.test.js +145 -0
  233. package/src/App/renderer/PluginInits.jsx +4 -0
  234. package/src/App/renderer/PluginInits.test.jsx +7 -1
  235. package/src/InteractiveMap/InteractiveMap.js +20 -0
  236. package/src/InteractiveMap/InteractiveMap.test.js +7 -0
  237. package/src/config/events.js +4 -0
  238. package/src/scss/settings/_dimensions.scss +3 -0
  239. package/src/services/announcer.js +38 -3
  240. package/src/services/announcer.test.js +133 -1
  241. package/src/services/hints.js +3 -0
  242. package/src/services/hints.test.js +19 -0
  243. package/src/types.js +12 -0
  244. package/src/utils/detectInterfaceType.js +5 -1
  245. package/src/utils/isMac.js +15 -0
  246. package/src/utils/isMac.test.js +37 -0
  247. 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
+ }