@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,148 @@
1
+ import MapboxSnap from 'mapbox-gl-snap/dist/esm/MapboxSnap.js'
2
+ import { polygon, lineString } from '@turf/helpers'
3
+ import { TOLERANCES } from '../defaults.js'
4
+ import { SNAP_HELPER_LAYER } from './constants.js'
5
+
6
+ const MAX_SNAP_FEATURE_CACHE = 100
7
+
8
+ // Skip setMapData/drawingSnapCheck when disabled; ensure layer visibility when enabled
9
+ function patchDataMethods (proto, orig) {
10
+ proto.setMapData = function (data) {
11
+ if (!this.status) {
12
+ return undefined
13
+ }
14
+ const result = orig.setMapData.call(this, data)
15
+ if (data?.features?.length > 0 && this.map?.getLayer(SNAP_HELPER_LAYER)) {
16
+ this.map.setLayoutProperty(SNAP_HELPER_LAYER, 'visibility', 'visible')
17
+ }
18
+ return result
19
+ }
20
+
21
+ // Skip drawingSnapCheck when disabled
22
+ proto.drawingSnapCheck = function () {
23
+ if (!this.status) {
24
+ return undefined
25
+ }
26
+ return orig.drawingSnapCheck.call(this)
27
+ }
28
+ }
29
+
30
+ // Fix typo ('coodinates') and validate coordinates; query within a radius bbox
31
+ function patchGeometryMethods (proto, orig) {
32
+ proto.getLines = function (feature, mouse, radiusArg) {
33
+ const geom = feature.geometry
34
+ if (!geom?.coordinates) {
35
+ return []
36
+ }
37
+ const coords = geom.coordinates
38
+ // Validate that we have actual coordinate arrays
39
+ if (!Array.isArray(coords) || coords.length === 0) {
40
+ return []
41
+ }
42
+ try {
43
+ if (geom.type === 'MultiPolygon') {
44
+ return coords.filter(c => Array.isArray(c) && c.length > 0).map(c => polygon(c))
45
+ }
46
+ if (geom.type === 'MultiLineString') {
47
+ return coords.filter(c => Array.isArray(c) && c.length > 0).map(c => lineString(c))
48
+ }
49
+ return orig.getLines.call(this, feature, mouse, radiusArg)
50
+ } catch {
51
+ // Invalid geometry - skip this feature
52
+ return []
53
+ }
54
+ }
55
+
56
+ // Query within radius bbox instead of just point, filter to existing layers
57
+ proto.getCloseFeatures = function (e, radiusInMeters) {
58
+ if (!this.status) {
59
+ return []
60
+ }
61
+ // Use active layers (per-call override) or fall back to default layers
62
+ const activeLayers = this._activeLayers || this._defaultLayers || []
63
+ this.options.layers = activeLayers.filter(l => this.map.getLayer(l))
64
+ const r = this.options.radius || TOLERANCES.snapRadius
65
+ const origPt = e.point
66
+ e.point = [[origPt.x - r, origPt.y - r], [origPt.x + r, origPt.y + r]]
67
+ const result = orig.getCloseFeatures.call(this, e, radiusInMeters)
68
+ e.point = origPt
69
+ return result
70
+ }
71
+ }
72
+
73
+ // Custom colors for snap indicators
74
+ function patchColorMethods (proto, orig, colors) {
75
+ proto.searchInVertex = function (...args) {
76
+ const r = orig.searchInVertex.apply(this, args)
77
+ if (r) {
78
+ r.color = colors.vertex
79
+ }
80
+ return r
81
+ }
82
+ proto.searchInMidPoint = function (...args) {
83
+ const r = orig.searchInMidPoint.apply(this, args)
84
+ if (r) {
85
+ r.color = colors.midpoint
86
+ }
87
+ return r
88
+ }
89
+ proto.searchInEdge = function (...args) {
90
+ const r = orig.searchInEdge.apply(this, args)
91
+ if (r) {
92
+ r.color = colors.edge
93
+ }
94
+ return r
95
+ }
96
+ }
97
+
98
+ // Skip when disabled or zooming; clean up internal arrays to prevent memory accumulation
99
+ function patchSnapMethod (proto, orig) {
100
+ proto.snapToClosestPoint = function (e) {
101
+ if (!this.status || this.map?._isZooming) {
102
+ return undefined
103
+ }
104
+ try {
105
+ const result = orig.snapToClosestPoint.call(this, e)
106
+ if (this.closeFeatures?.length > MAX_SNAP_FEATURE_CACHE) {
107
+ this.closeFeatures.length = 0
108
+ }
109
+ if (this.lines?.length > MAX_SNAP_FEATURE_CACHE) {
110
+ this.lines.length = 0
111
+ }
112
+ return result
113
+ } catch {
114
+ // Invalid geometry encountered - clear state and continue
115
+ this.snapStatus = false
116
+ this.snapCoords = null
117
+ return undefined
118
+ }
119
+ }
120
+ }
121
+
122
+ /** Apply patches to MapboxSnap prototype (once only) */
123
+ export function applyMapboxSnapPatches (colors) {
124
+ if (MapboxSnap.prototype.__snapPatched) {
125
+ return
126
+ }
127
+ MapboxSnap.prototype.__snapPatched = true
128
+
129
+ const proto = MapboxSnap.prototype
130
+ const orig = {
131
+ setMapData: proto.setMapData,
132
+ drawingSnapCheck: proto.drawingSnapCheck,
133
+ getLines: proto.getLines,
134
+ getCloseFeatures: proto.getCloseFeatures,
135
+ searchInVertex: proto.searchInVertex,
136
+ searchInMidPoint: proto.searchInMidPoint,
137
+ searchInEdge: proto.searchInEdge,
138
+ snapToClosestPoint: proto.snapToClosestPoint
139
+ }
140
+
141
+ // Disable changeSnappedPoints - we handle snap ourselves in drag handlers
142
+ proto.changeSnappedPoints = () => {}
143
+
144
+ patchDataMethods(proto, orig)
145
+ patchGeometryMethods(proto, orig)
146
+ patchColorMethods(proto, orig, colors)
147
+ patchSnapMethod(proto, orig)
148
+ }
@@ -0,0 +1,238 @@
1
+ import MapboxSnap from 'mapbox-gl-snap/dist/esm/MapboxSnap.js'
2
+ import { polygon, lineString } from '@turf/helpers'
3
+ import { applyMapboxSnapPatches } from './prototypePatches.js'
4
+
5
+ const SNAP_LAYER = 'snap-helper-circle'
6
+ const COLORS = { vertex: 'colour-v', midpoint: 'colour-m', edge: 'colour-e' }
7
+
8
+ jest.mock('mapbox-gl-snap/dist/esm/MapboxSnap.js', () => {
9
+ const orig = {
10
+ setMapData: jest.fn(function () { return 'orig-setMapData' }),
11
+ drawingSnapCheck: jest.fn(function () { return 'orig-drawingSnapCheck' }),
12
+ getLines: jest.fn(function () { return 'orig-getLines' }),
13
+ getCloseFeatures: jest.fn(function () { return 'orig-getCloseFeatures' }),
14
+ searchInVertex: jest.fn(function () { return { id: 'v' } }),
15
+ searchInMidPoint: jest.fn(function () { return { id: 'm' } }),
16
+ searchInEdge: jest.fn(function () { return { id: 'e' } }),
17
+ snapToClosestPoint: jest.fn(function () { return 'orig-snap' })
18
+ }
19
+ const MockSnap = jest.fn()
20
+ Object.assign(MockSnap.prototype, orig)
21
+ MockSnap.__orig = orig
22
+ return { __esModule: true, default: MockSnap }
23
+ })
24
+
25
+ jest.mock('@turf/helpers', () => ({
26
+ polygon: jest.fn((c) => ({ poly: c })),
27
+ lineString: jest.fn((c) => ({ line: c }))
28
+ }))
29
+
30
+ jest.mock('../defaults.js', () => ({ TOLERANCES: { snapRadius: 12 } }))
31
+
32
+ beforeAll(() => {
33
+ applyMapboxSnapPatches(COLORS)
34
+ })
35
+
36
+ beforeEach(() => {
37
+ jest.clearAllMocks()
38
+ })
39
+
40
+ describe('applyMapboxSnapPatches', () => {
41
+ test('patches the prototype only once', () => {
42
+ const patched = MapboxSnap.prototype.setMapData
43
+ applyMapboxSnapPatches(COLORS)
44
+ expect(MapboxSnap.prototype.setMapData).toBe(patched)
45
+ })
46
+
47
+ test('replaces changeSnappedPoints with a no-op', () => {
48
+ expect(MapboxSnap.prototype.changeSnappedPoints()).toBeUndefined()
49
+ })
50
+ })
51
+
52
+ describe('setMapData', () => {
53
+ test('returns undefined and skips the original when disabled', () => {
54
+ expect(MapboxSnap.prototype.setMapData.call({ status: false }, {})).toBeUndefined()
55
+ expect(MapboxSnap.__orig.setMapData).not.toHaveBeenCalled()
56
+ })
57
+
58
+ test('delegates and shows the layer when features exist and the layer is present', () => {
59
+ const map = { getLayer: jest.fn(() => ({})), setLayoutProperty: jest.fn() }
60
+ const data = { features: [{}] }
61
+
62
+ const result = MapboxSnap.prototype.setMapData.call({ status: true, map }, data)
63
+
64
+ expect(MapboxSnap.__orig.setMapData).toHaveBeenCalledWith(data)
65
+ expect(result).toBe('orig-setMapData')
66
+ expect(map.setLayoutProperty).toHaveBeenCalledWith(SNAP_LAYER, 'visibility', 'visible')
67
+ })
68
+
69
+ test('does not show the layer when there are no features', () => {
70
+ const map = { getLayer: jest.fn(() => ({})), setLayoutProperty: jest.fn() }
71
+ MapboxSnap.prototype.setMapData.call({ status: true, map }, { features: [] })
72
+ expect(map.setLayoutProperty).not.toHaveBeenCalled()
73
+ })
74
+
75
+ test('does not show the layer when it is absent', () => {
76
+ const map = { getLayer: jest.fn(() => null), setLayoutProperty: jest.fn() }
77
+ MapboxSnap.prototype.setMapData.call({ status: true, map }, { features: [{}] })
78
+ expect(map.setLayoutProperty).not.toHaveBeenCalled()
79
+ })
80
+
81
+ test('tolerates a missing map and missing data', () => {
82
+ expect(MapboxSnap.prototype.setMapData.call({ status: true }, { features: [{}] })).toBe('orig-setMapData')
83
+ expect(MapboxSnap.prototype.setMapData.call({ status: true }, undefined)).toBe('orig-setMapData')
84
+ })
85
+ })
86
+
87
+ describe('drawingSnapCheck', () => {
88
+ test('returns undefined when disabled', () => {
89
+ expect(MapboxSnap.prototype.drawingSnapCheck.call({ status: false })).toBeUndefined()
90
+ })
91
+
92
+ test('delegates to the original when enabled', () => {
93
+ expect(MapboxSnap.prototype.drawingSnapCheck.call({ status: true })).toBe('orig-drawingSnapCheck')
94
+ expect(MapboxSnap.__orig.drawingSnapCheck).toHaveBeenCalled()
95
+ })
96
+ })
97
+
98
+ describe('getLines', () => {
99
+ const call = (self, feature) => MapboxSnap.prototype.getLines.call(self, feature, 'mouse', 5)
100
+
101
+ test('returns [] when geometry or coordinates are missing', () => {
102
+ expect(call({}, { geometry: null })).toEqual([])
103
+ expect(call({}, { geometry: {} })).toEqual([])
104
+ })
105
+
106
+ test('returns [] when coordinates are not a non-empty array', () => {
107
+ expect(call({}, { geometry: { coordinates: 'x' } })).toEqual([])
108
+ expect(call({}, { geometry: { coordinates: [] } })).toEqual([])
109
+ })
110
+
111
+ test('maps MultiPolygon coordinates via turf polygon, filtering empties', () => {
112
+ const feature = { geometry: { type: 'MultiPolygon', coordinates: [[[[0, 0]]], []] } }
113
+ expect(call({}, feature)).toEqual([{ poly: [[[0, 0]]] }])
114
+ expect(polygon).toHaveBeenCalledWith([[[0, 0]]])
115
+ })
116
+
117
+ test('maps MultiLineString coordinates via turf lineString', () => {
118
+ const feature = { geometry: { type: 'MultiLineString', coordinates: [[[0, 0], [1, 1]]] } }
119
+ expect(call({}, feature)).toEqual([{ line: [[0, 0], [1, 1]] }])
120
+ expect(lineString).toHaveBeenCalledWith([[0, 0], [1, 1]])
121
+ })
122
+
123
+ test('delegates other geometry types to the original', () => {
124
+ const feature = { geometry: { type: 'Polygon', coordinates: [[0, 0]] } }
125
+ expect(call({}, feature)).toBe('orig-getLines')
126
+ expect(MapboxSnap.__orig.getLines).toHaveBeenCalledWith(feature, 'mouse', 5)
127
+ })
128
+
129
+ test('returns [] when turf throws on invalid geometry', () => {
130
+ polygon.mockImplementationOnce(() => { throw new Error('bad') })
131
+ const feature = { geometry: { type: 'MultiPolygon', coordinates: [[[[0, 0]]]] } }
132
+ expect(call({}, feature)).toEqual([])
133
+ })
134
+ })
135
+
136
+ describe('getCloseFeatures', () => {
137
+ test('returns [] when disabled', () => {
138
+ expect(MapboxSnap.prototype.getCloseFeatures.call({ status: false }, {}, 1)).toEqual([])
139
+ })
140
+
141
+ test('expands the query to a radius bbox and restores the point', () => {
142
+ const map = { getLayer: jest.fn((l) => l === 'layerA') }
143
+ const self = { status: true, map, _activeLayers: ['layerA', 'missing'], options: { radius: 10 } }
144
+ const e = { point: { x: 100, y: 200 } }
145
+
146
+ MapboxSnap.__orig.getCloseFeatures.mockImplementationOnce(function (ev) {
147
+ expect(ev.point).toEqual([[90, 190], [110, 210]])
148
+ return 'orig-getCloseFeatures'
149
+ })
150
+
151
+ const result = MapboxSnap.prototype.getCloseFeatures.call(self, e, 3)
152
+
153
+ expect(self.options.layers).toEqual(['layerA'])
154
+ expect(e.point).toEqual({ x: 100, y: 200 })
155
+ expect(result).toBe('orig-getCloseFeatures')
156
+ })
157
+
158
+ test('falls back to default layers and the configured radius', () => {
159
+ const map = { getLayer: jest.fn(() => true) }
160
+ const self = { status: true, map, _activeLayers: null, _defaultLayers: ['b'], options: {} }
161
+ const e = { point: { x: 0, y: 0 } }
162
+
163
+ MapboxSnap.__orig.getCloseFeatures.mockImplementationOnce(function (ev) {
164
+ expect(ev.point).toEqual([[-12, -12], [12, 12]])
165
+ return []
166
+ })
167
+
168
+ MapboxSnap.prototype.getCloseFeatures.call(self, e, 1)
169
+ expect(self.options.layers).toEqual(['b'])
170
+ })
171
+
172
+ test('handles a completely missing layer configuration', () => {
173
+ const map = { getLayer: jest.fn(() => true) }
174
+ const self = { status: true, map, options: {} }
175
+ const e = { point: { x: 0, y: 0 } }
176
+
177
+ MapboxSnap.prototype.getCloseFeatures.call(self, e, 1)
178
+ expect(self.options.layers).toEqual([])
179
+ })
180
+ })
181
+
182
+ describe('colour methods', () => {
183
+ test('searchInVertex tints the result and passes through falsy results', () => {
184
+ expect(MapboxSnap.prototype.searchInVertex.call({}).color).toBe(COLORS.vertex)
185
+ MapboxSnap.__orig.searchInVertex.mockReturnValueOnce(null)
186
+ expect(MapboxSnap.prototype.searchInVertex.call({})).toBeNull()
187
+ })
188
+
189
+ test('searchInMidPoint tints the result and passes through falsy results', () => {
190
+ expect(MapboxSnap.prototype.searchInMidPoint.call({}).color).toBe(COLORS.midpoint)
191
+ MapboxSnap.__orig.searchInMidPoint.mockReturnValueOnce(null)
192
+ expect(MapboxSnap.prototype.searchInMidPoint.call({})).toBeNull()
193
+ })
194
+
195
+ test('searchInEdge tints the result and passes through falsy results', () => {
196
+ expect(MapboxSnap.prototype.searchInEdge.call({}).color).toBe(COLORS.edge)
197
+ MapboxSnap.__orig.searchInEdge.mockReturnValueOnce(null)
198
+ expect(MapboxSnap.prototype.searchInEdge.call({})).toBeNull()
199
+ })
200
+ })
201
+
202
+ describe('snapToClosestPoint', () => {
203
+ test('skips and returns undefined when disabled', () => {
204
+ expect(MapboxSnap.prototype.snapToClosestPoint.call({ status: false })).toBeUndefined()
205
+ expect(MapboxSnap.__orig.snapToClosestPoint).not.toHaveBeenCalled()
206
+ })
207
+
208
+ test('skips while the map is zooming', () => {
209
+ expect(MapboxSnap.prototype.snapToClosestPoint.call({ status: true, map: { _isZooming: true } }, {})).toBeUndefined()
210
+ })
211
+
212
+ test('delegates and trims oversized caches', () => {
213
+ const self = { status: true, map: {}, closeFeatures: new Array(101), lines: new Array(101) }
214
+ expect(MapboxSnap.prototype.snapToClosestPoint.call(self, {})).toBe('orig-snap')
215
+ expect(self.closeFeatures).toHaveLength(0)
216
+ expect(self.lines).toHaveLength(0)
217
+ })
218
+
219
+ test('leaves small caches intact', () => {
220
+ const self = { status: true, map: {}, closeFeatures: [1], lines: [2] }
221
+ MapboxSnap.prototype.snapToClosestPoint.call(self, {})
222
+ expect(self.closeFeatures).toHaveLength(1)
223
+ expect(self.lines).toHaveLength(1)
224
+ })
225
+
226
+ test('tolerates absent caches and a missing map', () => {
227
+ expect(MapboxSnap.prototype.snapToClosestPoint.call({ status: true }, {})).toBe('orig-snap')
228
+ })
229
+
230
+ test('clears snap state when the original throws', () => {
231
+ MapboxSnap.__orig.snapToClosestPoint.mockImplementationOnce(() => { throw new Error('x') })
232
+ const self = { status: true, map: {}, snapStatus: true, snapCoords: [1, 2] }
233
+
234
+ expect(MapboxSnap.prototype.snapToClosestPoint.call(self, {})).toBeUndefined()
235
+ expect(self.snapStatus).toBe(false)
236
+ expect(self.snapCoords).toBeNull()
237
+ })
238
+ })
@@ -0,0 +1,115 @@
1
+ import MapboxSnap from 'mapbox-gl-snap/dist/esm/MapboxSnap.js'
2
+ import { patchSourceData } from './sourceData.js'
3
+ import { SNAP_HELPER_LAYER } from './constants.js'
4
+
5
+ /** Remove any stale snap helper source/layer before creating a new instance */
6
+ function cleanupOldSnap (map) {
7
+ if (map.getLayer(SNAP_HELPER_LAYER)) {
8
+ map.removeLayer(SNAP_HELPER_LAYER)
9
+ }
10
+ if (map.getSource(SNAP_HELPER_LAYER)) {
11
+ map.removeSource(SNAP_HELPER_LAYER)
12
+ }
13
+ }
14
+
15
+ // Snap indicators (and all snap processing) are only meaningful while actively
16
+ // drawing or editing vertices — outside these modes there is nothing to snap to.
17
+ const SNAP_ACTIVE_MODES = new Set(['draw_polygon', 'draw_line', 'edit_vertex'])
18
+
19
+ /**
20
+ * Externally-controlled status: ignore library writes so status is only changed
21
+ * via setSnapStatus(). The library otherwise sets status=true on mode/selection change.
22
+ *
23
+ * Also gated on the current draw mode: the library's own mousemove listener
24
+ * (added once in its constructor) calls snapToClosestPoint on every mouse move
25
+ * regardless of draw mode, repainting the snap-helper-circle layer whenever
26
+ * status is true. Without this gate, the marker reappears on the very next
27
+ * mouse move after leaving draw/edit mode, since mapbox-gl-draw's public
28
+ * changeMode API is silent by default and rarely gives us a mode-change event
29
+ * to react to.
30
+ */
31
+ function defineControlledStatus (snap, initialStatus, draw) {
32
+ let controlledStatus = initialStatus
33
+
34
+ Object.defineProperty(snap, 'status', {
35
+ get () { // nosonar
36
+ return controlledStatus && SNAP_ACTIVE_MODES.has(draw.getMode())
37
+ },
38
+ set () { // nosonar
39
+ // intentionally empty: library writes are ignored
40
+ },
41
+ configurable: true
42
+ })
43
+
44
+ snap.setSnapStatus = (value) => {
45
+ controlledStatus = value
46
+ }
47
+ }
48
+
49
+ /** Store default snap layers and expose a per-call override setter (null resets) */
50
+ function configureSnapLayers (snap, layers) {
51
+ snap._defaultLayers = layers
52
+ snap._activeLayers = null
53
+
54
+ snap.setSnapLayers = (overrideLayers) => {
55
+ if (overrideLayers === null || overrideLayers === undefined) {
56
+ snap._activeLayers = null // Use defaults
57
+ } else if (Array.isArray(overrideLayers)) {
58
+ snap._activeLayers = overrideLayers // Override defaults
59
+ } else {
60
+ // No action
61
+ }
62
+ }
63
+ }
64
+
65
+ /** Create the snap instance once the draw source is available */
66
+ export function createSnapInstance (map, draw, source, config) {
67
+ // Prevent duplicate creation (race between initial poll and style.load)
68
+ if (map._snapInstance || map._snapCreating) {
69
+ return map._snapInstance
70
+ }
71
+
72
+ map._snapCreating = true
73
+ cleanupOldSnap(map)
74
+ patchSourceData(source)
75
+
76
+ /** @type {any} */
77
+ const snap = new MapboxSnap({
78
+ map,
79
+ drawing: draw,
80
+ options: { layers: config.layers, radius: config.radius, rules: config.rules },
81
+ status: config.status,
82
+ onSnapped: config.onSnapped
83
+ })
84
+
85
+ defineControlledStatus(snap, config.status, draw)
86
+ configureSnapLayers(snap, config.layers)
87
+
88
+ // Apply any pending snap layers that were set before the instance was ready
89
+ if (map._pendingSnapLayers !== undefined) {
90
+ snap.setSnapLayers(map._pendingSnapLayers)
91
+ delete map._pendingSnapLayers
92
+ }
93
+
94
+ map._snapInstance = snap
95
+ return snap
96
+ }
97
+
98
+ /** Ensure the snap helper source and layer exist after a style change */
99
+ export function ensureSnapLayer (map) {
100
+ if (!map.getSource(SNAP_HELPER_LAYER)) {
101
+ map.addSource(SNAP_HELPER_LAYER, {
102
+ type: 'geojson',
103
+ data: { type: 'FeatureCollection', features: [] }
104
+ })
105
+ }
106
+ if (!map.getLayer(SNAP_HELPER_LAYER)) {
107
+ map.addLayer({
108
+ id: SNAP_HELPER_LAYER,
109
+ type: 'fill',
110
+ source: SNAP_HELPER_LAYER,
111
+ paint: { 'fill-color': ['get', 'color'] },
112
+ layout: { visibility: map._snapInstance?.status ? 'visible' : 'none' }
113
+ })
114
+ }
115
+ }
@@ -0,0 +1,151 @@
1
+ import MapboxSnap from 'mapbox-gl-snap/dist/esm/MapboxSnap.js'
2
+ import { patchSourceData } from './sourceData.js'
3
+ import { createSnapInstance, ensureSnapLayer } from './snapInstance.js'
4
+
5
+ const SNAP_LAYER = 'snap-helper-circle'
6
+
7
+ jest.mock('mapbox-gl-snap/dist/esm/MapboxSnap.js', () => ({
8
+ __esModule: true,
9
+ default: jest.fn(function (opts) { Object.assign(this, opts) })
10
+ }))
11
+
12
+ jest.mock('./sourceData.js', () => ({ patchSourceData: jest.fn() }))
13
+
14
+ const config = { layers: ['a'], radius: 12, rules: ['vertex'], status: false, onSnapped: () => {} }
15
+
16
+ const makeMap = (overrides = {}) => ({
17
+ getLayer: jest.fn(() => null),
18
+ removeLayer: jest.fn(),
19
+ getSource: jest.fn(() => null),
20
+ removeSource: jest.fn(),
21
+ addSource: jest.fn(),
22
+ addLayer: jest.fn(),
23
+ ...overrides
24
+ })
25
+
26
+ beforeEach(() => {
27
+ jest.clearAllMocks()
28
+ })
29
+
30
+ describe('createSnapInstance', () => {
31
+ test('constructs the snap instance, patches the source and stores it on the map', () => {
32
+ const map = makeMap()
33
+ const draw = { id: 'draw' }
34
+ const source = { id: 'src' }
35
+
36
+ const snap = createSnapInstance(map, draw, source, config)
37
+
38
+ expect(patchSourceData).toHaveBeenCalledWith(source)
39
+ expect(MapboxSnap).toHaveBeenCalledWith({
40
+ map,
41
+ drawing: draw,
42
+ options: { layers: ['a'], radius: 12, rules: ['vertex'] },
43
+ status: false,
44
+ onSnapped: config.onSnapped
45
+ })
46
+ expect(map._snapInstance).toBe(snap)
47
+ })
48
+
49
+ test('returns the existing instance without recreating', () => {
50
+ const map = makeMap({ _snapInstance: { id: 'existing' } })
51
+ expect(createSnapInstance(map, {}, {}, config)).toEqual({ id: 'existing' })
52
+ expect(MapboxSnap).not.toHaveBeenCalled()
53
+ })
54
+
55
+ test('bails out while another creation is in progress', () => {
56
+ const map = makeMap({ _snapCreating: true })
57
+ createSnapInstance(map, {}, {}, config)
58
+ expect(MapboxSnap).not.toHaveBeenCalled()
59
+ })
60
+
61
+ test('removes a stale snap layer and source before creating', () => {
62
+ const map = makeMap({ getLayer: jest.fn(() => ({})), getSource: jest.fn(() => ({})) })
63
+ createSnapInstance(map, {}, {}, config)
64
+ expect(map.removeLayer).toHaveBeenCalledWith(SNAP_LAYER)
65
+ expect(map.removeSource).toHaveBeenCalledWith(SNAP_LAYER)
66
+ })
67
+
68
+ test('exposes an externally-controlled status via setSnapStatus', () => {
69
+ const map = makeMap()
70
+ const draw = { getMode: jest.fn(() => 'draw_polygon') }
71
+ const snap = createSnapInstance(map, draw, {}, { ...config, status: false })
72
+
73
+ expect(snap.status).toBe(false)
74
+ snap.status = true // library write ignored
75
+ expect(snap.status).toBe(false)
76
+ snap.setSnapStatus(true)
77
+ expect(snap.status).toBe(true)
78
+ })
79
+
80
+ test('status is false outside draw/edit modes even when the toggle is on', () => {
81
+ const map = makeMap()
82
+ const draw = { getMode: jest.fn(() => 'simple_select') }
83
+ const snap = createSnapInstance(map, draw, {}, { ...config, status: false })
84
+
85
+ snap.setSnapStatus(true)
86
+ expect(snap.status).toBe(false)
87
+
88
+ draw.getMode.mockReturnValue('edit_vertex')
89
+ expect(snap.status).toBe(true)
90
+ })
91
+
92
+ test('setSnapLayers overrides, resets and ignores invalid input', () => {
93
+ const map = makeMap()
94
+ const snap = createSnapInstance(map, {}, {}, { ...config, layers: ['a'] })
95
+
96
+ expect(snap._defaultLayers).toEqual(['a'])
97
+ expect(snap._activeLayers).toBeNull()
98
+
99
+ snap.setSnapLayers(['b', 'c'])
100
+ expect(snap._activeLayers).toEqual(['b', 'c'])
101
+
102
+ snap.setSnapLayers(null)
103
+ expect(snap._activeLayers).toBeNull()
104
+
105
+ snap.setSnapLayers(['d'])
106
+ snap.setSnapLayers(undefined)
107
+ expect(snap._activeLayers).toBeNull()
108
+
109
+ snap.setSnapLayers(['x'])
110
+ snap.setSnapLayers('not-an-array') // no action
111
+ expect(snap._activeLayers).toEqual(['x'])
112
+ })
113
+
114
+ test('applies pending snap layers set before the instance was ready', () => {
115
+ const map = makeMap({ _pendingSnapLayers: ['pending'] })
116
+ const snap = createSnapInstance(map, {}, {}, { ...config, layers: ['default'] })
117
+
118
+ expect(snap._activeLayers).toEqual(['pending'])
119
+ expect(map._pendingSnapLayers).toBeUndefined()
120
+ })
121
+ })
122
+
123
+ describe('ensureSnapLayer', () => {
124
+ test('adds the missing source and layer (hidden when snapping is disabled)', () => {
125
+ const map = makeMap()
126
+ ensureSnapLayer(map)
127
+
128
+ expect(map.addSource).toHaveBeenCalledWith(SNAP_LAYER, expect.objectContaining({ type: 'geojson' }))
129
+ expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
130
+ id: SNAP_LAYER,
131
+ layout: { visibility: 'none' }
132
+ }))
133
+ })
134
+
135
+ test('shows the layer when snapping is active', () => {
136
+ const map = makeMap({ _snapInstance: { status: true } })
137
+ ensureSnapLayer(map)
138
+
139
+ expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
140
+ layout: { visibility: 'visible' }
141
+ }))
142
+ })
143
+
144
+ test('does not re-add an existing source or layer', () => {
145
+ const map = makeMap({ getSource: jest.fn(() => ({})), getLayer: jest.fn(() => ({})) })
146
+ ensureSnapLayer(map)
147
+
148
+ expect(map.addSource).not.toHaveBeenCalled()
149
+ expect(map.addLayer).not.toHaveBeenCalled()
150
+ })
151
+ })
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Poll until checkFn returns truthy, then call onSuccess with the result.
3
+ * A `null` result signals to stop polling; any other falsy value keeps polling.
4
+ */
5
+ export function pollUntil (checkFn, onSuccess) {
6
+ (function poll () {
7
+ const result = checkFn()
8
+ if (result === null) {
9
+ return
10
+ }
11
+ result ? onSuccess(result) : requestAnimationFrame(poll)
12
+ })()
13
+ }
14
+
15
+ /**
16
+ * Patch a GeoJSON source to expose _data for MapboxSnap compatibility.
17
+ * MapboxSnap expects source._data.features but MapLibre doesn't expose this.
18
+ */
19
+ export function patchSourceData (source) {
20
+ if (!source || (source._data && Array.isArray(source._data?.features))) {
21
+ return
22
+ }
23
+
24
+ let dataCache = { type: 'FeatureCollection', features: [] }
25
+ Object.defineProperty(source, '_data', {
26
+ get: () => dataCache,
27
+ set: (val) => {
28
+ dataCache = (val && typeof val === 'object' && Array.isArray(val.features))
29
+ ? val
30
+ : { type: 'FeatureCollection', features: [] }
31
+ },
32
+ configurable: true
33
+ })
34
+ }