@defra/interactive-map 0.0.36-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 (248) 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-es/src/events.js +1 -1
  15. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  16. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  17. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  18. package/plugins/beta/map-styles/dist/css/index.css +1 -97
  19. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  20. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  21. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  22. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  23. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +13 -0
  24. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -0
  25. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -0
  26. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -0
  27. package/plugins/draw/dist/esm/index.js +1 -0
  28. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -0
  29. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +2 -0
  30. package/plugins/draw/dist/umd/im-draw-ol-adapter.js.LICENSE.txt +1 -0
  31. package/plugins/draw/dist/umd/im-draw-plugin.js +2 -0
  32. package/plugins/draw/dist/umd/im-draw-plugin.js.LICENSE.txt +1 -0
  33. package/plugins/draw/dist/umd/index.js +2 -0
  34. package/plugins/draw/dist/umd/index.js.LICENSE.txt +1 -0
  35. package/plugins/draw/src/DrawInit.jsx +89 -0
  36. package/plugins/draw/src/DrawInit.test.jsx +184 -0
  37. package/plugins/draw/src/adapterEvents.js +127 -0
  38. package/plugins/draw/src/adapterEvents.test.js +20 -0
  39. package/plugins/draw/src/adapters/adapterContract.test.js +48 -0
  40. package/plugins/draw/src/adapters/loadDrawAdapter.js +14 -0
  41. package/plugins/draw/src/adapters/loadDrawAdapter.test.js +53 -0
  42. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +367 -0
  43. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +799 -0
  44. package/plugins/draw/src/adapters/maplibre/defaults.js +1 -0
  45. package/plugins/draw/src/adapters/maplibre/drawEvents.js +35 -0
  46. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +126 -0
  47. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +268 -0
  48. package/plugins/draw/src/adapters/maplibre/mapboxSnap.js +39 -0
  49. package/plugins/draw/src/adapters/maplibre/mapboxSnap.test.js +91 -0
  50. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.js +58 -0
  51. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +53 -0
  52. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.js +23 -0
  53. package/plugins/draw/src/adapters/maplibre/modes/disabledMode.test.js +21 -0
  54. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.js +15 -0
  55. package/plugins/draw/src/adapters/maplibre/modes/drawLineMode.test.js +37 -0
  56. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +115 -0
  57. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +193 -0
  58. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +241 -0
  59. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.js +78 -0
  60. package/plugins/draw/src/adapters/maplibre/modes/drawMode/keyboardHandlers.test.js +75 -0
  61. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +78 -0
  62. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +48 -0
  63. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +103 -0
  64. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +127 -0
  65. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.js +59 -0
  66. package/plugins/draw/src/adapters/maplibre/modes/drawMode/renderHelpers.test.js +34 -0
  67. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.js +193 -0
  68. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +195 -0
  69. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.js +16 -0
  70. package/plugins/draw/src/adapters/maplibre/modes/drawPolygonMode.test.js +27 -0
  71. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/__helpers__/harness.js +107 -0
  72. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +139 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +46 -0
  74. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.js +2 -0
  75. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/helpers.test.js +17 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +151 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +123 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +161 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.test.js +98 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +133 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +105 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +166 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +133 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +210 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +167 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +121 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +73 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +249 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +153 -0
  90. package/plugins/draw/src/adapters/maplibre/snap/constants.js +2 -0
  91. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.js +39 -0
  92. package/plugins/draw/src/adapters/maplibre/snap/mapHandlers.test.js +106 -0
  93. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +148 -0
  94. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +238 -0
  95. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +115 -0
  96. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +151 -0
  97. package/plugins/draw/src/adapters/maplibre/snap/sourceData.js +34 -0
  98. package/plugins/draw/src/adapters/maplibre/snap/sourceData.test.js +62 -0
  99. package/plugins/draw/src/adapters/maplibre/styles.js +207 -0
  100. package/plugins/draw/src/adapters/maplibre/styles.test.js +155 -0
  101. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +202 -0
  102. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +253 -0
  103. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.js +57 -0
  104. package/plugins/draw/src/adapters/maplibre/utils/touchClickWorkaround.test.js +106 -0
  105. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +117 -0
  106. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +159 -0
  107. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +90 -0
  108. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +178 -0
  109. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +153 -0
  110. package/plugins/draw/src/adapters/openlayers/core/featureStore.js +69 -0
  111. package/plugins/draw/src/adapters/openlayers/core/featureStore.test.js +58 -0
  112. package/plugins/draw/src/adapters/openlayers/core/internalEvents.js +8 -0
  113. package/plugins/draw/src/adapters/openlayers/core/styles.js +155 -0
  114. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +143 -0
  115. package/plugins/draw/src/adapters/openlayers/defaults.js +1 -0
  116. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +272 -0
  117. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +431 -0
  118. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +139 -0
  119. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +140 -0
  120. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +144 -0
  121. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +196 -0
  122. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +385 -0
  123. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +303 -0
  124. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.js +55 -0
  125. package/plugins/draw/src/adapters/openlayers/edit/activeVertexLayer.test.js +53 -0
  126. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +157 -0
  127. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +195 -0
  128. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.js +57 -0
  129. package/plugins/draw/src/adapters/openlayers/edit/midpointLayer.test.js +42 -0
  130. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +83 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +80 -0
  132. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +118 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +165 -0
  134. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.js +79 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/pointerHandlers.test.js +78 -0
  136. package/plugins/draw/src/adapters/openlayers/edit/selectionState.js +116 -0
  137. package/plugins/draw/src/adapters/openlayers/edit/selectionState.test.js +109 -0
  138. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +184 -0
  139. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +133 -0
  140. package/plugins/draw/src/adapters/openlayers/edit/undoOps.js +94 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/undoOps.test.js +64 -0
  142. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.js +74 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/vertexHitTest.test.js +28 -0
  144. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.js +49 -0
  145. package/plugins/draw/src/adapters/openlayers/edit/vertexLayer.test.js +42 -0
  146. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.js +100 -0
  147. package/plugins/draw/src/adapters/openlayers/edit/vertexOps.test.js +80 -0
  148. package/plugins/draw/src/adapters/openlayers/olDraw.js +40 -0
  149. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +70 -0
  150. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +220 -0
  151. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +232 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.js +198 -0
  153. package/plugins/draw/src/adapters/openlayers/snap/snapGeometry.test.js +136 -0
  154. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +81 -0
  155. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.test.js +72 -0
  156. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +57 -0
  157. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +68 -0
  158. package/plugins/draw/src/adapters/openlayers/snap/snapManager.js +99 -0
  159. package/plugins/draw/src/adapters/openlayers/snap/snapManager.test.js +101 -0
  160. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.js +103 -0
  161. package/plugins/draw/src/adapters/openlayers/utils/geometryHelpers.test.js +76 -0
  162. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +35 -0
  163. package/plugins/draw/src/adapters/openlayers/utils/olCoords.test.js +22 -0
  164. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.js +22 -0
  165. package/plugins/draw/src/adapters/openlayers/utils/sketchHelpers.test.js +18 -0
  166. package/plugins/draw/src/adapters/openlayers/utils/touchTarget.js +8 -0
  167. package/plugins/draw/src/api/addFeature.js +22 -0
  168. package/plugins/draw/src/api/addFeature.test.js +41 -0
  169. package/plugins/draw/src/api/deleteFeature.js +11 -0
  170. package/plugins/draw/src/api/deleteFeature.test.js +24 -0
  171. package/plugins/draw/src/api/editFeature.js +54 -0
  172. package/plugins/draw/src/api/editFeature.test.js +134 -0
  173. package/plugins/draw/src/api/merge.js +30 -0
  174. package/plugins/draw/src/api/merge.test.js +57 -0
  175. package/plugins/draw/src/api/newLine.js +41 -0
  176. package/plugins/draw/src/api/newLine.test.js +93 -0
  177. package/plugins/draw/src/api/newPolygon.js +41 -0
  178. package/plugins/draw/src/api/newPolygon.test.js +93 -0
  179. package/plugins/draw/src/api/split.js +117 -0
  180. package/plugins/draw/src/api/split.test.js +204 -0
  181. package/plugins/draw/src/defaults.js +52 -0
  182. package/plugins/draw/src/defaults.test.js +18 -0
  183. package/plugins/draw/src/draw.scss +43 -0
  184. package/plugins/draw/src/events.js +237 -0
  185. package/plugins/draw/src/events.test.js +369 -0
  186. package/plugins/draw/src/index.js +10 -0
  187. package/plugins/draw/src/index.test.js +24 -0
  188. package/plugins/draw/src/manifest.js +170 -0
  189. package/plugins/draw/src/manifest.test.js +147 -0
  190. package/plugins/draw/src/reducer.js +69 -0
  191. package/plugins/draw/src/reducer.test.js +85 -0
  192. package/plugins/draw/src/utils/debounce.js +16 -0
  193. package/plugins/draw/src/utils/debounce.test.js +40 -0
  194. package/plugins/draw/src/utils/eventBus.js +36 -0
  195. package/plugins/draw/src/utils/eventBus.test.js +47 -0
  196. package/plugins/draw/src/utils/flattenStyleProperties.js +25 -0
  197. package/plugins/draw/src/utils/flattenStyleProperties.test.js +33 -0
  198. package/plugins/draw/src/utils/getValueForStyle.js +33 -0
  199. package/plugins/draw/src/utils/getValueForStyle.test.js +32 -0
  200. package/plugins/draw/src/utils/resolveColors.js +39 -0
  201. package/plugins/draw/src/utils/resolveColors.test.js +34 -0
  202. package/plugins/draw/src/utils/spatial.js +243 -0
  203. package/plugins/draw/src/utils/spatial.test.js +243 -0
  204. package/plugins/draw/src/utils/touchTarget.js +95 -0
  205. package/plugins/draw/src/utils/touchTarget.test.js +107 -0
  206. package/plugins/draw/src/utils/undoStack.js +31 -0
  207. package/plugins/draw/src/utils/undoStack.test.js +49 -0
  208. package/plugins/draw/src/validation/liveDrawChecks.js +120 -0
  209. package/plugins/draw/src/validation/liveDrawChecks.test.js +159 -0
  210. package/plugins/draw/src/validation/liveStroke.js +85 -0
  211. package/plugins/draw/src/validation/liveStroke.test.js +150 -0
  212. package/plugins/draw/src/validation/rules.areaError.test.js +12 -0
  213. package/plugins/draw/src/validation/rules.js +186 -0
  214. package/plugins/draw/src/validation/rules.test.js +120 -0
  215. package/plugins/draw/src/validation/validateGeometry.js +119 -0
  216. package/plugins/draw/src/validation/validateGeometry.test.js +200 -0
  217. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  218. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  219. package/plugins/interact/src/hooks/useInteractionHandlers.js +1 -1
  220. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +1 -1
  221. package/plugins/interact/src/utils/buildStylesMap.js +1 -0
  222. package/plugins/search/dist/css/index.css +1 -317
  223. package/rollup.esm.mjs +35 -28
  224. package/sonar-project.properties +2 -2
  225. package/src/App/components/Attributions/Attributions.module.scss +1 -0
  226. package/src/App/components/Hints/Hints.module.scss +5 -4
  227. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +1 -1
  228. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +11 -0
  229. package/src/App/components/MoveControl/MoveControl.jsx +73 -9
  230. package/src/App/components/MoveControl/MoveControl.module.scss +12 -2
  231. package/src/App/components/MoveControl/MoveControl.test.jsx +136 -0
  232. package/src/App/hooks/useHintsAPI.js +67 -0
  233. package/src/App/hooks/useHintsAPI.test.js +145 -0
  234. package/src/App/renderer/PluginInits.jsx +4 -0
  235. package/src/App/renderer/PluginInits.test.jsx +7 -1
  236. package/src/InteractiveMap/InteractiveMap.js +20 -0
  237. package/src/InteractiveMap/InteractiveMap.test.js +7 -0
  238. package/src/config/events.js +4 -0
  239. package/src/scss/settings/_dimensions.scss +3 -0
  240. package/src/services/announcer.js +38 -3
  241. package/src/services/announcer.test.js +133 -1
  242. package/src/services/hints.js +3 -0
  243. package/src/services/hints.test.js +19 -0
  244. package/src/types.js +12 -0
  245. package/src/utils/detectInterfaceType.js +5 -1
  246. package/src/utils/isMac.js +15 -0
  247. package/src/utils/isMac.test.js +37 -0
  248. package/webpack.umd.mjs +1 -0
