@defra/interactive-map 0.0.40-alpha → 0.0.41-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 (351) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api/control-definition.md +9 -1
  7. package/docs/api/slots.md +26 -5
  8. package/docs/assets/images/slot-map.svg +61 -262
  9. package/docs/examples/draw-tools.mdx +3 -3
  10. package/docs/plugins/draw.md +79 -16
  11. package/docs/plugins/search.md +1 -1
  12. package/govuk-prototype-kit.config.json +2 -0
  13. package/jest.setup.js +5 -0
  14. package/package.json +9 -1
  15. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  16. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  17. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  18. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  19. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  20. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  21. package/plugins/datasets/dist/css/index.css +1 -1
  22. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  23. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  24. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  25. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  26. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  27. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  28. package/plugins/datasets/dist/umd/index.js +1 -1
  29. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
  30. package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
  31. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
  32. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +14 -0
  33. package/plugins/datasets/src/api/setOpacity.test.js +9 -0
  34. package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
  35. package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
  36. package/plugins/datasets/src/datasets.scss +0 -1
  37. package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
  38. package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
  39. package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
  40. package/plugins/datasets/src/index.test.js +36 -0
  41. package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
  42. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
  43. package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -4
  44. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
  45. package/plugins/datasets/src/manifest.js +0 -38
  46. package/plugins/datasets/src/manifest.test.js +49 -0
  47. package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
  48. package/plugins/datasets/src/reducers/pluginState.js +3 -16
  49. package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
  50. package/plugins/datasets/src/registry/dataset.js +13 -1
  51. package/plugins/datasets/src/registry/dataset.test.js +61 -0
  52. package/plugins/datasets/src/registry/datasetRegistry.js +12 -6
  53. package/plugins/datasets/src/registry/datasetRegistry.test.js +16 -4
  54. package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
  55. package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
  56. package/plugins/datasets/src/utils/bbox.test.js +125 -0
  57. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  58. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  59. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  60. package/plugins/draw/dist/esm/index.js +1 -1
  61. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
  62. package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
  63. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  64. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  65. package/plugins/draw/dist/umd/index.js +1 -1
  66. package/plugins/draw/src/DrawInit.jsx +10 -2
  67. package/plugins/draw/src/DrawInit.test.jsx +51 -10
  68. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +91 -11
  69. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
  70. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
  71. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
  72. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
  74. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
  75. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
  90. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +4 -27
  91. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
  92. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
  93. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
  94. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
  95. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
  96. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
  97. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +3 -53
  98. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
  99. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
  100. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +10 -3
  101. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
  102. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
  103. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
  104. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
  105. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
  106. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
  107. package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
  108. package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
  109. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
  110. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
  111. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
  112. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
  113. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
  114. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
  115. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
  116. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
  117. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
  118. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
  119. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
  120. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
  121. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
  122. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
  123. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
  124. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
  125. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
  126. package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
  127. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
  128. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +12 -0
  129. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +8 -2
  130. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +31 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
  132. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +33 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
  134. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +10 -3
  136. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +24 -1
  137. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
  138. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
  139. package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
  140. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
  141. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
  142. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
  143. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
  144. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
  145. package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
  146. package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
  147. package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
  148. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
  149. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
  150. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
  151. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
  153. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
  154. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
  155. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
  156. package/plugins/draw/src/api/addFeature.js +16 -1
  157. package/plugins/draw/src/api/addFeature.test.js +62 -5
  158. package/plugins/draw/src/api/editFeature.js +7 -4
  159. package/plugins/draw/src/api/editFeature.test.js +16 -0
  160. package/plugins/draw/src/api/newPoint.js +42 -0
  161. package/plugins/draw/src/api/newPoint.test.js +117 -0
  162. package/plugins/draw/src/api/setStyle.js +43 -0
  163. package/plugins/draw/src/api/setStyle.test.js +75 -0
  164. package/plugins/draw/src/events.js +17 -10
  165. package/plugins/draw/src/events.test.js +14 -0
  166. package/plugins/draw/src/manifest.js +18 -6
  167. package/plugins/draw/src/manifest.test.js +32 -1
  168. package/plugins/draw/src/reducer.test.js +7 -0
  169. package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
  170. package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
  171. package/plugins/draw/src/utils/symbolKeys.js +5 -0
  172. package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
  173. package/plugins/draw/src/validation/liveStroke.test.js +64 -1
  174. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  175. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  176. package/plugins/interact/dist/umd/index.js +1 -1
  177. package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
  178. package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
  179. package/plugins/interact/src/utils/featureQueries.js +18 -0
  180. package/plugins/interact/src/utils/featureQueries.test.js +15 -0
  181. package/plugins/interact/src/utils/spatial.js +9 -3
  182. package/plugins/interact/src/utils/spatial.test.js +12 -1
  183. package/plugins/map-key/dist/css/index.css +62 -0
  184. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
  185. package/plugins/map-key/dist/esm/index.js +1 -0
  186. package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
  187. package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
  188. package/plugins/map-key/dist/umd/index.js +2 -0
  189. package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
  190. package/plugins/map-key/package.json +28 -0
  191. package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
  192. package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
  193. package/plugins/map-key/src/components/Key/Key.jsx +41 -0
  194. package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +15 -14
  195. package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
  196. package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
  197. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
  198. package/plugins/map-key/src/components/Key/KeyItem.jsx +29 -0
  199. package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
  200. package/plugins/map-key/src/components/Key/KeySvg.jsx +42 -0
  201. package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
  202. package/plugins/map-key/src/components/Key/KeySvgLine.jsx +28 -0
  203. package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
  204. package/plugins/map-key/src/components/Key/KeySvgPattern.jsx +23 -0
  205. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +2 -2
  206. package/plugins/map-key/src/components/Key/KeySvgRect.jsx +33 -0
  207. package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
  208. package/plugins/map-key/src/components/Key/KeySvgSymbol.jsx +27 -0
  209. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +5 -6
  210. package/plugins/map-key/src/components/Key/MapKey.jsx +50 -0
  211. package/plugins/map-key/src/components/Key/MapKey.test.jsx +130 -0
  212. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
  213. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
  214. package/plugins/map-key/src/index.js +13 -0
  215. package/plugins/map-key/src/index.test.js +31 -0
  216. package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
  217. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
  218. package/plugins/map-key/src/manifest.js +30 -0
  219. package/plugins/map-key/src/manifest.test.js +63 -0
  220. package/plugins/map-key/src/mapKey.scss +4 -0
  221. package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
  222. package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
  223. package/plugins/map-key/src/registry/index.js +3 -0
  224. package/plugins/map-key/src/registry/index.test.js +15 -0
  225. package/plugins/menu/dist/css/index.css +80 -0
  226. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
  227. package/plugins/menu/dist/esm/index.js +1 -0
  228. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
  229. package/plugins/menu/dist/umd/index.js +2 -0
  230. package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
  231. package/plugins/menu/package.json +28 -0
  232. package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
  233. package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
  234. package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
  235. package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
  236. package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
  237. package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
  238. package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
  239. package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
  240. package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
  241. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
  242. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
  243. package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
  244. package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
  245. package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
  246. package/plugins/menu/src/index.js +13 -0
  247. package/plugins/menu/src/index.test.js +31 -0
  248. package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
  249. package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
  250. package/plugins/menu/src/initialise/createMenu.js +10 -0
  251. package/plugins/menu/src/initialise/createMenu.test.js +37 -0
  252. package/plugins/menu/src/manifest.js +35 -0
  253. package/plugins/menu/src/manifest.test.js +63 -0
  254. package/plugins/menu/src/menu.scss +4 -0
  255. package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
  256. package/plugins/menu/src/reducers/pluginState.js +35 -0
  257. package/plugins/menu/src/reducers/pluginState.test.js +57 -0
  258. package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
  259. package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
  260. package/plugins/menu/src/registry/index.js +3 -0
  261. package/plugins/menu/src/registry/index.test.js +15 -0
  262. package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
  263. package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
  264. package/plugins/search/dist/css/index.css +1 -1
  265. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  266. package/plugins/search/dist/esm/index.js +1 -1
  267. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  268. package/plugins/search/dist/umd/index.js +1 -1
  269. package/plugins/search/src/Search.jsx +2 -3
  270. package/plugins/search/src/components/Form/Form.module.scss +9 -20
  271. package/plugins/search/src/index.js +0 -1
  272. package/plugins/search/src/index.test.js +2 -4
  273. package/plugins/search/src/manifest.js +1 -1
  274. package/plugins/search/src/search.scss +17 -16
  275. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  276. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  277. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  278. package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
  279. package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
  280. package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
  281. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
  282. package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
  283. package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
  284. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  285. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  286. package/providers/maplibre/dist/umd/index.js +1 -1
  287. package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
  288. package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
  289. package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
  290. package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
  291. package/providers/maplibre/src/utils/hoverCursor.js +30 -14
  292. package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
  293. package/providers/maplibre/src/utils/patternImages.js +1 -1
  294. package/providers/maplibre/src/utils/symbolImages.js +41 -3
  295. package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
  296. package/rollup.esm.mjs +11 -1
  297. package/src/App/components/Actions/Actions.test.jsx +47 -0
  298. package/src/App/components/Hints/Hints.test.jsx +13 -0
  299. package/src/App/components/MoveControls/MoveControls.module.scss +41 -10
  300. package/src/App/components/Panel/Panel.jsx +18 -3
  301. package/src/App/components/Panel/Panel.module.scss +18 -8
  302. package/src/App/components/Panel/Panel.test.jsx +34 -0
  303. package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
  304. package/src/App/hooks/useFeatureItems.js +16 -7
  305. package/src/App/hooks/useFeatureItems.test.js +57 -0
  306. package/src/App/hooks/useLayoutMeasurements.js +95 -84
  307. package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
  308. package/src/App/layout/Layout.jsx +9 -10
  309. package/src/App/layout/Layout.test.jsx +2 -0
  310. package/src/App/layout/layout.module.scss +31 -17
  311. package/src/App/registry/pluginRegistry.js +1 -1
  312. package/src/App/registry/pluginRegistry.test.js +12 -0
  313. package/src/App/renderer/HtmlElementHost.jsx +18 -5
  314. package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
  315. package/src/App/renderer/mapButtons.js +28 -12
  316. package/src/App/renderer/mapButtons.test.js +17 -4
  317. package/src/App/renderer/mapControls.js +3 -1
  318. package/src/App/renderer/mapControls.test.js +16 -0
  319. package/src/App/renderer/mapPanels.js +44 -9
  320. package/src/App/renderer/mapPanels.test.js +51 -2
  321. package/src/App/renderer/orderItems.js +28 -0
  322. package/src/App/renderer/orderItems.test.js +50 -0
  323. package/src/App/renderer/slotAggregator.js +2 -18
  324. package/src/App/renderer/slotHelpers.js +3 -1
  325. package/src/App/renderer/slotHelpers.test.js +5 -0
  326. package/src/App/renderer/slots.js +3 -0
  327. package/src/App/store/AppProvider.jsx +1 -0
  328. package/src/App/store/appDispatchMiddleware.js +1 -1
  329. package/src/App/store/appDispatchMiddleware.test.js +8 -0
  330. package/src/config/events.js +2 -0
  331. package/src/scss/settings/_colors.scss +1 -0
  332. package/src/scss/settings/_dimensions.scss +2 -3
  333. package/src/scss/settings/_transition.scss +1 -1
  334. package/src/services/eventBus.js +44 -0
  335. package/src/services/eventBus.test.js +83 -0
  336. package/src/services/symbolRegistry.js +1 -1
  337. package/src/types.js +11 -1
  338. package/webpack.umd.mjs +2 -0
  339. package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
  340. package/plugins/datasets/src/components/Key/Key.jsx +0 -50
  341. package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
  342. package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
  343. package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
  344. package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
  345. package/plugins/datasets/src/components/Key/KeySvgPattern.jsx +0 -13
  346. package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
  347. package/plugins/datasets/src/components/Key/KeySvgSymbol.jsx +0 -16
  348. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
  349. /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
  350. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
  351. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
