@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.
Files changed (244) 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/draw/dist/esm/im-draw-ml-adapter.js +1 -0
  21. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
  22. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
  23. package/plugins/draw/dist/esm/index.js +1 -0
  24. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
  25. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
  26. package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
  27. package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
  28. package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
  29. package/plugins/draw/dist/umd/index.js +2 -0
  30. package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
  31. package/plugins/draw/src/DrawInit.jsx +89 -0
  32. package/plugins/draw/src/DrawInit.test.jsx +184 -0
  33. package/plugins/draw/src/adapterEvents.js +127 -0
  34. package/plugins/draw/src/adapterEvents.test.js +20 -0
  35. package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
  36. package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
  37. package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
  38. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
  39. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
  40. package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
  41. package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
  42. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
  43. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
  44. package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
  45. package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
  46. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
  47. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
  48. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
  49. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
  50. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
  51. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
  52. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
  53. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
  54. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
  55. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
  56. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
  57. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
  58. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
  59. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
  60. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
  61. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
  62. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
  63. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
  64. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
  65. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
  66. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
  67. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
  68. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
  69. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
  70. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
  71. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
  72. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
  74. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
  75. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
  86. package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
  87. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
  88. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
  89. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
  90. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
  91. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
  92. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
  93. package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
  94. package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
  95. package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
  96. package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
  97. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
  98. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
  99. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
  100. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
  101. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
  102. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
  103. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
  104. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
  105. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
  106. package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
  107. package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
  108. package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
  109. package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
  110. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
  111. package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
  112. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
  113. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
  114. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
  115. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
  116. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
  117. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
  118. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
  119. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
  120. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
  121. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
  122. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
  123. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
  124. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
  125. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
  126. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
  127. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
  128. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
  129. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
  130. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
  132. package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
  134. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
  136. package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
  137. package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
  138. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
  139. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
  140. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
  142. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
  144. package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
  145. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
  146. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
  147. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
  148. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
  149. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
  150. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
  151. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
  153. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
  154. package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
  155. package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
  156. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
  157. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
  158. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
  159. package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
  160. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
  161. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
  162. package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
  163. package/plugins/draw/src/api/addFeature.js +22 -0
  164. package/plugins/draw/src/api/addFeature.test.js +41 -0
  165. package/plugins/draw/src/api/deleteFeature.js +11 -0
  166. package/plugins/draw/src/api/deleteFeature.test.js +24 -0
  167. package/plugins/draw/src/api/editFeature.js +54 -0
  168. package/plugins/draw/src/api/editFeature.test.js +134 -0
  169. package/plugins/draw/src/api/merge.js +30 -0
  170. package/plugins/draw/src/api/merge.test.js +57 -0
  171. package/plugins/draw/src/api/newLine.js +41 -0
  172. package/plugins/draw/src/api/newLine.test.js +93 -0
  173. package/plugins/draw/src/api/newPolygon.js +41 -0
  174. package/plugins/draw/src/api/newPolygon.test.js +93 -0
  175. package/plugins/draw/src/api/split.js +117 -0
  176. package/plugins/draw/src/api/split.test.js +204 -0
  177. package/plugins/draw/src/defaults.js +52 -0
  178. package/plugins/draw/src/defaults.test.js +18 -0
  179. package/plugins/draw/src/draw.scss +43 -0
  180. package/plugins/draw/src/events.js +237 -0
  181. package/plugins/draw/src/events.test.js +369 -0
  182. package/plugins/draw/src/index.js +10 -0
  183. package/plugins/draw/src/index.test.js +24 -0
  184. package/plugins/draw/src/manifest.js +170 -0
  185. package/plugins/draw/src/manifest.test.js +147 -0
  186. package/plugins/draw/src/reducer.js +69 -0
  187. package/plugins/draw/src/reducer.test.js +85 -0
  188. package/plugins/draw/src/utils/debounce.js +16 -0
  189. package/plugins/draw/src/utils/debounce.test.js +40 -0
  190. package/plugins/draw/src/utils/eventBus.js +36 -0
  191. package/plugins/draw/src/utils/eventBus.test.js +47 -0
  192. package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
  193. package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
  194. package/plugins/draw/src/utils/getValueForStyle.js +33 -0
  195. package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
  196. package/plugins/draw/src/utils/resolveColors.js +39 -0
  197. package/plugins/draw/src/utils/resolveColors.test.js +34 -0
  198. package/plugins/draw/src/utils/spatial.js +243 -0
  199. package/plugins/draw/src/utils/spatial.test.js +243 -0
  200. package/plugins/draw/src/utils/touchTarget.js +95 -0
  201. package/plugins/draw/src/utils/touchTarget.test.js +107 -0
  202. package/plugins/draw/src/utils/undoStack.js +31 -0
  203. package/plugins/draw/src/utils/undoStack.test.js +49 -0
  204. package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
  205. package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
  206. package/plugins/draw/src/validation/liveStroke.js +85 -0
  207. package/plugins/draw/src/validation/liveStroke.test.js +150 -0
  208. package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
  209. package/plugins/draw/src/validation/rules.js +186 -0
  210. package/plugins/draw/src/validation/rules.test.js +120 -0
  211. package/plugins/draw/src/validation/validateGeometry.js +119 -0
  212. package/plugins/draw/src/validation/validateGeometry.test.js +200 -0
  213. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  214. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  215. package/plugins/interact/src/hooks/useInteractionHandlers.js +1 -1
  216. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
  217. package/plugins/interact/src/utils/buildStylesMap.js +1 -0
  218. package/plugins/search/dist/css/index.css +1 -317
  219. package/rollup.esm.mjs +35 -28
  220. package/sonar-project.properties +2 -2
  221. package/src/App/components/Attributions/Attributions.module.scss +1 -0
  222. package/src/App/components/Hints/Hints.module.scss +5 -4
  223. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
  224. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
  225. package/src/App/components/MoveControl/MoveControl.jsx +73 -9
  226. package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
  227. package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
  228. package/src/App/hooks/useHintsAPI.js +67 -0
  229. package/src/App/hooks/useHintsAPI.test.js +145 -0
  230. package/src/App/renderer/PluginInits.jsx +4 -0
  231. package/src/App/renderer/PluginInits.test.jsx +7 -1
  232. package/src/InteractiveMap/InteractiveMap.js +20 -0
  233. package/src/InteractiveMap/InteractiveMap.test.js +7 -0
  234. package/src/config/events.js +4 -0
  235. package/src/scss/settings/_dimensions.scss +3 -0
  236. package/src/services/announcer.js +38 -3
  237. package/src/services/announcer.test.js +133 -1
  238. package/src/services/hints.js +3 -0
  239. package/src/services/hints.test.js +19 -0
  240. package/src/types.js +12 -0
  241. package/src/utils/detectInterfaceType.js +5 -1
  242. package/src/utils/isMac.js +15 -0
  243. package/src/utils/isMac.test.js +37 -0
  244. package/webpack.umd.mjs +1 -0