@@ -0,0 +1,62 @@
1
+ import { pollUntil, patchSourceData } from './sourceData.js'
2
+
3
+ describe('pollUntil', () => {
4
+ beforeEach(() => {
5
+ global.requestAnimationFrame = jest.fn()
6
+ })
7
+
8
+ test('stops when checkFn returns null', () => {
9
+ const onSuccess = jest.fn()
10
+ pollUntil(() => null, onSuccess)
11
+ expect(onSuccess).not.toHaveBeenCalled()
12
+ expect(global.requestAnimationFrame).not.toHaveBeenCalled()
13
+ })
14
+
15
+ test('calls onSuccess when checkFn returns a truthy value', () => {
16
+ const onSuccess = jest.fn()
17
+ pollUntil(() => 'ready', onSuccess)
18
+ expect(onSuccess).toHaveBeenCalledWith('ready')
19
+ })
20
+
21
+ test('reschedules via requestAnimationFrame until the value becomes available', () => {
22
+ const values = [undefined, 'ready']
23
+ let i = 0
24
+ global.requestAnimationFrame = jest.fn((cb) => cb())
25
+ const onSuccess = jest.fn()
26
+
27
+ pollUntil(() => values[i++], onSuccess)
28
+
29
+ expect(global.requestAnimationFrame).toHaveBeenCalledTimes(1)
30
+ expect(onSuccess).toHaveBeenCalledWith('ready')
31
+ })
32
+ })
33
+
34
+ describe('patchSourceData', () => {
35
+ test('ignores a nullish source', () => {
36
+ expect(() => patchSourceData(null)).not.toThrow()
37
+ })
38
+
39
+ test('ignores a source that already exposes _data.features', () => {
40
+ const existing = { features: [] }
41
+ const source = { _data: existing }
42
+ patchSourceData(source)
43
+ expect(source._data).toBe(existing)
44
+ })
45
+
46
+ test('installs a _data accessor that normalizes assigned values', () => {
47
+ const source = {}
48
+ patchSourceData(source)
49
+
50
+ expect(source._data).toEqual({ type: 'FeatureCollection', features: [] })
51
+
52
+ const fc = { type: 'FeatureCollection', features: [{ id: 1 }] }
53
+ source._data = fc
54
+ expect(source._data).toBe(fc)
55
+
56
+ source._data = 'nonsense'
57
+ expect(source._data).toEqual({ type: 'FeatureCollection', features: [] })
58
+
59
+ source._data = { features: 'not-array' }
60
+ expect(source._data).toEqual({ type: 'FeatureCollection', features: [] })
61
+ })
62
+ })
@@ -0,0 +1,207 @@
1
+ // styles.js
2
+ import { SIZES } from './defaults.js'
3
+ import { resolveColors } from '../../utils/resolveColors.js'
4
+
5
+ // `defaultValue` is the already-resolved fallback (colors[defaultsKey] — the
6
+ // plugin-config override if one was set, else the built-in default), not the
7
+ // raw COLORS constant, so a shapeStroke/shapeFill override applies to every
8
+ // feature that doesn't set its own stroke/fill property.
9
+ const getUserProp = (mapStyle, prop, defaultValue) => [
10
+ 'coalesce',
11
+ ['get', `user_${prop}${mapStyle.id.charAt(0).toUpperCase() + mapStyle.id.slice(1)}`],
12
+ ['get', `user_${prop}`],
13
+ defaultValue
14
+ ]
15
+
16
+ // Inactive lines and fills
17
+ const fillInactive = (mapStyle, colors) => ({
18
+ id: 'fill-inactive',
19
+ type: 'fill',
20
+ filter: ['all', ['==', '$type', 'Polygon'], ['==', 'active', 'false']],
21
+ paint: { 'fill-color': getUserProp(mapStyle, 'fill', colors.shapeFill) }
22
+ })
23
+
24
+ const strokeInactive = (mapStyle, colors) => ({
25
+ id: 'stroke-inactive',
26
+ type: 'line',
27
+ filter: ['all', ['any', ['==', '$type', 'Polygon'], ['==', '$type', 'LineString']], ['==', 'active', 'false'], ['!has', 'user_splitter']],
28
+ layout: { 'line-cap': 'round', 'line-join': 'round' },
29
+ paint: {
30
+ 'line-color': getUserProp(mapStyle, 'stroke', colors.shapeStroke),
31
+ 'line-width': colors.strokeWidth
32
+ }
33
+ })
34
+
35
+ // Active lines and fills (sketch during drawing)
36
+ const fillActive = (editFillColor) => ({
37
+ id: 'fill-active',
38
+ type: 'fill',
39
+ filter: ['all', ['==', '$type', 'Polygon'], ['==', 'active', 'true']],
40
+ paint: { 'fill-color': editFillColor }
41
+ })
42
+
43
+ const strokeActive = (editStrokeColor) => ({
44
+ id: 'stroke-active',
45
+ type: 'line',
46
+ filter: ['all', ['any', ['==', '$type', 'Polygon'], ['==', '$type', 'LineString']], ['==', 'active', 'true'], ['!has', 'user_splitter']],
47
+ layout: { 'line-cap': 'round', 'line-join': 'round' },
48
+ paint: { 'line-color': editStrokeColor, 'line-width': 2, 'line-opacity': 1 }
49
+ })
50
+
51
+ // Dashed stroke shown in place of stroke-active while the shape is invalid. Same
52
+ // filter as stroke-active; the adapter's setInvalid() toggles the two layers'
53
+ // visibility (line-dasharray can't be data-driven per feature in MapLibre).
54
+ const strokeActiveInvalid = (invalidStrokeColor) => ({
55
+ id: 'stroke-active-invalid',
56
+ type: 'line',
57
+ filter: ['all', ['any', ['==', '$type', 'Polygon'], ['==', '$type', 'LineString']], ['==', 'active', 'true'], ['!has', 'user_splitter']],
58
+ layout: { 'line-cap': 'round', 'line-join': 'round', visibility: 'none' },
59
+ paint: {
60
+ 'line-color': invalidStrokeColor,
61
+ 'line-width': 2,
62
+ 'line-dasharray': [0.2, 2], // NOSONAR
63
+ 'line-opacity': 1
64
+ }
65
+ })
66
+
67
+ // Splitter line
68
+ const drawInvalidSplitter = (splitInvalidColor) => ({
69
+ id: 'stroke-invalid-splitter',
70
+ type: 'line',
71
+ filter: ['all', ['==', '$type', 'LineString'], ['==', 'active', 'true'], ['==', 'user_splitter', 'invalid']],
72
+ layout: { 'line-cap': 'round', 'line-join': 'round' },
73
+ paint: {
74
+ 'line-color': splitInvalidColor,
75
+ 'line-width': 2,
76
+ 'line-dasharray': [0.2, 2], // NOSONAR
77
+ 'line-opacity': 1
78
+ }
79
+ })
80
+
81
+ const drawValidSplitter = (splitValidColor) => ({
82
+ id: 'stroke-valid-splitter',
83
+ type: 'line',
84
+ filter: ['all', ['==', '$type', 'LineString'], ['==', 'active', 'true'], ['==', 'user_splitter', 'valid']],
85
+ layout: { 'line-cap': 'round', 'line-join': 'round' },
86
+ paint: {
87
+ 'line-color': splitValidColor,
88
+ 'line-width': 2,
89
+ 'line-opacity': 1
90
+ }
91
+ })
92
+
93
+ // Dashed preview line
94
+ const drawPreviewLine = (editStrokeColor) => ({
95
+ id: 'stroke-preview-line',
96
+ type: 'line',
97
+ filter: ['all', ['==', '$type', 'LineString'], ['==', 'active', 'true'], ['!has', 'user_splitter']],
98
+ layout: { 'line-cap': 'round', 'line-join': 'round' },
99
+ paint: { 'line-color': editStrokeColor, 'line-width': 2, 'line-dasharray': [0.2, 2], 'line-opacity': 1 } // NOSONAR
100
+ })
101
+
102
+ // Vertex layers ('draw-vertex' = display-only markers on placed vertices while drawing)
103
+ const vertex = (editVertexColor, vertexRadius) => ({
104
+ id: 'vertex',
105
+ type: 'circle',
106
+ filter: ['all', ['==', '$type', 'Point'], ['in', 'meta', 'vertex', 'draw-vertex']],
107
+ paint: { 'circle-radius': vertexRadius, 'circle-color': editVertexColor }
108
+ })
109
+
110
+ const vertexHalo = (editHaloColor, editActiveColor, vertexHaloRadius) => ({
111
+ id: 'vertex-halo',
112
+ type: 'circle',
113
+ filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'vertex'], ['==', 'active', 'true']],
114
+ paint: { 'circle-radius': vertexHaloRadius, 'circle-stroke-width': 3, 'circle-color': editHaloColor, 'circle-stroke-color': editActiveColor }
115
+ })
116
+
117
+ const vertexActive = (editVertexColor, vertexRadius) => ({
118
+ id: 'vertex-active',
119
+ type: 'circle',
120
+ filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'vertex'], ['==', 'active', 'true']],
121
+ paint: { 'circle-radius': vertexRadius, 'circle-color': editVertexColor }
122
+ })
123
+
124
+ // Midpoints
125
+ const midpoint = (editMidpointColor, midpointRadius) => ({
126
+ id: 'midpoint',
127
+ type: 'circle',
128
+ filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'midpoint']],
129
+ paint: { 'circle-radius': midpointRadius, 'circle-color': editMidpointColor }
130
+ })
131
+
132
+ const midpointHalo = (editHaloColor, editActiveColor, midpointHaloRadius) => ({
133
+ id: 'midpoint-halo',
134
+ type: 'circle',
135
+ filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'midpoint'], ['==', 'active', 'true']],
136
+ paint: { 'circle-radius': midpointHaloRadius, 'circle-stroke-width': 3, 'circle-color': editHaloColor, 'circle-stroke-color': editActiveColor }
137
+ })
138
+
139
+ const midpointActive = (editMidpointColor, midpointRadius) => ({
140
+ id: 'midpoint-active',
141
+ type: 'circle',
142
+ filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'midpoint'], ['==', 'active', 'true']],
143
+ paint: { 'circle-radius': midpointRadius, 'circle-color': editMidpointColor }
144
+ })
145
+
146
+ const circle = (editStrokeColor) => ({
147
+ id: 'circle',
148
+ type: 'line',
149
+ filter: ['==', 'id', 'circle'],
150
+ paint: { 'line-color': editStrokeColor, 'line-width': 2, 'line-opacity': 0.8 }
151
+ })
152
+
153
+ const touchVertexIndicator = () => ({
154
+ id: 'touch-vertex-indicator',
155
+ type: 'circle',
156
+ filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'touch-vertex-indicator']],
157
+ paint: { 'circle-radius': 30, 'circle-color': '#3bb2d0', 'circle-stroke-width': 3, 'circle-stroke-color': '#ffffff', 'circle-opacity': 0.9 }
158
+ })
159
+
160
+ // `pluginConfig` lets callers override any of these colours/strokeWidth — see
161
+ // resolveColors() and the draw plugin's own options docs. Unaffected keys
162
+ // still resolve to the built-in COLORS/SIZES defaults.
163
+ const createDrawStyles = (mapStyle, pluginConfig = {}) => {
164
+ const colors = resolveColors(mapStyle, pluginConfig)
165
+ const { vertexRadius, midpointRadius, vertexHaloRadius, midpointHaloRadius } = SIZES
166
+
167
+ return [
168
+ fillInactive(mapStyle, colors),
169
+ fillActive(colors.editFill),
170
+ strokeActive(colors.editStroke),
171
+ strokeActiveInvalid(colors.invalidStroke),
172
+ strokeInactive(mapStyle, colors),
173
+ drawInvalidSplitter(colors.splitInvalid),
174
+ drawValidSplitter(colors.splitValid),
175
+ drawPreviewLine(colors.editStroke),
176
+ midpoint(colors.editMidpoint, midpointRadius),
177
+ midpointHalo(colors.editHalo, colors.editActive, midpointHaloRadius),
178
+ midpointActive(colors.editMidpoint, midpointRadius),
179
+ vertex(colors.editVertex, vertexRadius),
180
+ vertexHalo(colors.editHalo, colors.editActive, vertexHaloRadius),
181
+ vertexActive(colors.editVertex, vertexRadius),
182
+ circle(colors.editStroke),
183
+ touchVertexIndicator()
184
+ ]
185
+ }
186
+
187
+ /**
188
+ * Helper to iterate over a MapLibre map and apply new paint properties
189
+ */
190
+ const updateDrawStyles = (map, mapStyle, pluginConfig = {}) => {
191
+ const layers = createDrawStyles(mapStyle, pluginConfig)
192
+ layers.forEach(layer => {
193
+ Object.entries(layer.paint).forEach(([prop, value]) => {
194
+ if (map.getLayer(`${layer.id}.cold`)) {
195
+ map.setPaintProperty(`${layer.id}.cold`, prop, value)
196
+ }
197
+ if (map.getLayer(`${layer.id}.hot`)) {
198
+ map.setPaintProperty(`${layer.id}.hot`, prop, value)
199
+ }
200
+ })
201
+ })
202
+ }
203
+
204
+ export {
205
+ createDrawStyles,
206
+ updateDrawStyles
207
+ }
@@ -0,0 +1,155 @@
1
+ import { createDrawStyles, updateDrawStyles } from './styles.js'
2
+ import { COLORS, SIZES } from './defaults.js'
3
+ import { getValueForStyle } from '../../utils/getValueForStyle.js'
4
+
5
+ const findLayer = (layers, id) => layers.find((l) => l.id === id)
6
+
7
+ describe('createDrawStyles', () => {
8
+ const mapStyle = { id: 'outdoor', mapColorScheme: 'light' }
9
+
10
+ test('returns every draw layer in order', () => {
11
+ const layers = createDrawStyles(mapStyle)
12
+ expect(layers.map((l) => l.id)).toEqual([
13
+ 'fill-inactive', 'fill-active', 'stroke-active', 'stroke-active-invalid', 'stroke-inactive',
14
+ 'stroke-invalid-splitter', 'stroke-valid-splitter', 'stroke-preview-line',
15
+ 'midpoint', 'midpoint-halo', 'midpoint-active',
16
+ 'vertex', 'vertex-halo', 'vertex-active', 'circle', 'touch-vertex-indicator'
17
+ ])
18
+ })
19
+
20
+ test('the invalid stroke layer is a hidden dashed line matching the active shape', () => {
21
+ const layers = createDrawStyles(mapStyle)
22
+ const invalid = findLayer(layers, 'stroke-active-invalid')
23
+ expect(invalid.layout.visibility).toBe('none') // hidden until the shape is invalid
24
+ expect(invalid.paint['line-dasharray']).toEqual([0.2, 2])
25
+ expect(invalid.paint['line-color']).toBe(getValueForStyle(COLORS.invalidStroke, 'light'))
26
+ expect(invalid.filter).toEqual(findLayer(layers, 'stroke-active').filter)
27
+ })
28
+
29
+ test('resolves light-scheme colours', () => {
30
+ const layers = createDrawStyles({ id: 'outdoor', mapColorScheme: 'light' })
31
+ expect(findLayer(layers, 'stroke-active').paint['line-color']).toBe(getValueForStyle(COLORS.editStroke, 'light'))
32
+ expect(findLayer(layers, 'fill-active').paint['fill-color']).toBe(getValueForStyle(COLORS.editFill, 'light'))
33
+ expect(findLayer(layers, 'vertex').paint['circle-color']).toBe(getValueForStyle(COLORS.editVertex, 'light'))
34
+ expect(findLayer(layers, 'midpoint').paint['circle-color']).toBe(getValueForStyle(COLORS.editMidpoint, 'light'))
35
+ })
36
+
37
+ test('resolves dark-scheme colours', () => {
38
+ const layers = createDrawStyles({ id: 'night', mapColorScheme: 'dark' })
39
+ expect(findLayer(layers, 'stroke-active').paint['line-color']).toBe(getValueForStyle(COLORS.editStroke, 'dark'))
40
+ expect(findLayer(layers, 'vertex-halo').paint['circle-color']).toBe(getValueForStyle(COLORS.editHalo, 'dark'))
41
+ expect(findLayer(layers, 'vertex-halo').paint['circle-stroke-color']).toBe(getValueForStyle(COLORS.editActive, 'dark'))
42
+ })
43
+
44
+ test('defaults to the light scheme when none is provided', () => {
45
+ const layers = createDrawStyles({ id: 'outdoor' })
46
+ expect(findLayer(layers, 'stroke-active').paint['line-color']).toBe(getValueForStyle(COLORS.editStroke, 'light'))
47
+ })
48
+
49
+ test('applies the configured sizes', () => {
50
+ const layers = createDrawStyles(mapStyle)
51
+ expect(findLayer(layers, 'vertex').paint['circle-radius']).toBe(SIZES.vertexRadius)
52
+ expect(findLayer(layers, 'vertex-halo').paint['circle-radius']).toBe(SIZES.vertexHaloRadius)
53
+ expect(findLayer(layers, 'midpoint').paint['circle-radius']).toBe(SIZES.midpointRadius)
54
+ expect(findLayer(layers, 'midpoint-halo').paint['circle-radius']).toBe(SIZES.midpointHaloRadius)
55
+ expect(findLayer(layers, 'stroke-inactive').paint['line-width']).toBe(SIZES.strokeWidth)
56
+ })
57
+
58
+ test('builds per-style user-property coalesce expressions for inactive fill and stroke', () => {
59
+ const layers = createDrawStyles({ id: 'outdoor', mapColorScheme: 'light' })
60
+
61
+ expect(findLayer(layers, 'fill-inactive').paint['fill-color']).toEqual([
62
+ 'coalesce',
63
+ ['get', 'user_fillOutdoor'],
64
+ ['get', 'user_fill'],
65
+ COLORS.shapeFill
66
+ ])
67
+ expect(findLayer(layers, 'stroke-inactive').paint['line-color']).toEqual([
68
+ 'coalesce',
69
+ ['get', 'user_strokeOutdoor'],
70
+ ['get', 'user_stroke'],
71
+ COLORS.shapeStroke
72
+ ])
73
+ })
74
+
75
+ test('capitalises the style id in the user-property key', () => {
76
+ const layers = createDrawStyles({ id: 'satellite', mapColorScheme: 'light' })
77
+ expect(findLayer(layers, 'fill-inactive').paint['fill-color'][1]).toEqual(['get', 'user_fillSatellite'])
78
+ })
79
+
80
+ test('colours the splitter and touch-indicator layers', () => {
81
+ const layers = createDrawStyles(mapStyle)
82
+ expect(findLayer(layers, 'stroke-invalid-splitter').paint['line-color']).toBe(getValueForStyle(COLORS.splitInvalid, 'light'))
83
+ expect(findLayer(layers, 'stroke-valid-splitter').paint['line-color']).toBe(getValueForStyle(COLORS.splitValid, 'light'))
84
+ expect(findLayer(layers, 'touch-vertex-indicator').paint['circle-color']).toBe('#3bb2d0')
85
+ })
86
+
87
+ test('a pluginConfig override replaces the default colour/width everywhere it applies', () => {
88
+ const pluginConfig = { editStroke: '#custom-stroke', shapeStroke: '#custom-shape', strokeWidth: 9 }
89
+ const layers = createDrawStyles(mapStyle, pluginConfig)
90
+
91
+ expect(findLayer(layers, 'stroke-active').paint['line-color']).toBe('#custom-stroke')
92
+ expect(findLayer(layers, 'stroke-preview-line').paint['line-color']).toBe('#custom-stroke')
93
+ expect(findLayer(layers, 'circle').paint['line-color']).toBe('#custom-stroke')
94
+ expect(findLayer(layers, 'stroke-inactive').paint['line-color']).toEqual([
95
+ 'coalesce',
96
+ ['get', 'user_strokeOutdoor'],
97
+ ['get', 'user_stroke'],
98
+ '#custom-shape'
99
+ ])
100
+ expect(findLayer(layers, 'stroke-inactive').paint['line-width']).toBe(9)
101
+ })
102
+
103
+ test('a per-feature stroke/fill property still wins over a pluginConfig override — coalesce checks it first', () => {
104
+ const layers = createDrawStyles(mapStyle, { shapeStroke: '#custom-shape' })
105
+ const [, userStyleKey, userKey, fallback] = findLayer(layers, 'stroke-inactive').paint['line-color']
106
+ expect(userStyleKey).toEqual(['get', 'user_strokeOutdoor'])
107
+ expect(userKey).toEqual(['get', 'user_stroke'])
108
+ expect(fallback).toBe('#custom-shape') // only reached when the feature sets neither
109
+ })
110
+ })
111
+
112
+ describe('updateDrawStyles', () => {
113
+ const mapStyle = { id: 'outdoor', mapColorScheme: 'light' }
114
+
115
+ test('applies paint properties to both the cold and hot copies of each layer', () => {
116
+ const map = { getLayer: jest.fn(() => true), setPaintProperty: jest.fn() }
117
+
118
+ updateDrawStyles(map, mapStyle)
119
+
120
+ expect(map.setPaintProperty).toHaveBeenCalledWith('fill-inactive.cold', 'fill-color', expect.anything())
121
+ expect(map.setPaintProperty).toHaveBeenCalledWith('fill-inactive.hot', 'fill-color', expect.anything())
122
+ expect(map.setPaintProperty).toHaveBeenCalledWith('vertex.cold', 'circle-radius', SIZES.vertexRadius)
123
+ expect(map.setPaintProperty).toHaveBeenCalledWith('vertex.hot', 'circle-color', expect.anything())
124
+ })
125
+
126
+ test('skips layers that are not present on the map', () => {
127
+ const map = { getLayer: jest.fn(() => false), setPaintProperty: jest.fn() }
128
+
129
+ updateDrawStyles(map, mapStyle)
130
+
131
+ expect(map.setPaintProperty).not.toHaveBeenCalled()
132
+ })
133
+
134
+ test('updates only the cold layer when the hot copy is absent', () => {
135
+ const map = {
136
+ getLayer: jest.fn((id) => id.endsWith('.cold')),
137
+ setPaintProperty: jest.fn()
138
+ }
139
+
140
+ updateDrawStyles(map, mapStyle)
141
+
142
+ const targets = map.setPaintProperty.mock.calls.map(([id]) => id)
143
+ expect(targets.length).toBeGreaterThan(0)
144
+ expect(targets.every((id) => id.endsWith('.cold'))).toBe(true)
145
+ expect(targets.some((id) => id.endsWith('.hot'))).toBe(false)
146
+ })
147
+
148
+ test('applies a pluginConfig override on the re-styled layers', () => {
149
+ const map = { getLayer: jest.fn(() => true), setPaintProperty: jest.fn() }
150
+
151
+ updateDrawStyles(map, mapStyle, { editStroke: '#custom' })
152
+
153
+ expect(map.setPaintProperty).toHaveBeenCalledWith('stroke-active.cold', 'line-color', '#custom')
154
+ })
155
+ })
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Snap helper utilities for draw modes
3
+ * Provides a consistent interface for snap detection and coordinate retrieval
4
+ */
5
+
6
+ import { TOLERANCES } from '../../../defaults.js'
7
+
8
+ /**
9
+ * Get the snap instance from the map
10
+ * @param {maplibregl.Map} map - Map instance
11
+ * @returns {MapboxSnap|null} Snap instance or null
12
+ */
13
+ export function getSnapInstance (map) {
14
+ return map?._snapInstance ?? null
15
+ }
16
+
17
+ /**
18
+ * Check if snapping is currently active
19
+ * @param {MapboxSnap} snap - Snap instance
20
+ * @returns {boolean} True if snap is active with valid coordinates
21
+ */
22
+ export function isSnapActive (snap) {
23
+ // Also check snap.status to ensure snap feature is enabled
24
+ return !!(snap?.status && snap?.snapStatus && snap.snapCoords?.length >= 2)
25
+ }
26
+
27
+ /**
28
+ * Get snapped coordinates as lngLat object
29
+ * @param {MapboxSnap} snap - Snap instance
30
+ * @returns {{ lng: number, lat: number }|null} Snapped coordinates or null
31
+ */
32
+ export function getSnapLngLat (snap) {
33
+ if (!isSnapActive(snap)) {
34
+ return null
35
+ }
36
+ return {
37
+ lng: snap.snapCoords[0],
38
+ lat: snap.snapCoords[1]
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Get snapped coordinates as array [lng, lat]
44
+ * @param {MapboxSnap} snap - Snap instance
45
+ * @returns {[number, number]|null} Snapped coordinates or null
46
+ */
47
+ export function getSnapCoords (snap) {
48
+ if (!isSnapActive(snap)) {
49
+ return null
50
+ }
51
+ return [snap.snapCoords[0], snap.snapCoords[1]]
52
+ }
53
+
54
+ /**
55
+ * Trigger snap detection at a given point
56
+ * @param {MapboxSnap} snap - Snap instance
57
+ * @param {maplibregl.Map} map - Map instance
58
+ * @param {{ x: number, y: number }} point - Screen point
59
+ * @returns {boolean} True if snap was triggered
60
+ */
61
+ export function triggerSnapAtPoint (snap, map, point) {
62
+ if (!snap || !map || !snap.status) {
63
+ return false
64
+ }
65
+
66
+ const lngLat = map.unproject(point)
67
+ snap.snapToClosestPoint({ point, lngLat })
68
+ return true
69
+ }
70
+
71
+ /**
72
+ * Trigger snap detection at map center (for touch/keyboard modes)
73
+ * @param {MapboxSnap} snap - Snap instance
74
+ * @param {maplibregl.Map} map - Map instance
75
+ * @returns {boolean} True if snap was triggered
76
+ */
77
+ export function triggerSnapAtCenter (snap, map) {
78
+ // Don't trigger snap if library is disabled
79
+ if (!snap || !map || !snap.status) {
80
+ return false
81
+ }
82
+
83
+ const center = map.getCenter()
84
+ const point = map.project(center)
85
+ snap.snapToClosestPoint({ point, lngLat: center })
86
+ return true
87
+ }
88
+
89
+ /**
90
+ * Clear the snap indicator circle and all internal snap state
91
+ * @param {MapboxSnap} snap - Snap instance
92
+ * @param {maplibregl.Map} [map] - Optional map instance for direct layer control
93
+ */
94
+ export function clearSnapIndicator (snap, map) {
95
+ if (snap) {
96
+ snap.snapStatus = false
97
+ snap.snapCoords = null
98
+ // Clear arrays in place (avoids creating new objects, reduces GC pressure)
99
+ if (snap.snappedFeatures?.length) {
100
+ snap.snappedFeatures.length = 0
101
+ }
102
+ if (snap.closeFeatures?.length) {
103
+ snap.closeFeatures.length = 0
104
+ }
105
+ if (snap.lines?.length) {
106
+ snap.lines.length = 0
107
+ }
108
+ // Note: Avoid calling setMapData here - it's expensive in Safari
109
+ }
110
+
111
+ // Just hide the layer - much cheaper than setData() in Safari
112
+ if (map?.getLayer('snap-helper-circle')) {
113
+ map.setLayoutProperty('snap-helper-circle', 'visibility', 'none')
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Clear all snap state (for use between drag operations)
119
+ * @param {MapboxSnap} snap - Snap instance
120
+ */
121
+ export function clearSnapState (snap) {
122
+ if (!snap) {
123
+ return
124
+ }
125
+ snap.snapStatus = false
126
+ snap.snapCoords = null
127
+ // Clear arrays in place (avoids creating new objects, reduces GC pressure)
128
+ if (snap.snappedFeatures?.length) {
129
+ snap.snappedFeatures.length = 0
130
+ }
131
+ if (snap.closeFeatures?.length) {
132
+ snap.closeFeatures.length = 0
133
+ }
134
+ if (snap.lines?.length) {
135
+ snap.lines.length = 0
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Get snap radius in pixels
141
+ * @param {MapboxSnap} snap - Snap instance
142
+ * @returns {number} Snap radius in pixels (falls back to the configured default)
143
+ */
144
+ export function getSnapRadius (snap) {
145
+ return snap?.options?.radius ?? TOLERANCES.snapRadius
146
+ }
147
+
148
+ /**
149
+ * Check if snapping is enabled for a given state
150
+ * @param {object} state - Mode state with optional getSnapEnabled function
151
+ * @returns {boolean} True if snapping is enabled
152
+ */
153
+ export function isSnapEnabled (state) {
154
+ // Only return true if getSnapEnabled exists and explicitly returns true
155
+ if (typeof state?.getSnapEnabled !== 'function') {
156
+ return false
157
+ }
158
+ return state.getSnapEnabled() === true
159
+ }
160
+
161
+ /**
162
+ * Create a synthetic map event with snapped coordinates
163
+ * @param {object} e - Original event
164
+ * @param {MapboxSnap} snap - Snap instance
165
+ * @returns {object} Event with snapped lngLat or original event
166
+ */
167
+ export function createSnappedEvent (e, snap) {
168
+ const lngLat = getSnapLngLat(snap)
169
+ if (!lngLat) {
170
+ return e
171
+ }
172
+
173
+ return {
174
+ ...e,
175
+ lngLat
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Create a synthetic click event at snapped coordinates
181
+ * @param {maplibregl.Map} map - Map instance
182
+ * @param {MapboxSnap} snap - Snap instance
183
+ * @returns {object|null} Synthetic event or null if no snap
184
+ */
185
+ export function createSnappedClickEvent (map, snap) {
186
+ const lngLat = getSnapLngLat(snap)
187
+ if (!lngLat) {
188
+ return null
189
+ }
190
+
191
+ const point = map.project([lngLat.lng, lngLat.lat])
192
+ return {
193
+ lngLat,
194
+ point,
195
+ originalEvent: new MouseEvent('click', {
196
+ clientX: point.x,
197
+ clientY: point.y,
198
+ bubbles: true,
199
+ cancelable: true
200
+ })
201
+ }
202
+ }