@@ -16,8 +16,9 @@ describe('onSetup / onStop lifecycle', () => {
16
16
  expect(state.midpoints).toHaveLength(4)
17
17
  expect(map._lastEditFeatureId).toBe('feat-1')
18
18
  expect(map._drawEditContainer).toBe(state.container)
19
+ // draw.update is edit_vertex's own extra listener on top of the shared set (covered by
20
+ // editModeEvents.test.js).
19
21
  expect(ctx.map.on).toHaveBeenCalledWith('draw.update', expect.any(Function))
20
- expect(ctx.map.on).toHaveBeenCalledWith('draw.nudgevertex', expect.any(Function))
21
22
  })
22
23
 
23
24
  test('does not clear the undo stack when re-entering the same feature', () => {
@@ -28,13 +29,14 @@ describe('onSetup / onStop lifecycle', () => {
28
29
  expect(map._undoStack.length).toBe(1)
29
30
  })
30
31
 
31
- test('onStop removes listeners and clears editing container; DirectSelect touch stubs are inert', () => {
32
+ // Shared listener teardown is covered by editModeEvents.test.js; draw.update is edit_vertex's
33
+ // own extra listener.
34
+ test('onStop removes the draw.update listener and clears editing container; DirectSelect touch stubs are inert', () => {
32
35
  const { ctx, state, map } = createHarness()
33
36
  expect(() => { ctx.onTouchStart(); ctx.onTouchMove(); ctx.onTouchEnd() }).not.toThrow()
34
37
  ctx.onStop(state)
35
38
  expect(map._drawEditContainer).toBeNull()
36
39
  expect(map.off).toHaveBeenCalledWith('draw.update', expect.any(Function))
37
- expect(map.off).toHaveBeenCalledWith('draw.nudgevertex', expect.any(Function))
38
40
  })
39
41
 
40
42
  test('onSetup edge cases: clears an active snap indicator, positions or skips the touch target, clears an explicit -1 selection', () => {
@@ -110,6 +112,11 @@ describe('selection, scale and update events', () => {
110
112
  expect({ top: state.touchVertexTarget.style.top, left: state.touchVertexTarget.style.left }).not.toEqual(targetBefore)
111
113
  })
112
114
 
115
+ test('draw.nudgevertex with no vertex selected moves nothing and skips repositioning the touch target', () => {
116
+ const { map } = createHarness(POLYGON(), { interfaceType: 'touch', selectedVertexIndex: -1 })
117
+ expect(() => map.fire('draw.nudgevertex', { dx: 1, dy: 0, isLargeStep: false })).not.toThrow()
118
+ })
119
+
113
120
  test('onUpdate re-selects a changed vertex only when the vertex count is ambiguous', () => {
114
121
  const { ctx, state } = createHarness()
115
122
  ctx.onUpdate(state) // unique coords → no change
@@ -0,0 +1,158 @@
1
+ import { symbolRegistry } from '../../../../../src/services/symbolRegistry.js'
2
+ import { getSymbolAnchor, getSymbolViewBox } from '../../../../../src/utils/symbolUtils.js'
3
+ import { anchorToMaplibre, anchorToMaplibreOffset } from '../../../../../providers/maplibre/src/utils/symbolImages.js'
4
+
5
+ /**
6
+ * Resolves and registers a drawn point's symbol-config icon (same schema as addMarker/dataset
7
+ * points — see src/config/symbolConfig.js), writing the resolved image id/anchor back onto
8
+ * the feature so styles.js's pointSymbol() layer can render it. icon-offset is handled
9
+ * separately below since it can't safely be a per-feature property.
10
+ */
11
+
12
+ export const hasSymbolStyle = (properties) => !!(properties?.symbol || properties?.symbolSvgContent)
13
+
14
+ const POINT_SYMBOL_LAYER_ID = 'point-symbol'
15
+
16
+ // icon-offset can't be a raw per-feature `get` on an array property — MapLibre's GeoJSON
17
+ // sources silently JSON.stringify arrays, so it reads back a string at render time. Instead
18
+ // each symbolImageId's offset is folded into a `match` expression keyed on that (safe, string) property.
19
+ const buildIconOffsetExpression = (offsetsByImageId) => {
20
+ const expression = ['match', ['get', 'user_symbolImageId']]
21
+ for (const [imageId, offset] of Object.entries(offsetsByImageId)) {
22
+ expression.push(imageId, ['literal', offset])
23
+ }
24
+ expression.push(['literal', [0, 0]]) // fallback for any id not yet registered
25
+ return expression
26
+ }
27
+
28
+ const registerSymbolIconOffset = (map, symbolImageId, offset) => {
29
+ map._symbolIconOffsetMap ??= {}
30
+ if (map._symbolIconOffsetMap[symbolImageId]) {
31
+ return // offset is deterministic per id — already registered, nothing changed
32
+ }
33
+ map._symbolIconOffsetMap[symbolImageId] = offset
34
+ const expression = buildIconOffsetExpression(map._symbolIconOffsetMap)
35
+ ;['hot', 'cold'].forEach((suffix) => {
36
+ const layerId = `${POINT_SYMBOL_LAYER_ID}.${suffix}`
37
+ if (map.getLayer(layerId)) {
38
+ map.setLayoutProperty(layerId, 'icon-offset', expression)
39
+ }
40
+ })
41
+ }
42
+
43
+ // map.getPixelRatio() is only set once at map construction, so it won't reflect a later
44
+ // map-size change on its own. Callers reacting to MAP_SET_PIXEL_RATIO should pass the fresh
45
+ // value through as pixelRatioOverride instead.
46
+ export const getPixelRatio = (map) => map.getPixelRatio?.() || 1
47
+
48
+ // Does the async work for one point without touching the store — draw.add() is left to the
49
+ // caller so refreshAllPointSymbols can batch every point into one call (see its comment why).
50
+ // Returns null if there's nothing to write back (no symbol config, feature gone, or unresolvable id).
51
+ const resolvePointSymbolFeature = async ({ draw, mapProvider, map, featureId, properties, pixelRatioOverride }) => {
52
+ if (!hasSymbolStyle(properties)) {
53
+ return null
54
+ }
55
+
56
+ const mapStyle = map._drawCurrentMapStyle
57
+ const pixelRatio = pixelRatioOverride ?? getPixelRatio(map)
58
+
59
+ await mapProvider.addSymbolsToMap([properties], mapStyle, symbolRegistry)
60
+
61
+ const feature = draw.get(featureId)
62
+ if (!feature) {
63
+ return null
64
+ }
65
+
66
+ const symbolImageId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, pixelRatio)
67
+ if (!symbolImageId) {
68
+ return null
69
+ }
70
+ // addSymbolsToMap() (just awaited above) already mapped this id's active/selected variants
71
+ // into map._activeSymbolImageMap/_selectedSymbolImageMap — read them back so the highlight
72
+ // ring (highlightFeatures.js) can reference each point's own precomputed variant directly.
73
+ const symbolActiveImageId = map._activeSymbolImageMap?.[symbolImageId] ?? null
74
+ const symbolSelectedImageId = map._selectedSymbolImageMap?.[symbolImageId] ?? null
75
+ const symbolDef = symbolRegistry.getSymbolDef(properties)
76
+ const rawAnchor = getSymbolAnchor(properties, symbolDef)
77
+ const anchor = anchorToMaplibre(rawAnchor)
78
+ // icon-anchor only has 9 discrete positions, so an off-grid anchor (e.g. pin's [0.5, 0.9])
79
+ // loses precision snapping to the nearest one — icon-offset corrects that gap.
80
+ const offset = anchorToMaplibreOffset(rawAnchor, getSymbolViewBox(properties, symbolDef))
81
+ registerSymbolIconOffset(map, symbolImageId, offset)
82
+
83
+ return {
84
+ ...feature,
85
+ properties: {
86
+ ...properties,
87
+ symbolImageId,
88
+ symbolIconAnchor: anchor,
89
+ symbolActiveImageId,
90
+ symbolSelectedImageId
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Resolve one point feature's symbol image and write it back onto the feature.
97
+ * A no-op for points with no symbol config (nothing to render as an icon).
98
+ *
99
+ * @param {Object} params
100
+ * @param {Object} params.draw - the raw MapboxDraw instance (needs get/add)
101
+ * @param {Object} params.mapProvider - MapLibreProvider (needs addSymbolsToMap)
102
+ * @param {Object} params.map - MapLibre map instance (needs _drawCurrentMapStyle, getPixelRatio)
103
+ * @param {string} params.featureId
104
+ * @param {Object} params.properties - the point feature's current properties
105
+ * @param {number} [params.pixelRatioOverride] - use this instead of map.getPixelRatio(), for
106
+ * callers (map-size refresh) that already know the freshly computed value
107
+ * @returns {Promise<void>}
108
+ */
109
+ export const resolvePointSymbol = async (params) => {
110
+ try {
111
+ const feature = await resolvePointSymbolFeature(params)
112
+ if (!feature) {
113
+ return
114
+ }
115
+ // draw.setFeatureProperty() only marks the feature dirty for mapbox-gl-draw's own mode-
116
+ // dispatch loop, which won't run again until the next interaction. draw.add() on an
117
+ // existing id updates its properties and renders unconditionally, so it's used instead.
118
+ params.draw.add(feature)
119
+ } catch (err) {
120
+ // A silent failure here means a point simply never gets/keeps an icon, with nothing in
121
+ // the UI to explain why — surface it instead of letting the rejection vanish.
122
+ console.error('[draw] failed to resolve point symbol', params.featureId, err) // NOSONAR
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Re-resolve every existing drawn point's symbol — called on map style/pixel-ratio change,
128
+ * since the rasterised image (colours, and pixel-ratio-scaled dimensions) depends on both.
129
+ *
130
+ * @param {Object} params
131
+ * @param {Object} params.draw - the raw MapboxDraw instance (needs getAll)
132
+ * @param {Object} params.mapProvider
133
+ * @param {Object} params.map
134
+ * @param {number} [params.pixelRatioOverride] - see resolvePointSymbol
135
+ * @returns {Promise<void>}
136
+ */
137
+ export const refreshAllPointSymbols = async ({ draw, mapProvider, map, pixelRatioOverride }) => {
138
+ const points = draw.getAll().features.filter(
139
+ (f) => f.geometry.type === 'Point' && hasSymbolStyle(f.properties)
140
+ )
141
+ const results = await Promise.allSettled(points.map((f) =>
142
+ resolvePointSymbolFeature({ draw, mapProvider, map, featureId: f.id, properties: f.properties, pixelRatioOverride })
143
+ ))
144
+
145
+ results.filter((r) => r.status === 'rejected').forEach((r) => {
146
+ console.error('[draw] failed to resolve point symbol', r.reason) // NOSONAR
147
+ })
148
+
149
+ // One combined draw.add() call triggers a single render. Adding one point at a time here
150
+ // would let mapbox-gl-draw's debounced render fire mid-batch, painting a not-yet-resolved
151
+ // point with its stale, now-unregistered image id ("Image X could not be loaded").
152
+ const features = results
153
+ .filter((r) => r.status === 'fulfilled' && r.value)
154
+ .map((r) => r.value)
155
+ if (features.length) {
156
+ draw.add({ type: 'FeatureCollection', features })
157
+ }
158
+ }
@@ -0,0 +1,314 @@
1
+ import { hasSymbolStyle, getPixelRatio, resolvePointSymbol, refreshAllPointSymbols } from './pointSymbolImages.js'
2
+ import { symbolRegistry } from '../../../../../src/services/symbolRegistry.js'
3
+
4
+ const mapStyle = { id: 'outdoor', mapColorScheme: 'light' }
5
+
6
+ const createMap = ({ pixelRatio = 2 } = {}) => ({
7
+ _drawCurrentMapStyle: mapStyle,
8
+ getPixelRatio: () => pixelRatio,
9
+ // point-symbol.hot/.cold — registerSymbolIconOffset() looks these up to reapply the
10
+ // icon-offset match expression whenever a new symbolImageId's offset is registered.
11
+ getLayer: jest.fn(() => true),
12
+ setLayoutProperty: jest.fn()
13
+ })
14
+
15
+ const createMapProvider = () => ({
16
+ addSymbolsToMap: jest.fn(() => Promise.resolve())
17
+ })
18
+
19
+ const createDraw = (features = []) => ({
20
+ get: jest.fn((id) => features.find((f) => f.id === id) ?? null),
21
+ add: jest.fn(),
22
+ getAll: jest.fn(() => ({ features }))
23
+ })
24
+
25
+ const point = (id, properties, coordinates = [1, 2]) =>
26
+ ({ id, type: 'Feature', geometry: { type: 'Point', coordinates }, properties })
27
+
28
+ describe('hasSymbolStyle', () => {
29
+ it('is true when symbol is set', () => {
30
+ expect(hasSymbolStyle({ symbol: 'pin' })).toBe(true)
31
+ })
32
+
33
+ it('is true when symbolSvgContent is set', () => {
34
+ expect(hasSymbolStyle({ symbolSvgContent: '<path/>' })).toBe(true)
35
+ })
36
+
37
+ it('is false with neither, or no properties at all', () => {
38
+ expect(hasSymbolStyle({})).toBe(false)
39
+ expect(hasSymbolStyle(undefined)).toBe(false)
40
+ })
41
+ })
42
+
43
+ describe('getPixelRatio', () => {
44
+ it('reads map.getPixelRatio()', () => {
45
+ expect(getPixelRatio(createMap({ pixelRatio: 3 }))).toBe(3)
46
+ })
47
+
48
+ it('falls back to 1 when getPixelRatio is unavailable or falsy', () => {
49
+ expect(getPixelRatio({})).toBe(1)
50
+ expect(getPixelRatio({ getPixelRatio: () => 0 })).toBe(1)
51
+ })
52
+ })
53
+
54
+ describe('resolvePointSymbol', () => {
55
+ it('does nothing for a feature with no symbol config', async () => {
56
+ const map = createMap()
57
+ const mapProvider = createMapProvider()
58
+ const draw = createDraw()
59
+ await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties: {} })
60
+ expect(mapProvider.addSymbolsToMap).not.toHaveBeenCalled()
61
+ expect(draw.add).not.toHaveBeenCalled()
62
+ })
63
+
64
+ it('registers the symbol image and re-adds the feature with symbolImageId/symbolIconAnchor, forcing a render', async () => {
65
+ const map = createMap()
66
+ const mapProvider = createMapProvider()
67
+ const properties = { symbol: 'pin', label: 'a point' }
68
+ const draw = createDraw([point('p1', properties)])
69
+
70
+ await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
71
+
72
+ expect(mapProvider.addSymbolsToMap).toHaveBeenCalledWith([properties], mapStyle, symbolRegistry)
73
+ const expectedImageId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, 2)
74
+ expect(draw.add).toHaveBeenCalledWith({
75
+ id: 'p1',
76
+ type: 'Feature',
77
+ geometry: { type: 'Point', coordinates: [1, 2] },
78
+ properties: {
79
+ ...properties,
80
+ symbolImageId: expectedImageId,
81
+ symbolIconAnchor: 'bottom', // pin's default anchor [0.5, 0.9] snaps to icon-anchor 'bottom'
82
+ // Nothing registered in map._activeSymbolImageMap/_selectedSymbolImageMap here since
83
+ // addSymbolsToMap is mocked out (a no-op) in this test — see the dedicated test below
84
+ // for the case where it actually populates them.
85
+ symbolActiveImageId: null,
86
+ symbolSelectedImageId: null
87
+ }
88
+ })
89
+ // icon-offset corrects the precision lost snapping 0.9 to 1.0 against pin's 44px viewBox —
90
+ // registered into the point-symbol layers' icon-offset match expression instead of onto
91
+ // the feature (see registerSymbolIconOffset's comment for why).
92
+ expect(map._symbolIconOffsetMap[expectedImageId]).toEqual([0, 4.4])
93
+ expect(map.setLayoutProperty).toHaveBeenCalledWith('point-symbol.hot', 'icon-offset', [
94
+ 'match', ['get', 'user_symbolImageId'], expectedImageId, ['literal', [0, 4.4]], ['literal', [0, 0]]
95
+ ])
96
+ expect(map.setLayoutProperty).toHaveBeenCalledWith('point-symbol.cold', 'icon-offset', [
97
+ 'match', ['get', 'user_symbolImageId'], expectedImageId, ['literal', [0, 4.4]], ['literal', [0, 0]]
98
+ ])
99
+ })
100
+
101
+ it('only applies the icon-offset expression to point-symbol layers that actually exist on the map', async () => {
102
+ const map = createMap()
103
+ map.getLayer.mockImplementation((layerId) => layerId === 'point-symbol.cold') // hot copy absent
104
+ const mapProvider = createMapProvider()
105
+ const properties = { symbol: 'pin' }
106
+ const draw = createDraw([point('p1', properties)])
107
+
108
+ await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
109
+
110
+ expect(map.setLayoutProperty).toHaveBeenCalledWith('point-symbol.cold', 'icon-offset', expect.anything())
111
+ expect(map.setLayoutProperty).not.toHaveBeenCalledWith('point-symbol.hot', 'icon-offset', expect.anything())
112
+ })
113
+
114
+ it('does not re-register or re-apply icon-offset for a symbolImageId already known (offset is deterministic per id)', async () => {
115
+ const map = createMap()
116
+ const mapProvider = createMapProvider()
117
+ const properties = { symbol: 'pin' }
118
+ const draw = createDraw([point('p1', properties), point('p2', properties)])
119
+
120
+ await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
121
+ map.setLayoutProperty.mockClear()
122
+
123
+ await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p2', properties })
124
+
125
+ expect(map.setLayoutProperty).not.toHaveBeenCalled()
126
+ })
127
+
128
+ it('reads the active/selected variant ids back from map._activeSymbolImageMap/_selectedSymbolImageMap once addSymbolsToMap has registered them', async () => {
129
+ const map = createMap()
130
+ const properties = { symbol: 'pin' }
131
+ const draw = createDraw([point('p1', properties)])
132
+ const expectedImageId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, 2)
133
+ // Simulates what the real addSymbolsToMap (providers/maplibre/src/utils/symbolImages.js)
134
+ // does as a side effect: populate these maps, keyed by the normal variant's own id.
135
+ const mapProvider = {
136
+ addSymbolsToMap: jest.fn(() => {
137
+ map._activeSymbolImageMap = { [expectedImageId]: 'symbol-act-xyz' }
138
+ map._selectedSymbolImageMap = { [expectedImageId]: 'symbol-sel-xyz' }
139
+ return Promise.resolve()
140
+ })
141
+ }
142
+
143
+ await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
144
+
145
+ expect(draw.add).toHaveBeenCalledWith(expect.objectContaining({
146
+ properties: expect.objectContaining({
147
+ symbolActiveImageId: 'symbol-act-xyz',
148
+ symbolSelectedImageId: 'symbol-sel-xyz'
149
+ })
150
+ }))
151
+ })
152
+
153
+ it('does nothing when getSymbolImageId resolves null (e.g. an unresolvable symbol id)', async () => {
154
+ const map = createMap()
155
+ const mapProvider = createMapProvider()
156
+ const properties = { symbol: 'not-a-real-symbol' }
157
+ const draw = createDraw([point('p1', properties)])
158
+ const spy = jest.spyOn(symbolRegistry, 'getSymbolImageId').mockReturnValueOnce(null)
159
+
160
+ await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
161
+
162
+ expect(draw.add).not.toHaveBeenCalled()
163
+ spy.mockRestore()
164
+ })
165
+
166
+ it('does not re-add the feature if it was deleted while registration was in flight', async () => {
167
+ const map = createMap()
168
+ const properties = { symbol: 'pin' }
169
+ let resolveRegistration
170
+ const mapProvider = { addSymbolsToMap: jest.fn(() => new Promise((resolve) => { resolveRegistration = resolve })) }
171
+ const draw = createDraw() // empty — feature already gone
172
+
173
+ const pending = resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
174
+ resolveRegistration()
175
+ await pending
176
+
177
+ expect(draw.add).not.toHaveBeenCalled()
178
+ })
179
+
180
+ it('uses a custom symbolAnchor when the style provides one, with no offset needed for an on-grid anchor', async () => {
181
+ const map = createMap()
182
+ const mapProvider = createMapProvider()
183
+ const properties = { symbol: 'circle', symbolAnchor: [0, 0] }
184
+ const draw = createDraw([point('p1', properties)])
185
+
186
+ await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
187
+
188
+ expect(draw.add).toHaveBeenCalledWith(expect.objectContaining({
189
+ properties: expect.objectContaining({ symbolIconAnchor: 'top-left' })
190
+ }))
191
+ const expectedImageId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, 2)
192
+ expect(map._symbolIconOffsetMap[expectedImageId]).toEqual([0, 0])
193
+ })
194
+
195
+ it('computes a non-zero icon-offset for an off-grid custom symbolAnchor', async () => {
196
+ const map = createMap()
197
+ const mapProvider = createMapProvider()
198
+ const properties = { symbol: 'circle', symbolAnchor: [0.5, 0.8] }
199
+ const draw = createDraw([point('p1', properties)])
200
+
201
+ await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
202
+
203
+ // circle's viewBox is 44×44 — anchor 0.8 snaps to icon-anchor 'bottom' (1.0), offset corrects the gap
204
+ expect(draw.add).toHaveBeenCalledWith(expect.objectContaining({
205
+ properties: expect.objectContaining({ symbolIconAnchor: 'bottom' })
206
+ }))
207
+ const expectedImageId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, 2)
208
+ expect(map._symbolIconOffsetMap[expectedImageId]).toEqual([0, 8.8])
209
+ })
210
+
211
+ it('respects a custom symbolViewBox when computing the offset', async () => {
212
+ const map = createMap()
213
+ const mapProvider = createMapProvider()
214
+ const properties = { symbol: 'pin', symbolViewBox: '0 0 100 100' }
215
+ const draw = createDraw([point('p1', properties)])
216
+
217
+ await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
218
+
219
+ // pin's anchor [0.5, 0.9] against a 100×100 viewBox instead of the built-in 44×44
220
+ const expectedImageId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, 2)
221
+ expect(map._symbolIconOffsetMap[expectedImageId]).toEqual([0, 10])
222
+ })
223
+
224
+ it('surfaces (rather than swallows) a registration failure', async () => {
225
+ const map = createMap()
226
+ const properties = { symbol: 'pin' }
227
+ const mapProvider = { addSymbolsToMap: jest.fn(() => Promise.reject(new Error('rasterise failed'))) }
228
+ const draw = createDraw([point('p1', properties)])
229
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
230
+
231
+ await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
232
+
233
+ expect(draw.add).not.toHaveBeenCalled()
234
+ expect(consoleError).toHaveBeenCalledWith('[draw] failed to resolve point symbol', 'p1', expect.any(Error))
235
+ consoleError.mockRestore()
236
+ })
237
+ })
238
+
239
+ describe('refreshAllPointSymbols', () => {
240
+ it('re-resolves every point feature with a symbol config, ignoring non-point and unstyled features', async () => {
241
+ const map = createMap()
242
+ const mapProvider = createMapProvider()
243
+ const pointWithSymbol = { id: 'p1', geometry: { type: 'Point' }, properties: { symbol: 'pin' } }
244
+ const pointWithoutSymbol = { id: 'p2', geometry: { type: 'Point' }, properties: {} }
245
+ const polygon = { id: 'poly1', geometry: { type: 'Polygon' }, properties: { symbol: 'pin' } }
246
+ const draw = createDraw([pointWithSymbol, pointWithoutSymbol, polygon])
247
+
248
+ await refreshAllPointSymbols({ draw, mapProvider, map })
249
+
250
+ expect(mapProvider.addSymbolsToMap).toHaveBeenCalledTimes(1)
251
+ expect(mapProvider.addSymbolsToMap).toHaveBeenCalledWith([pointWithSymbol.properties], mapStyle, symbolRegistry)
252
+ })
253
+
254
+ it('does nothing when there are no drawn points', async () => {
255
+ const map = createMap()
256
+ const mapProvider = createMapProvider()
257
+ const draw = createDraw([])
258
+ await refreshAllPointSymbols({ draw, mapProvider, map })
259
+ expect(mapProvider.addSymbolsToMap).not.toHaveBeenCalled()
260
+ })
261
+
262
+ // A style change re-resolves every point concurrently; each one's addSymbolsToMap()
263
+ // settles on its own schedule. A single combined draw.add() (rather than one call per
264
+ // point) is what keeps mapbox-gl-draw's debounced render from ever painting one point's
265
+ // fresh data alongside another's still-stale, now-unregistered image id — see
266
+ // refreshAllPointSymbols's own comment.
267
+ it('batches every resolved point into a single draw.add() call, not one per point', async () => {
268
+ const map = createMap()
269
+ const p1 = point('p1', { symbol: 'pin' })
270
+ const p2 = point('p2', { symbol: 'square' }, [3, 4])
271
+ const draw = createDraw([p1, p2])
272
+ // Stagger completion — p2 resolves after p1 to simulate genuinely concurrent, unevenly
273
+ // timed rasterisation.
274
+ let resolveP1, resolveP2
275
+ const mapProvider = {
276
+ addSymbolsToMap: jest.fn((configs) => new Promise((resolve) => {
277
+ const target = configs[0].symbol === 'pin' ? (v) => { resolveP1 = v } : (v) => { resolveP2 = v }
278
+ target(resolve)
279
+ }))
280
+ }
281
+
282
+ const pending = refreshAllPointSymbols({ draw, mapProvider, map })
283
+ resolveP2()
284
+ await Promise.resolve()
285
+ resolveP1()
286
+ await pending
287
+
288
+ expect(draw.add).toHaveBeenCalledTimes(1)
289
+ const [{ type, features }] = draw.add.mock.calls[0]
290
+ expect(type).toBe('FeatureCollection')
291
+ expect(features.map((f) => f.id).sort()).toEqual(['p1', 'p2'])
292
+ })
293
+
294
+ it('logs and still applies the points that did resolve when one point fails', async () => {
295
+ const map = createMap()
296
+ const p1 = point('p1', { symbol: 'pin' })
297
+ const p2 = point('p2', { symbol: 'not-a-real-symbol' })
298
+ const draw = createDraw([p1, p2])
299
+ const mapProvider = {
300
+ addSymbolsToMap: jest.fn((configs) =>
301
+ configs[0].symbol === 'not-a-real-symbol' ? Promise.reject(new Error('rasterise failed')) : Promise.resolve()
302
+ )
303
+ }
304
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
305
+
306
+ await refreshAllPointSymbols({ draw, mapProvider, map })
307
+
308
+ expect(consoleError).toHaveBeenCalledWith('[draw] failed to resolve point symbol', expect.any(Error))
309
+ expect(draw.add).toHaveBeenCalledTimes(1)
310
+ const [{ features }] = draw.add.mock.calls[0]
311
+ expect(features.map((f) => f.id)).toEqual(['p1'])
312
+ consoleError.mockRestore()
313
+ })
314
+ })
@@ -95,12 +95,17 @@ function patchColorMethods (proto, orig, colors) {
95
95
  }
96
96
  }
97
97
 
98
- // Skip when disabled or zooming; clean up internal arrays to prevent memory accumulation
98
+ // Skip when disabled, zooming, or (for unmarked calls only, while suspended see
99
+ // snapHelpers.js's setAutoSnapSuspended) mid-drag; clean up internal arrays to prevent memory
100
+ // accumulation
99
101
  function patchSnapMethod (proto, orig) {
100
102
  proto.snapToClosestPoint = function (e) {
101
103
  if (!this.status || this.map?._isZooming) {
102
104
  return undefined
103
105
  }
106
+ if (this._autoSnapSuspended && !e._explicit) {
107
+ return undefined
108
+ }
104
109
  try {
105
110
  const result = orig.snapToClosestPoint.call(this, e)
106
111
  if (this.closeFeatures?.length > MAX_SNAP_FEATURE_CACHE) {
@@ -235,4 +235,25 @@ describe('snapToClosestPoint', () => {
235
235
  expect(self.snapStatus).toBe(false)
236
236
  expect(self.snapCoords).toBeNull()
237
237
  })
238
+
239
+ // While an edit mode's own offset-aware drag has suspended it (setAutoSnapSuspended, see its
240
+ // own comment), the snap library's own uncontrolled mousemove-driven auto-query must be
241
+ // skipped — otherwise it re-queries at the raw cursor and fights the mode's own query, which
242
+ // is why the two used to visibly fall out of sync (the indicator tracking the cursor while
243
+ // the dragged feature snapped to a different, offset-corrected result).
244
+ test('skips an unmarked call while auto-snap is suspended', () => {
245
+ const self = { status: true, map: {}, _autoSnapSuspended: true }
246
+ expect(MapboxSnap.prototype.snapToClosestPoint.call(self, {})).toBeUndefined()
247
+ expect(MapboxSnap.__orig.snapToClosestPoint).not.toHaveBeenCalled()
248
+ })
249
+
250
+ test('still runs an explicit call while auto-snap is suspended', () => {
251
+ const self = { status: true, map: {}, _autoSnapSuspended: true }
252
+ expect(MapboxSnap.prototype.snapToClosestPoint.call(self, { _explicit: true })).toBe('orig-snap')
253
+ })
254
+
255
+ test('runs unmarked calls normally once auto-snap is no longer suspended', () => {
256
+ const self = { status: true, map: {}, _autoSnapSuspended: false }
257
+ expect(MapboxSnap.prototype.snapToClosestPoint.call(self, {})).toBe('orig-snap')
258
+ })
238
259
  })
@@ -13,8 +13,12 @@ function cleanupOldSnap (map) {
13
13
  }
14
14
 
15
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'])
16
+ // drawing or editing — outside these modes there is nothing to snap to. This list is
17
+ // independent of (and must be kept in sync with) MaplibreDrawAdapter's own DRAW_MODES set,
18
+ // which only controls when the snap *indicator* gets cleared on mode exit — this one gates
19
+ // snap.status itself, which both the visual indicator AND actual snap-to-coordinate
20
+ // (isSnapActive() in snapHelpers.js) depend on.
21
+ const SNAP_ACTIVE_MODES = new Set(['draw_polygon', 'draw_line', 'draw_point', 'edit_vertex', 'edit_point'])
18
22
 
19
23
  /**
20
24
  * Externally-controlled status: ignore library writes so status is only changed
@@ -89,6 +89,18 @@ describe('createSnapInstance', () => {
89
89
  expect(snap.status).toBe(true)
90
90
  })
91
91
 
92
+ test('status is true for every draw/edit mode, including point placement/edit', () => {
93
+ const map = makeMap()
94
+ const draw = { getMode: jest.fn(() => 'draw_polygon') }
95
+ const snap = createSnapInstance(map, draw, {}, { ...config, status: false })
96
+ snap.setSnapStatus(true)
97
+
98
+ for (const mode of ['draw_polygon', 'draw_line', 'draw_point', 'edit_vertex', 'edit_point']) {
99
+ draw.getMode.mockReturnValue(mode)
100
+ expect(snap.status).toBe(true)
101
+ }
102
+ })
103
+
92
104
  test('setSnapLayers overrides, resets and ignores invalid input', () => {
93
105
  const map = makeMap()
94
106
  const snap = createSnapInstance(map, {}, {}, { ...config, layers: ['a'] })