@@ -0,0 +1,253 @@
1
+ import {
2
+ getSnapInstance,
3
+ isSnapActive,
4
+ getSnapLngLat,
5
+ getSnapCoords,
6
+ triggerSnapAtPoint,
7
+ triggerSnapAtCenter,
8
+ clearSnapIndicator,
9
+ clearSnapState,
10
+ getSnapRadius,
11
+ isSnapEnabled,
12
+ createSnappedEvent,
13
+ createSnappedClickEvent
14
+ } from './snapHelpers.js'
15
+ import { TOLERANCES } from '../../../defaults.js'
16
+
17
+ const activeSnap = (overrides = {}) => ({
18
+ status: true,
19
+ snapStatus: true,
20
+ snapCoords: [1, 2],
21
+ ...overrides
22
+ })
23
+
24
+ describe('getSnapInstance', () => {
25
+ test('returns the snap instance when present', () => {
26
+ const snap = {}
27
+ expect(getSnapInstance({ _snapInstance: snap })).toBe(snap)
28
+ })
29
+
30
+ test('returns null when the map has no instance', () => {
31
+ expect(getSnapInstance({})).toBeNull()
32
+ })
33
+
34
+ test('returns null when the map is nullish', () => {
35
+ expect(getSnapInstance(null)).toBeNull()
36
+ })
37
+ })
38
+
39
+ describe('isSnapActive', () => {
40
+ test('returns true when status, snapStatus and >=2 coords are present', () => {
41
+ expect(isSnapActive(activeSnap())).toBe(true)
42
+ })
43
+
44
+ test('returns false when snap is nullish', () => {
45
+ expect(isSnapActive(null)).toBe(false)
46
+ })
47
+
48
+ test('returns false when status is falsy', () => {
49
+ expect(isSnapActive(activeSnap({ status: false }))).toBe(false)
50
+ })
51
+
52
+ test('returns false when snapStatus is falsy', () => {
53
+ expect(isSnapActive(activeSnap({ snapStatus: false }))).toBe(false)
54
+ })
55
+
56
+ test('returns false when coords have fewer than 2 entries', () => {
57
+ expect(isSnapActive(activeSnap({ snapCoords: [1] }))).toBe(false)
58
+ })
59
+ })
60
+
61
+ describe('getSnapLngLat', () => {
62
+ test('returns lng/lat object when snap is active', () => {
63
+ expect(getSnapLngLat(activeSnap())).toEqual({ lng: 1, lat: 2 })
64
+ })
65
+
66
+ test('returns null when snap is inactive', () => {
67
+ expect(getSnapLngLat(null)).toBeNull()
68
+ })
69
+ })
70
+
71
+ describe('getSnapCoords', () => {
72
+ test('returns [lng, lat] array when snap is active', () => {
73
+ expect(getSnapCoords(activeSnap())).toEqual([1, 2])
74
+ })
75
+
76
+ test('returns null when snap is inactive', () => {
77
+ expect(getSnapCoords(null)).toBeNull()
78
+ })
79
+ })
80
+
81
+ describe('triggerSnapAtPoint', () => {
82
+ test('unprojects the point and triggers snap detection', () => {
83
+ const snap = { status: true, snapToClosestPoint: jest.fn() }
84
+ const lngLat = { lng: 5, lat: 6 }
85
+ const map = { unproject: jest.fn(() => lngLat) }
86
+ const point = { x: 10, y: 20 }
87
+
88
+ expect(triggerSnapAtPoint(snap, map, point)).toBe(true)
89
+ expect(map.unproject).toHaveBeenCalledWith(point)
90
+ expect(snap.snapToClosestPoint).toHaveBeenCalledWith({ point, lngLat })
91
+ })
92
+
93
+ test('returns false when snap is missing', () => {
94
+ expect(triggerSnapAtPoint(null, {}, {})).toBe(false)
95
+ })
96
+
97
+ test('returns false when map is missing', () => {
98
+ expect(triggerSnapAtPoint({ status: true }, null, {})).toBe(false)
99
+ })
100
+
101
+ test('returns false when snap is disabled', () => {
102
+ expect(triggerSnapAtPoint({ status: false }, {}, {})).toBe(false)
103
+ })
104
+ })
105
+
106
+ describe('triggerSnapAtCenter', () => {
107
+ test('projects the map centre and triggers snap detection', () => {
108
+ const snap = { status: true, snapToClosestPoint: jest.fn() }
109
+ const center = { lng: 1, lat: 2 }
110
+ const point = { x: 3, y: 4 }
111
+ const map = { getCenter: jest.fn(() => center), project: jest.fn(() => point) }
112
+
113
+ expect(triggerSnapAtCenter(snap, map)).toBe(true)
114
+ expect(map.project).toHaveBeenCalledWith(center)
115
+ expect(snap.snapToClosestPoint).toHaveBeenCalledWith({ point, lngLat: center })
116
+ })
117
+
118
+ test('returns false when snap is missing', () => {
119
+ expect(triggerSnapAtCenter(null, {})).toBe(false)
120
+ })
121
+
122
+ test('returns false when map is missing', () => {
123
+ expect(triggerSnapAtCenter({ status: true }, null)).toBe(false)
124
+ })
125
+
126
+ test('returns false when snap is disabled', () => {
127
+ expect(triggerSnapAtCenter({ status: false }, {})).toBe(false)
128
+ })
129
+ })
130
+
131
+ describe('clearSnapIndicator', () => {
132
+ test('resets snap state, empties the feature arrays and hides the layer', () => {
133
+ const snap = {
134
+ snapStatus: true,
135
+ snapCoords: [1, 2],
136
+ snappedFeatures: [{}, {}],
137
+ closeFeatures: [{}],
138
+ lines: [{}]
139
+ }
140
+ const map = {
141
+ getLayer: jest.fn(() => ({})),
142
+ setLayoutProperty: jest.fn()
143
+ }
144
+
145
+ clearSnapIndicator(snap, map)
146
+
147
+ expect(snap.snapStatus).toBe(false)
148
+ expect(snap.snapCoords).toBeNull()
149
+ expect(snap.snappedFeatures).toHaveLength(0)
150
+ expect(snap.closeFeatures).toHaveLength(0)
151
+ expect(snap.lines).toHaveLength(0)
152
+ expect(map.setLayoutProperty).toHaveBeenCalledWith('snap-helper-circle', 'visibility', 'none')
153
+ })
154
+
155
+ test('handles a snap with empty/absent arrays and no map', () => {
156
+ const snap = { snapStatus: true, snapCoords: [1, 2] }
157
+ expect(() => clearSnapIndicator(snap)).not.toThrow()
158
+ expect(snap.snapStatus).toBe(false)
159
+ expect(snap.snapCoords).toBeNull()
160
+ })
161
+
162
+ test('does not touch the layer when it is absent', () => {
163
+ const map = { getLayer: jest.fn(() => null), setLayoutProperty: jest.fn() }
164
+ clearSnapIndicator(null, map)
165
+ expect(map.setLayoutProperty).not.toHaveBeenCalled()
166
+ })
167
+ })
168
+
169
+ describe('clearSnapState', () => {
170
+ test('returns early when snap is nullish', () => {
171
+ expect(() => clearSnapState(null)).not.toThrow()
172
+ })
173
+
174
+ test('resets snap state and empties the feature arrays', () => {
175
+ const snap = {
176
+ snapStatus: true,
177
+ snapCoords: [1, 2],
178
+ snappedFeatures: [{}, {}],
179
+ closeFeatures: [{}],
180
+ lines: [{}]
181
+ }
182
+
183
+ clearSnapState(snap)
184
+
185
+ expect(snap.snapStatus).toBe(false)
186
+ expect(snap.snapCoords).toBeNull()
187
+ expect(snap.snappedFeatures).toHaveLength(0)
188
+ expect(snap.closeFeatures).toHaveLength(0)
189
+ expect(snap.lines).toHaveLength(0)
190
+ })
191
+
192
+ test('handles a snap with empty/absent arrays', () => {
193
+ const snap = { snapStatus: true, snapCoords: [1, 2] }
194
+ expect(() => clearSnapState(snap)).not.toThrow()
195
+ expect(snap.snapCoords).toBeNull()
196
+ })
197
+ })
198
+
199
+ describe('getSnapRadius', () => {
200
+ test('returns the configured radius', () => {
201
+ expect(getSnapRadius({ options: { radius: 25 } })).toBe(25)
202
+ })
203
+
204
+ test('falls back to the configured snap radius when unset', () => {
205
+ expect(getSnapRadius(null)).toBe(TOLERANCES.snapRadius)
206
+ })
207
+ })
208
+
209
+ describe('isSnapEnabled', () => {
210
+ test('returns true when getSnapEnabled returns true', () => {
211
+ expect(isSnapEnabled({ getSnapEnabled: () => true })).toBe(true)
212
+ })
213
+
214
+ test('returns false when getSnapEnabled returns a non-true value', () => {
215
+ expect(isSnapEnabled({ getSnapEnabled: () => false })).toBe(false)
216
+ })
217
+
218
+ test('returns false when getSnapEnabled is not a function', () => {
219
+ expect(isSnapEnabled({})).toBe(false)
220
+ })
221
+ })
222
+
223
+ describe('createSnappedEvent', () => {
224
+ test('merges the snapped lngLat into the event when snapping', () => {
225
+ const e = { type: 'click', lngLat: { lng: 0, lat: 0 } }
226
+ const result = createSnappedEvent(e, activeSnap())
227
+ expect(result).toEqual({ type: 'click', lngLat: { lng: 1, lat: 2 } })
228
+ })
229
+
230
+ test('returns the original event when there is no snap', () => {
231
+ const e = { type: 'click' }
232
+ expect(createSnappedEvent(e, null)).toBe(e)
233
+ })
234
+ })
235
+
236
+ describe('createSnappedClickEvent', () => {
237
+ test('builds a synthetic click event at the snapped point', () => {
238
+ const point = { x: 12, y: 34 }
239
+ const map = { project: jest.fn(() => point) }
240
+
241
+ const result = createSnappedClickEvent(map, activeSnap())
242
+
243
+ expect(map.project).toHaveBeenCalledWith([1, 2])
244
+ expect(result.lngLat).toEqual({ lng: 1, lat: 2 })
245
+ expect(result.point).toBe(point)
246
+ expect(result.originalEvent).toBeInstanceOf(MouseEvent)
247
+ expect(result.originalEvent.type).toBe('click')
248
+ })
249
+
250
+ test('returns null when there is no snap', () => {
251
+ expect(createSnappedClickEvent({}, null)).toBeNull()
252
+ })
253
+ })
@@ -0,0 +1,57 @@
1
+ const MAX_TAP_DURATION_MS = 300
2
+ const MAX_TAP_MOVEMENT_PX = 10
3
+
4
+ /**
5
+ * Workaround: mapbox-gl-draw calls preventDefault() on touchend even in disabled
6
+ * mode, which stops the browser synthesizing a click event. Detect quick taps
7
+ * while in disabled mode and manually dispatch a click on the canvas so app-level
8
+ * click handlers (e.g. feature selection) still fire.
9
+ *
10
+ * @param {Object} map - MapLibre map instance
11
+ * @param {Object} draw - MapboxDraw instance (used to check the current mode)
12
+ * @returns {{ remove: Function }}
13
+ */
14
+ export const setupTouchClickWorkaround = (map, draw) => {
15
+ const canvas = map.getCanvas()
16
+ let touchStart = null
17
+
18
+ const handleTouchStart = (e) => {
19
+ if (e.touches.length === 1) {
20
+ touchStart = { x: e.touches[0].clientX, y: e.touches[0].clientY, time: Date.now() }
21
+ }
22
+ }
23
+
24
+ const handleTouchEnd = (e) => {
25
+ if (draw.getMode() !== 'disabled' || !touchStart) {
26
+ touchStart = null
27
+ return
28
+ }
29
+
30
+ const touch = e.changedTouches[0]
31
+ const dx = touch.clientX - touchStart.x
32
+ const dy = touch.clientY - touchStart.y
33
+ const duration = Date.now() - touchStart.time
34
+
35
+ // Only synthesize click for quick taps with minimal movement
36
+ if (duration < MAX_TAP_DURATION_MS && Math.abs(dx) < MAX_TAP_MOVEMENT_PX && Math.abs(dy) < MAX_TAP_MOVEMENT_PX) {
37
+ canvas.dispatchEvent(new MouseEvent('click', {
38
+ bubbles: true,
39
+ cancelable: true,
40
+ clientX: touch.clientX,
41
+ clientY: touch.clientY
42
+ }))
43
+ }
44
+
45
+ touchStart = null
46
+ }
47
+
48
+ canvas.addEventListener('touchstart', handleTouchStart, { passive: true })
49
+ canvas.addEventListener('touchend', handleTouchEnd, { passive: true })
50
+
51
+ return {
52
+ remove () {
53
+ canvas.removeEventListener('touchstart', handleTouchStart)
54
+ canvas.removeEventListener('touchend', handleTouchEnd)
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,106 @@
1
+ import { setupTouchClickWorkaround } from './touchClickWorkaround.js'
2
+
3
+ const createCanvas = () => {
4
+ const listeners = {}
5
+ return {
6
+ addEventListener: jest.fn((type, handler) => { listeners[type] = handler }),
7
+ removeEventListener: jest.fn(),
8
+ dispatchEvent: jest.fn(),
9
+ fire (type, event) { listeners[type]?.(event) }
10
+ }
11
+ }
12
+
13
+ const touchStartEvent = (touches) => ({ touches })
14
+ const touchEndEvent = (changedTouches) => ({ changedTouches })
15
+
16
+ describe('setupTouchClickWorkaround', () => {
17
+ let canvas
18
+ let map
19
+ let draw
20
+
21
+ beforeEach(() => {
22
+ canvas = createCanvas()
23
+ map = { getCanvas: () => canvas }
24
+ draw = { getMode: jest.fn(() => 'disabled') }
25
+ jest.spyOn(Date, 'now')
26
+ })
27
+
28
+ afterEach(() => {
29
+ Date.now.mockRestore()
30
+ })
31
+
32
+ test('registers passive touchstart/touchend listeners on the canvas', () => {
33
+ setupTouchClickWorkaround(map, draw)
34
+ expect(canvas.addEventListener).toHaveBeenCalledWith('touchstart', expect.any(Function), { passive: true })
35
+ expect(canvas.addEventListener).toHaveBeenCalledWith('touchend', expect.any(Function), { passive: true })
36
+ })
37
+
38
+ test('synthesizes a click for a quick, stationary tap in disabled mode', () => {
39
+ Date.now.mockReturnValueOnce(1000).mockReturnValueOnce(1100)
40
+ setupTouchClickWorkaround(map, draw)
41
+
42
+ canvas.fire('touchstart', touchStartEvent([{ clientX: 50, clientY: 60 }]))
43
+ canvas.fire('touchend', touchEndEvent([{ clientX: 52, clientY: 61 }]))
44
+
45
+ expect(canvas.dispatchEvent).toHaveBeenCalledTimes(1)
46
+ const dispatched = canvas.dispatchEvent.mock.calls[0][0]
47
+ expect(dispatched).toBeInstanceOf(MouseEvent)
48
+ expect(dispatched.type).toBe('click')
49
+ expect(dispatched.clientX).toBe(52)
50
+ expect(dispatched.clientY).toBe(61)
51
+ })
52
+
53
+ test('does not synthesize a click when the tap is too slow', () => {
54
+ Date.now.mockReturnValueOnce(1000).mockReturnValueOnce(1400)
55
+ setupTouchClickWorkaround(map, draw)
56
+
57
+ canvas.fire('touchstart', touchStartEvent([{ clientX: 50, clientY: 60 }]))
58
+ canvas.fire('touchend', touchEndEvent([{ clientX: 50, clientY: 60 }]))
59
+
60
+ expect(canvas.dispatchEvent).not.toHaveBeenCalled()
61
+ })
62
+
63
+ test('does not synthesize a click when the finger moves too far', () => {
64
+ Date.now.mockReturnValueOnce(1000).mockReturnValueOnce(1100)
65
+ setupTouchClickWorkaround(map, draw)
66
+
67
+ canvas.fire('touchstart', touchStartEvent([{ clientX: 50, clientY: 60 }]))
68
+ canvas.fire('touchend', touchEndEvent([{ clientX: 100, clientY: 60 }]))
69
+
70
+ expect(canvas.dispatchEvent).not.toHaveBeenCalled()
71
+ })
72
+
73
+ test('ignores multi-touch gestures on touchstart', () => {
74
+ setupTouchClickWorkaround(map, draw)
75
+
76
+ canvas.fire('touchstart', touchStartEvent([{ clientX: 1, clientY: 1 }, { clientX: 2, clientY: 2 }]))
77
+ canvas.fire('touchend', touchEndEvent([{ clientX: 1, clientY: 1 }]))
78
+
79
+ expect(canvas.dispatchEvent).not.toHaveBeenCalled()
80
+ })
81
+
82
+ test('does nothing when the current mode is not disabled', () => {
83
+ draw.getMode.mockReturnValue('draw_polygon')
84
+ setupTouchClickWorkaround(map, draw)
85
+
86
+ canvas.fire('touchstart', touchStartEvent([{ clientX: 50, clientY: 60 }]))
87
+ canvas.fire('touchend', touchEndEvent([{ clientX: 50, clientY: 60 }]))
88
+
89
+ expect(canvas.dispatchEvent).not.toHaveBeenCalled()
90
+ })
91
+
92
+ test('does nothing on touchend without a preceding single-finger touchstart', () => {
93
+ setupTouchClickWorkaround(map, draw)
94
+
95
+ canvas.fire('touchend', touchEndEvent([{ clientX: 50, clientY: 60 }]))
96
+
97
+ expect(canvas.dispatchEvent).not.toHaveBeenCalled()
98
+ })
99
+
100
+ test('remove() detaches both listeners', () => {
101
+ const { remove } = setupTouchClickWorkaround(map, draw)
102
+ remove()
103
+ expect(canvas.removeEventListener).toHaveBeenCalledWith('touchstart', expect.any(Function))
104
+ expect(canvas.removeEventListener).toHaveBeenCalledWith('touchend', expect.any(Function))
105
+ })
106
+ })
@@ -0,0 +1,117 @@
1
+ import { createOLDraw } from './olDraw.js'
2
+
3
+ // split.js passes this literal — MapLibre's own always-present "already-drawn
4
+ // shapes" style layer id — as a snapLayers entry so the split line snaps to the
5
+ // polygon it's splitting. On MapLibre it needs no help: mapbox-gl-snap resolves
6
+ // it by querying the map's rendered style layers directly. OL's snap engine has
7
+ // no such string-based lookup for plain vector layers (only direct instances or
8
+ // vector-tile layer names), so this adapter recognises the same literal here and
9
+ // resolves it to the draw plugin's own VectorLayer instance instead.
10
+ const DRAW_OUTLINE_STYLE_LAYER = 'stroke-inactive.cold'
11
+
12
+ /**
13
+ * Draw adapter for OpenLayers.
14
+ *
15
+ * Wraps OLDrawManager (via createOLDraw) and exposes the shared adapter interface
16
+ * consumed by events.js, DrawInit, and the api entry points.
17
+ *
18
+ * OLDrawManager already exposes on/off/emit, changeMode, done/cancel/undo/deleteVertex,
19
+ * get/add/delete/deleteAll, and setInterfaceType. This adapter adds the snap methods
20
+ * and undo-stack clearing (OL manages its own undo stack on the manager).
21
+ *
22
+ * Adapter interface (also implemented by MaplibreDrawAdapter):
23
+ * changeMode(name, options)
24
+ * getMode()
25
+ * setInterfaceType(type)
26
+ * done() / cancel() / undo() / deleteVertex()
27
+ * nudgeSelectedVertex(dx, dy, isLargeStep)
28
+ * get(id) / add(feature) / delete(id) / deleteAll()
29
+ * setSnapEnabled(bool) / setSnapLayers(layers) / isSnapEnabled()
30
+ * setFeatureProperty(id, property, value) / setDrawingPreviewProperty(property, value)
31
+ * on(event, handler) / off(event, handler)
32
+ * remove()
33
+ */
34
+ export class OLDrawAdapter {
35
+ _snapEnabled = false
36
+
37
+ constructor (mapProvider, options) {
38
+ const { manager, remove } = createOLDraw({
39
+ mapProvider,
40
+ events: options.events,
41
+ eventBus: options.eventBus,
42
+ // The full pluginConfig, not just snapLayers — OLDrawManager also reads
43
+ // colour/size overrides (shapeStroke, editStroke, strokeWidth, snapRadius,
44
+ // etc.) off this object via resolveColors().
45
+ pluginConfig: options.pluginConfig ?? { snapLayers: options.snapLayers },
46
+ mapStyle: options.mapStyle
47
+ })
48
+ this._cleanupOLDraw = remove
49
+ this._manager = manager
50
+ this._mapProvider = mapProvider
51
+ }
52
+
53
+ changeMode (name, options = {}) {
54
+ // Inject OL-specific options that the unified api doesn't supply
55
+ const opts = { ...options, mapProvider: this._mapProvider }
56
+ if (name === 'draw_polygon') { opts.geometryType = 'Polygon' }
57
+ if (name === 'draw_line') { opts.geometryType = 'LineString' }
58
+ return this._manager.changeMode(name, opts)
59
+ }
60
+
61
+ getMode () { return this._manager.getMode() }
62
+
63
+ setInterfaceType (type) { this._manager.setInterfaceType(type) }
64
+
65
+ done () {
66
+ this._manager.undoStack.clear()
67
+ this._manager.done()
68
+ }
69
+
70
+ cancel () {
71
+ this._manager.undoStack.clear()
72
+ this._manager.cancel()
73
+ }
74
+
75
+ undo () { this._manager.undo() }
76
+ deleteVertex () { this._manager.deleteVertex() }
77
+ nudgeSelectedVertex (dx, dy, isLargeStep) { this._manager.nudgeSelectedVertex(dx, dy, isLargeStep) }
78
+
79
+ // Record the current geometry validity so the draw mode can block finish gestures
80
+ // (double-click / click-to-close) while the in-progress shape is invalid.
81
+ setGeometryValid (valid) { this._manager._geometryValid = valid }
82
+
83
+ // The api entry points assign the active user validator to the adapter; store it
84
+ // on the manager so the draw mode can veto placements synchronously.
85
+ set _geometryValidator (fn) { this._manager._geometryValidator = fn }
86
+ get _geometryValidator () { return this._manager._geometryValidator }
87
+
88
+ // Show/hide the dashed invalid stroke on the active sketch or edit feature.
89
+ setInvalid (invalid) { this._manager.setInvalid(invalid) }
90
+
91
+ get (id) { return this._manager.get(id) }
92
+ add (feature) { return this._manager.add(feature) }
93
+ delete (id) { return this._manager.delete(id) }
94
+ deleteAll () { return this._manager.deleteAll() }
95
+
96
+ setSnapEnabled (bool) {
97
+ this._snapEnabled = bool
98
+ this._manager.snap?.setActive(bool)
99
+ }
100
+
101
+ setSnapLayers (layers) {
102
+ const translated = layers?.map((l) => (l === DRAW_OUTLINE_STYLE_LAYER ? this._manager._layer : l))
103
+ this._manager.snap?.setSnapLayers(translated)
104
+ }
105
+
106
+ isSnapEnabled () { return this._snapEnabled }
107
+
108
+ setFeatureProperty () { /* not implemented for OL */ }
109
+ setDrawingPreviewProperty (property, value) { this._manager.setDrawingPreviewProperty(property, value) }
110
+
111
+ on (type, handler) { this._manager.on(type, handler) }
112
+ off (type, handler) { this._manager.off(type, handler) }
113
+
114
+ remove () {
115
+ this._cleanupOLDraw()
116
+ }
117
+ }
@@ -0,0 +1,159 @@
1
+ import { OLDrawAdapter } from './OLDrawAdapter.js'
2
+ import { createOLDraw } from './olDraw.js'
3
+
4
+ // The literal split.js passes — see OLDrawAdapter.js's DRAW_OUTLINE_STYLE_LAYER comment.
5
+ const DRAW_OUTLINE_STYLE_LAYER = 'stroke-inactive.cold'
6
+
7
+ const fakeVectorLayer = { id: 'draw-layer' }
8
+
9
+ const fakeManager = () => ({
10
+ _layer: fakeVectorLayer,
11
+ changeMode: jest.fn(),
12
+ getMode: jest.fn(() => 'disabled'),
13
+ setInterfaceType: jest.fn(),
14
+ done: jest.fn(),
15
+ cancel: jest.fn(),
16
+ undo: jest.fn(),
17
+ deleteVertex: jest.fn(),
18
+ nudgeSelectedVertex: jest.fn(),
19
+ setInvalid: jest.fn(),
20
+ setDrawingPreviewProperty: jest.fn(),
21
+ get: jest.fn(() => 'feature'),
22
+ add: jest.fn(),
23
+ delete: jest.fn(),
24
+ deleteAll: jest.fn(),
25
+ on: jest.fn(),
26
+ off: jest.fn(),
27
+ undoStack: { clear: jest.fn() },
28
+ snap: { setActive: jest.fn(), setSnapLayers: jest.fn() }
29
+ })
30
+
31
+ jest.mock('./olDraw.js', () => ({
32
+ createOLDraw: jest.fn(({ mapProvider }) => ({ manager: mapProvider._testManager, remove: jest.fn() }))
33
+ }))
34
+
35
+ const setup = () => {
36
+ const manager = fakeManager()
37
+ const mapProvider = { _testManager: manager }
38
+ const adapter = new OLDrawAdapter(mapProvider, {
39
+ events: { MAP_SET_STYLE: 's' },
40
+ eventBus: {},
41
+ snapLayers: ['boundaries'],
42
+ mapStyle: { id: 'default' }
43
+ })
44
+ return { manager, mapProvider, adapter }
45
+ }
46
+
47
+ afterEach(() => jest.clearAllMocks())
48
+
49
+ test('wires olDraw with the plugin options and uses the returned manager', () => {
50
+ const { manager, adapter } = setup()
51
+ expect(createOLDraw).toHaveBeenCalledWith(expect.objectContaining({
52
+ pluginConfig: { snapLayers: ['boundaries'] },
53
+ mapStyle: { id: 'default' }
54
+ }))
55
+ expect(adapter.getMode()).toBe('disabled')
56
+ expect(manager.getMode).toHaveBeenCalled()
57
+ })
58
+
59
+ test('forwards the full pluginConfig (colour/size overrides too), not just snapLayers', () => {
60
+ const manager = fakeManager()
61
+ const mapProvider = { _testManager: manager }
62
+ const pluginConfig = { snapLayers: ['boundaries'], shapeStroke: '#custom', strokeWidth: 5 }
63
+ // eslint-disable-next-line no-new
64
+ new OLDrawAdapter(mapProvider, {
65
+ events: { MAP_SET_STYLE: 's' },
66
+ eventBus: {},
67
+ pluginConfig,
68
+ mapStyle: { id: 'default' }
69
+ })
70
+ expect(createOLDraw).toHaveBeenCalledWith(expect.objectContaining({ pluginConfig }))
71
+ })
72
+
73
+ test('changeMode injects the mapProvider and the OL geometry type per draw mode', () => {
74
+ const { manager, mapProvider, adapter } = setup()
75
+ adapter.changeMode('draw_polygon', { featureId: 'f1' })
76
+ expect(manager.changeMode).toHaveBeenCalledWith('draw_polygon', { featureId: 'f1', mapProvider, geometryType: 'Polygon' })
77
+ adapter.changeMode('draw_line')
78
+ expect(manager.changeMode).toHaveBeenCalledWith('draw_line', { mapProvider, geometryType: 'LineString' })
79
+ adapter.changeMode('edit_vertex', { featureId: 'f1' })
80
+ expect(manager.changeMode).toHaveBeenCalledWith('edit_vertex', { featureId: 'f1', mapProvider })
81
+ })
82
+
83
+ test('done and cancel clear the undo stack before delegating', () => {
84
+ const { manager, adapter } = setup()
85
+ adapter.done()
86
+ adapter.cancel()
87
+ expect(manager.undoStack.clear).toHaveBeenCalledTimes(2)
88
+ expect(manager.done).toHaveBeenCalled()
89
+ expect(manager.cancel).toHaveBeenCalled()
90
+ })
91
+
92
+ test('snap state is tracked locally and forwarded, tolerating a missing snap manager', () => {
93
+ const { manager, adapter } = setup()
94
+ expect(adapter.isSnapEnabled()).toBe(false)
95
+ adapter.setSnapEnabled(true)
96
+ expect(adapter.isSnapEnabled()).toBe(true)
97
+ expect(manager.snap.setActive).toHaveBeenCalledWith(true)
98
+ adapter.setSnapLayers(['x'])
99
+ expect(manager.snap.setSnapLayers).toHaveBeenCalledWith(['x'])
100
+
101
+ // The shared sentinel translates onto the draw plugin's own VectorLayer instance.
102
+ adapter.setSnapLayers([DRAW_OUTLINE_STYLE_LAYER, 'x'])
103
+ expect(manager.snap.setSnapLayers).toHaveBeenCalledWith([fakeVectorLayer, 'x'])
104
+
105
+ manager.snap = null
106
+ adapter.setSnapEnabled(false)
107
+ adapter.setSnapLayers(['y']) // no throw
108
+ expect(adapter.isSnapEnabled()).toBe(false)
109
+ })
110
+
111
+ test('remaining calls delegate straight through; setFeatureProperty is a deliberate no-op', () => {
112
+ const { manager, adapter } = setup()
113
+ adapter.setInterfaceType('touch')
114
+ expect(adapter.get('f1')).toBe('feature')
115
+ adapter.add({ id: 'f2' })
116
+ adapter.delete('f1')
117
+ adapter.deleteAll()
118
+ adapter.undo()
119
+ adapter.deleteVertex()
120
+ adapter.nudgeSelectedVertex(1, 0, true)
121
+ const handler = () => {}
122
+ adapter.on('create', handler)
123
+ adapter.off('create', handler)
124
+ expect(adapter.setFeatureProperty('f1', 'stroke', '#000')).toBeUndefined()
125
+ adapter.setDrawingPreviewProperty('splitter', 'valid')
126
+ expect(manager.setDrawingPreviewProperty).toHaveBeenCalledWith('splitter', 'valid')
127
+ expect(manager.setInterfaceType).toHaveBeenCalledWith('touch')
128
+ expect(manager.on).toHaveBeenCalledWith('create', handler)
129
+ expect(manager.off).toHaveBeenCalledWith('create', handler)
130
+ expect(manager.undo).toHaveBeenCalled()
131
+ expect(manager.deleteVertex).toHaveBeenCalled()
132
+ expect(manager.nudgeSelectedVertex).toHaveBeenCalledWith(1, 0, true)
133
+ })
134
+
135
+ test('setGeometryValid records validity on the manager for finish gating', () => {
136
+ const { adapter, manager } = setup()
137
+ adapter.setGeometryValid(false)
138
+ expect(manager._geometryValid).toBe(false)
139
+ })
140
+
141
+ test('_geometryValidator accessor stores and retrieves the user callback on the manager', () => {
142
+ const { adapter, manager } = setup()
143
+ const validator = jest.fn(() => ({ valid: true }))
144
+ adapter._geometryValidator = validator
145
+ expect(manager._geometryValidator).toBe(validator)
146
+ expect(adapter._geometryValidator).toBe(validator)
147
+ })
148
+
149
+ test('setInvalid delegates to the manager to toggle the dashed stroke', () => {
150
+ const { adapter, manager } = setup()
151
+ adapter.setInvalid(true)
152
+ expect(manager.setInvalid).toHaveBeenCalledWith(true)
153
+ })
154
+
155
+ test('remove runs the olDraw cleanup', () => {
156
+ const { adapter } = setup()
157
+ adapter.remove()
158
+ expect(createOLDraw.mock.results.at(-1).value.remove).toHaveBeenCalled()
159
+ })