@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
@@ -1,2 +1,2 @@
1
1
  /*! For license information please see index.js.LICENSE.txt */
2
- !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("defra",[],t):"object"==typeof exports?exports.defra=t():(e.defra=e.defra||{},e.defra.openLayersProvider=t())}(this,()=>(()=>{"use strict";var e,t,r={},n={};function o(e){var t=n[e];if(void 0!==t)return t.exports;var i=n[e]={exports:{}};return r[e](i,i.exports,o),i.exports}o.m=r,o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.f={},o.e=e=>Promise.all(Object.keys(o.f).reduce((t,r)=>(o.f[r](e,t),t),[])),o.u=e=>"im-openlayers-provider.js",o.miniCssF=e=>{},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e={},t="defra.openLayersProvider:",o.l=(r,n,i,a)=>{if(e[r])e[r].push(n);else{var u,c;if(void 0!==i)for(var f=document.getElementsByTagName("script"),p=0;p<f.length;p++){var s=f[p];if(s.getAttribute("src")==r||s.getAttribute("data-webpack")==t+i){u=s;break}}u||(c=!0,(u=document.createElement("script")).charset="utf-8",o.nc&&u.setAttribute("nonce",o.nc),u.setAttribute("data-webpack",t+i),u.src=r),e[r]=[n];var l=(t,n)=>{u.onerror=u.onload=null,clearTimeout(d);var o=e[r];if(delete e[r],u.parentNode&&u.parentNode.removeChild(u),o&&o.forEach(e=>e(n)),t)return t(n)},d=setTimeout(l.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=l.bind(null,u.onerror),u.onload=l.bind(null,u.onload),c&&document.head.appendChild(u)}},o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;o.g.importScripts&&(e=o.g.location+"");var t=o.g.document;if(!e&&t&&(t.currentScript&&"SCRIPT"===t.currentScript.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var n=r.length-1;n>-1&&(!e||!/^http(s?):/.test(e));)e=r[n--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),o.p=e})(),(()=>{var e={57:0};o.f.j=(t,r)=>{var n=o.o(e,t)?e[t]:void 0;if(0!==n)if(n)r.push(n[2]);else{var i=new Promise((r,o)=>n=e[t]=[r,o]);r.push(n[2]=i);var a=o.p+o.u(t),u=new Error;o.l(a,r=>{if(o.o(e,t)&&(0!==(n=e[t])&&(e[t]=void 0),n)){var i=r&&("load"===r.type?"missing":r.type),a=r&&r.target&&r.target.src;u.message="Loading chunk "+t+" failed.\n("+i+": "+a+")",u.name="ChunkLoadError",u.type=i,u.request=a,n[1](u)}},"chunk-"+t,t)}};var t=(t,r)=>{var n,i,[a,u,c]=r,f=0;if(a.some(t=>0!==e[t])){for(n in u)o.o(u,n)&&(o.m[n]=u[n]);c&&c(o)}for(t&&t(r);f<a.length;f++)i=a[f],o.o(e,i)&&e[i]&&e[i][0](),e[i]=0},r=this.webpackChunkdefra_DefraMap=this.webpackChunkdefra_DefraMap||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})();var i={};function a(){var e,t,r="function"==typeof Symbol?Symbol:{},n=r.iterator||"@@iterator",o=r.toStringTag||"@@toStringTag";function i(r,n,o,i){var a=n&&n.prototype instanceof f?n:f,p=Object.create(a.prototype);return u(p,"_invoke",function(r,n,o){var i,a,u,f=0,p=o||[],s=!1,l={p:0,n:0,v:e,a:d,f:d.bind(e,4),d:function(t,r){return i=t,a=0,u=e,l.n=r,c}};function d(r,n){for(a=r,u=n,t=0;!s&&f&&!o&&t<p.length;t++){var o,i=p[t],d=l.p,v=i[2];r>3?(o=v===n)&&(u=i[(a=i[4])?5:(a=3,3)],i[4]=i[5]=e):i[0]<=d&&((o=r<2&&d<i[1])?(a=0,l.v=n,l.n=i[1]):d<v&&(o=r<3||i[0]>n||n>v)&&(i[4]=r,i[5]=n,l.n=v,a=0))}if(o||r>1)return c;throw s=!0,n}return function(o,p,v){if(f>1)throw TypeError("Generator is already running");for(s&&1===p&&d(p,v),a=p,u=v;(t=a<2?e:u)||!s;){i||(a?a<3?(a>1&&(l.n=-1),d(a,u)):l.n=u:l.v=u);try{if(f=2,i){if(a||(o="next"),t=i[o]){if(!(t=t.call(i,u)))throw TypeError("iterator result is not an object");if(!t.done)return t;u=t.value,a<2&&(a=0)}else 1===a&&(t=i.return)&&t.call(i),a<2&&(u=TypeError("The iterator does not provide a '"+o+"' method"),a=1);i=e}else if((t=(s=l.n<0)?u:r.call(n,l))!==c)break}catch(t){i=e,a=1,u=t}finally{f=1}}return{value:t,done:s}}}(r,o,i),!0),p}var c={};function f(){}function p(){}function s(){}t=Object.getPrototypeOf;var l=[][n]?t(t([][n]())):(u(t={},n,function(){return this}),t),d=s.prototype=f.prototype=Object.create(l);function v(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,s):(e.__proto__=s,u(e,o,"GeneratorFunction")),e.prototype=Object.create(d),e}return p.prototype=s,u(d,"constructor",s),u(s,"constructor",p),p.displayName="GeneratorFunction",u(s,o,"GeneratorFunction"),u(d),u(d,o,"Generator"),u(d,n,function(){return this}),u(d,"toString",function(){return"[object Generator]"}),(a=function(){return{w:i,m:v}})()}function u(e,t,r,n){var o=Object.defineProperty;try{o({},"",{})}catch(e){o=0}u=function(e,t,r,n){function i(t,r){u(e,t,function(e){return this._invoke(t,r,e)})}t?o?o(e,t,{value:r,enumerable:!n,configurable:!n,writable:!n}):e[t]=r:(i("next",0),i("throw",1),i("return",2))},u(e,t,r,n)}function c(e,t,r,n,o,i,a){try{var u=e[i](a),c=u.value}catch(e){return void r(e)}u.done?t(c):Promise.resolve(c).then(n,o)}function f(){var e,t,r=(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}).zoomAlignment;return{checkDeviceCapabilities:function(){return{isSupported:!!document.createElement("canvas").getContext,error:"Canvas is not supported in this browser"}},load:(e=a().m(function e(){var t,n;return a().w(function(e){for(;;)switch(e.n){case 0:return e.n=1,o.e(142).then(o.bind(o,480));case 1:return t=e.v.default,n={zoomAlignment:r,crs:"EPSG:27700"},e.a(2,{MapProvider:t,mapProviderConfig:n})}},e)}),t=function(){var t=this,r=arguments;return new Promise(function(n,o){var i=e.apply(t,r);function a(e){c(i,n,o,a,u,"next",e)}function u(e){c(i,n,o,a,u,"throw",e)}a(void 0)})},function(){return t.apply(this,arguments)})}}return o.d(i,{default:()=>f}),i.default})());
2
+ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("defra",[],t):"object"==typeof exports?exports.defra=t():(e.defra=e.defra||{},e.defra.openLayersProvider=t())}(this,()=>(()=>{"use strict";var e,t,r={},n={};function o(e){var t=n[e];if(void 0!==t)return t.exports;var i=n[e]={exports:{}};return r[e](i,i.exports,o),i.exports}o.m=r,o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.f={},o.e=e=>Promise.all(Object.keys(o.f).reduce((t,r)=>(o.f[r](e,t),t),[])),o.u=e=>"im-openlayers-provider.js",o.miniCssF=e=>{},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e={},t="defra.openLayersProvider:",o.l=(r,n,i,a)=>{if(e[r])e[r].push(n);else{var u,c;if(void 0!==i)for(var f=document.getElementsByTagName("script"),p=0;p<f.length;p++){var s=f[p];if(s.getAttribute("src")==r||s.getAttribute("data-webpack")==t+i){u=s;break}}u||(c=!0,(u=document.createElement("script")).charset="utf-8",o.nc&&u.setAttribute("nonce",o.nc),u.setAttribute("data-webpack",t+i),u.src=r),e[r]=[n];var l=(t,n)=>{u.onerror=u.onload=null,clearTimeout(d);var o=e[r];if(delete e[r],u.parentNode&&u.parentNode.removeChild(u),o&&o.forEach(e=>e(n)),t)return t(n)},d=setTimeout(l.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=l.bind(null,u.onerror),u.onload=l.bind(null,u.onload),c&&document.head.appendChild(u)}},o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;o.g.importScripts&&(e=o.g.location+"");var t=o.g.document;if(!e&&t&&(t.currentScript&&"SCRIPT"===t.currentScript.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var n=r.length-1;n>-1&&(!e||!/^http(s?):/.test(e));)e=r[n--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),o.p=e})(),(()=>{var e={57:0};o.f.j=(t,r)=>{var n=o.o(e,t)?e[t]:void 0;if(0!==n)if(n)r.push(n[2]);else{var i=new Promise((r,o)=>n=e[t]=[r,o]);r.push(n[2]=i);var a=o.p+o.u(t),u=new Error;o.l(a,r=>{if(o.o(e,t)&&(0!==(n=e[t])&&(e[t]=void 0),n)){var i=r&&("load"===r.type?"missing":r.type),a=r&&r.target&&r.target.src;u.message="Loading chunk "+t+" failed.\n("+i+": "+a+")",u.name="ChunkLoadError",u.type=i,u.request=a,n[1](u)}},"chunk-"+t,t)}};var t=(t,r)=>{var n,i,[a,u,c]=r,f=0;if(a.some(t=>0!==e[t])){for(n in u)o.o(u,n)&&(o.m[n]=u[n]);c&&c(o)}for(t&&t(r);f<a.length;f++)i=a[f],o.o(e,i)&&e[i]&&e[i][0](),e[i]=0},r=this.webpackChunkdefra_DefraMap=this.webpackChunkdefra_DefraMap||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})();var i={};function a(){var e,t,r="function"==typeof Symbol?Symbol:{},n=r.iterator||"@@iterator",o=r.toStringTag||"@@toStringTag";function i(r,n,o,i){var a=n&&n.prototype instanceof f?n:f,p=Object.create(a.prototype);return u(p,"_invoke",function(r,n,o){var i,a,u,f=0,p=o||[],s=!1,l={p:0,n:0,v:e,a:d,f:d.bind(e,4),d:function(t,r){return i=t,a=0,u=e,l.n=r,c}};function d(r,n){for(a=r,u=n,t=0;!s&&f&&!o&&t<p.length;t++){var o,i=p[t],d=l.p,v=i[2];r>3?(o=v===n)&&(u=i[(a=i[4])?5:(a=3,3)],i[4]=i[5]=e):i[0]<=d&&((o=r<2&&d<i[1])?(a=0,l.v=n,l.n=i[1]):d<v&&(o=r<3||i[0]>n||n>v)&&(i[4]=r,i[5]=n,l.n=v,a=0))}if(o||r>1)return c;throw s=!0,n}return function(o,p,v){if(f>1)throw TypeError("Generator is already running");for(s&&1===p&&d(p,v),a=p,u=v;(t=a<2?e:u)||!s;){i||(a?a<3?(a>1&&(l.n=-1),d(a,u)):l.n=u:l.v=u);try{if(f=2,i){if(a||(o="next"),t=i[o]){if(!(t=t.call(i,u)))throw TypeError("iterator result is not an object");if(!t.done)return t;u=t.value,a<2&&(a=0)}else 1===a&&(t=i.return)&&t.call(i),a<2&&(u=TypeError("The iterator does not provide a '"+o+"' method"),a=1);i=e}else if((t=(s=l.n<0)?u:r.call(n,l))!==c)break}catch(t){i=e,a=1,u=t}finally{f=1}}return{value:t,done:s}}}(r,o,i),!0),p}var c={};function f(){}function p(){}function s(){}t=Object.getPrototypeOf;var l=[][n]?t(t([][n]())):(u(t={},n,function(){return this}),t),d=s.prototype=f.prototype=Object.create(l);function v(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,s):(e.__proto__=s,u(e,o,"GeneratorFunction")),e.prototype=Object.create(d),e}return p.prototype=s,u(d,"constructor",s),u(s,"constructor",p),p.displayName="GeneratorFunction",u(s,o,"GeneratorFunction"),u(d),u(d,o,"Generator"),u(d,n,function(){return this}),u(d,"toString",function(){return"[object Generator]"}),(a=function(){return{w:i,m:v}})()}function u(e,t,r,n){var o=Object.defineProperty;try{o({},"",{})}catch(e){o=0}u=function(e,t,r,n){function i(t,r){u(e,t,function(e){return this._invoke(t,r,e)})}t?o?o(e,t,{value:r,enumerable:!n,configurable:!n,writable:!n}):e[t]=r:(i("next",0),i("throw",1),i("return",2))},u(e,t,r,n)}function c(e,t,r,n,o,i,a){try{var u=e[i](a),c=u.value}catch(e){return void r(e)}u.done?t(c):Promise.resolve(c).then(n,o)}function f(){var e,t,r=(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}).zoomAlignment;return{checkDeviceCapabilities:function(){return{isSupported:!!document.createElement("canvas").getContext,error:"Canvas is not supported in this browser"}},load:(e=a().m(function e(){var t,n;return a().w(function(e){for(;;)switch(e.n){case 0:return e.n=1,o.e(142).then(o.bind(o,501));case 1:return t=e.v.default,n={zoomAlignment:r,crs:"EPSG:27700"},e.a(2,{MapProvider:t,mapProviderConfig:n})}},e)}),t=function(){var t=this,r=arguments;return new Promise(function(n,o){var i=e.apply(t,r);function a(e){c(i,n,o,a,u,"next",e)}function u(e){c(i,n,o,a,u,"throw",e)}a(void 0)})},function(){return t.apply(this,arguments)})}}return o.d(i,{default:()=>f}),i.default})());
@@ -2,6 +2,7 @@ import { MapProvider } from '../../../mapProvider.js'
2
2
  import OlMap from 'ol/Map.js'
3
3
  import View from 'ol/View.js'
4
4
  import { defaults as defaultInteractions } from 'ol/interaction/defaults.js'
5
+ import { getCenter as getExtentCenter } from 'ol/extent.js'
5
6
  import proj4 from 'proj4'
6
7
  import { register } from 'ol/proj/proj4.js'
7
8
  import { supportedShortcuts, DEFAULTS } from './defaults.js'
@@ -57,9 +58,11 @@ export default class OpenLayersProvider extends MapProvider {
57
58
 
58
59
  const viewResolutions = getViewResolutionConfig(this.zoomAlignment ?? ZOOM_ALIGNMENT.UK)
59
60
 
61
+ // A View with no center never renders a frame, so 'rendercomplete' below (which applies
62
+ // `bounds`) would never fire — fall back to the bounds' own center so it can.
60
63
  const view = new View({
61
64
  projection: CRS,
62
- center,
65
+ center: center ?? (bounds ? getExtentCenter(bounds) : undefined),
63
66
  zoom: zoom ?? viewResolutions.defaultMinZoom,
64
67
  minZoom: Math.max(minZoom ?? viewResolutions.defaultMinZoom, viewResolutions.defaultMinZoom),
65
68
  maxZoom: maxZoom ?? viewResolutions.maxZoom,
@@ -3,6 +3,7 @@ import OpenLayersProvider from './openlayersProvider.js'
3
3
  import { attachMapEvents } from './mapEvents.js'
4
4
  import { attachAppEvents, createMapStyleLayer } from './appEvents.js'
5
5
  import { getExtentFromGeoJSON, isGeometryObscured } from './utils/spatial.js'
6
+ import View from 'ol/View.js'
6
7
 
7
8
  const mockAnimate = jest.fn()
8
9
  const mockGetCenter = jest.fn(() => [400000.126, 300000.455])
@@ -154,6 +155,23 @@ describe('OpenLayersProvider', () => {
154
155
  expect(mockMapOnce).not.toHaveBeenCalled()
155
156
  })
156
157
 
158
+ // Regression: a View with no center is never "defined" (ol/View#isDef), so the map can't
159
+ // render a single frame — meaning 'rendercomplete' (which is what applies bounds) never
160
+ // fires either, deadlocking on a permanently blank map for bounds-only config.
161
+ it('derives an initial center from bounds when no explicit center is given, so the map can render at all', async () => {
162
+ const { provider } = makeProvider()
163
+ const bounds = [100, 200, 300, 400]
164
+ await provider.initMap({ ...defaultInitConfig, center: undefined, bounds })
165
+ expect(View).toHaveBeenCalledWith(expect.objectContaining({ center: [200, 300] }))
166
+ })
167
+
168
+ it('prefers an explicit center over one derived from bounds', async () => {
169
+ const { provider } = makeProvider()
170
+ const bounds = [100, 200, 300, 400]
171
+ await provider.initMap({ ...defaultInitConfig, center: [400000, 300000], bounds })
172
+ expect(View).toHaveBeenCalledWith(expect.objectContaining({ center: [400000, 300000] }))
173
+ })
174
+
157
175
  it('creates the initial layer from the map style', async () => {
158
176
  const { provider } = makeProvider()
159
177
  await provider.initMap(defaultInitConfig)
@@ -6,7 +6,11 @@ import GeoJSON from 'ol/format/GeoJSON.js'
6
6
  import Style from 'ol/style/Style.js'
7
7
  import Stroke from 'ol/style/Stroke.js'
8
8
  import Fill from 'ol/style/Fill.js'
9
+ import Icon from 'ol/style/Icon.js'
9
10
  import { collectTileFragments } from './vtTileFragments.js'
11
+ import { getCachedSymbolImage } from './symbolImages.js'
12
+ import { symbolRegistry } from '../../../../../src/services/symbolRegistry.js'
13
+ import { getSymbolAnchor } from '../../../../../src/utils/symbolUtils.js'
10
14
 
11
15
  const CRS = 'EPSG:27700'
12
16
  const geoJsonFormat = new GeoJSON({ dataProjection: CRS, featureProjection: CRS })
@@ -34,6 +38,28 @@ const buildHighlightStyles = (styleEntry, isActive) => {
34
38
  return styles
35
39
  }
36
40
 
41
+ const hasSymbolStyle = (properties) => !!(properties?.symbol || properties?.symbolSvgContent)
42
+
43
+ // A drawn point renders as a real symbol icon, not Stroke/Fill, so its selected/active ring is
44
+ // the active/selected variant of that same icon instead. Returns null (not []) for a
45
+ // non-symbol feature so the caller falls through to buildHighlightStyles.
46
+ const buildSymbolHighlightStyle = (properties, isActive) => {
47
+ if (!hasSymbolStyle(properties)) {
48
+ return null
49
+ }
50
+ const imageId = isActive ? properties.symbolActiveImageId : properties.symbolSelectedImageId
51
+ const canvas = imageId && getCachedSymbolImage(imageId)
52
+ if (!canvas) {
53
+ return null
54
+ }
55
+ const symbolDef = symbolRegistry.getSymbolDef(properties)
56
+ const anchor = getSymbolAnchor(properties, symbolDef)
57
+ // The cached canvas is rasterised at symbolPixelRatio for crispness — ol/style/Icon draws
58
+ // it at native size, so the inverse scale keeps the displayed size correct.
59
+ const scale = 1 / (properties.symbolPixelRatio || 1)
60
+ return [new Style({ image: new Icon({ img: canvas, anchor, scale }), zIndex: HIGHLIGHT_Z })]
61
+ }
62
+
37
63
  // ---------------------------------------------------------------------------
38
64
  // VectorTileLayer: style-function wrap
39
65
  // Evaluated per feature per tile at render time — automatically covers all
@@ -127,12 +153,33 @@ const getOrCreateHighlightLayer = (map) => {
127
153
  return layer
128
154
  }
129
155
 
130
- const addVectorHighlights = (source, features, isActive, stylesMap) => {
131
- for (const { layerId, geometry } of features ?? []) {
156
+ // interact's selectedFeatures carry a `properties` snapshot taken at selection time, which
157
+ // goes stale for the symbol icon path once a map style change re-resolves the point's image
158
+ // ids — reading straight off the live feature avoids showing the previous theme's variant.
159
+ const getLiveProperties = (map, layerId, featureId) => {
160
+ if (featureId == null) {
161
+ return undefined
162
+ }
163
+ let properties
164
+ map.getLayers().forEach(l => {
165
+ if (properties || !(l instanceof VectorLayer) || l.get(HIGHLIGHT_MARKER) || l.get('layerId') !== layerId) {
166
+ return
167
+ }
168
+ const feature = l.getSource()?.getFeatureById(String(featureId))
169
+ if (feature) {
170
+ properties = feature.getProperties()
171
+ }
172
+ })
173
+ return properties
174
+ }
175
+
176
+ const addVectorHighlights = (map, source, features, isActive, stylesMap) => {
177
+ for (const { layerId, featureId, geometry } of features ?? []) {
132
178
  if (!geometry) {
133
179
  continue
134
180
  }
135
- const styles = buildHighlightStyles(stylesMap?.[layerId], isActive)
181
+ const liveProperties = getLiveProperties(map, layerId, featureId)
182
+ const styles = buildSymbolHighlightStyle(liveProperties, isActive) ?? buildHighlightStyles(stylesMap?.[layerId], isActive)
136
183
  if (!styles.length) {
137
184
  continue
138
185
  }
@@ -236,8 +283,8 @@ export const updateHighlightedFeatures = (map, selectedFeatures, activeFeatures,
236
283
  const vecActive = (activeFeatures ?? []).filter(f => vectorLayerIds.has(f.layerId))
237
284
 
238
285
  // VT features are handled by style-wrap; only add vector features to overlay
239
- addVectorHighlights(hlSource, vecActive, true, stylesMap)
240
- addVectorHighlights(hlSource, vecSelected, false, stylesMap)
286
+ addVectorHighlights(map, hlSource, vecActive, true, stylesMap)
287
+ addVectorHighlights(map, hlSource, vecSelected, false, stylesMap)
241
288
 
242
289
  // Bounds from all tile fragments of selected features
243
290
  const allGeoms = (selectedFeatures ?? []).flatMap(feat => resolveGeometries(map, feat))
@@ -0,0 +1,176 @@
1
+ import VectorLayer from 'ol/layer/Vector.js'
2
+ import VectorSource from 'ol/source/Vector.js'
3
+ import VectorTileLayer from 'ol/layer/VectorTile.js'
4
+ import OlFeature from 'ol/Feature.js'
5
+ import Point from 'ol/geom/Point.js'
6
+ import Icon from 'ol/style/Icon.js'
7
+ import { updateHighlightedFeatures } from './highlightFeatures.js'
8
+ import { getOrCreateSymbolImage, clearSymbolImageCache } from './symbolImages.js'
9
+
10
+ const HIGHLIGHT_MARKER = '_highlight'
11
+
12
+ // Mirrors the map interface this module actually calls: getLayers().forEach(...) to find/
13
+ // enumerate real ol/layer instances, and addLayer() to register new ones. Layers passed in
14
+ // (or added during a test) are real ol/layer/Vector or ol/layer/VectorTile instances, same as
15
+ // the rest of this codebase's OL tests use real ol classes over mocks for anything the
16
+ // module under test actually instantiates or type-checks (instanceof).
17
+ const createFakeMap = (layers = []) => {
18
+ const list = [...layers]
19
+ return {
20
+ _layers: list,
21
+ getLayers: () => ({ forEach: (cb) => list.forEach(cb) }),
22
+ addLayer: jest.fn((l) => list.push(l))
23
+ }
24
+ }
25
+
26
+ const getHighlightLayer = (map) => map._layers.find(l => l.get(HIGHLIGHT_MARKER))
27
+
28
+ // features: [[id, properties]] — populates the layer's real VectorSource, since the module
29
+ // under test now reads a selected/active point's CURRENT properties straight off this live
30
+ // feature (not off whatever the caller's selectedFeatures/activeFeatures snapshot carries) —
31
+ // see getLiveProperties's own comment for why.
32
+ const drawLayer = (features = []) => {
33
+ const source = new VectorSource()
34
+ features.forEach(([id, properties]) => {
35
+ const f = new OlFeature({ geometry: new Point([1, 2]), ...properties })
36
+ f.setId(id)
37
+ source.addFeature(f)
38
+ })
39
+ const layer = new VectorLayer({ source })
40
+ layer.set('layerId', 'draw')
41
+ return layer
42
+ }
43
+
44
+ const selEntry = (id, geometry = { type: 'Point', coordinates: [1, 2] }) => ({ layerId: 'draw', featureId: id, geometry })
45
+
46
+ beforeEach(() => {
47
+ clearSymbolImageCache()
48
+ HTMLCanvasElement.prototype.getContext = jest.fn(function () {
49
+ this._ctx ??= { putImageData: jest.fn() }
50
+ return this._ctx
51
+ })
52
+ })
53
+
54
+ describe('updateHighlightedFeatures', () => {
55
+ test('returns null and does nothing when there is no map', () => {
56
+ expect(updateHighlightedFeatures(null, [], [], {})).toBeNull()
57
+ })
58
+
59
+ test('creates the highlight overlay layer once and reuses it on later calls', () => {
60
+ const map = createFakeMap([drawLayer()])
61
+ updateHighlightedFeatures(map, [], [], {})
62
+ expect(map.addLayer).toHaveBeenCalledTimes(1)
63
+ updateHighlightedFeatures(map, [], [], {})
64
+ expect(map.addLayer).toHaveBeenCalledTimes(1) // reused, not re-added
65
+ })
66
+
67
+ test('clears previous overlay features before adding the current selection', () => {
68
+ const map = createFakeMap([drawLayer([['f1', {}]])])
69
+ const stylesMap = { draw: { stroke: '#000', selectionStroke: '#000', fill: 'transparent', strokeWidth: 2, activeStrokeWidth: 2 } }
70
+ updateHighlightedFeatures(map, [{ ...selEntry('f1'), geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] } }], [], stylesMap)
71
+ expect(getHighlightLayer(map).getSource().getFeatures()).toHaveLength(1)
72
+ updateHighlightedFeatures(map, [], [], stylesMap)
73
+ expect(getHighlightLayer(map).getSource().getFeatures()).toHaveLength(0)
74
+ })
75
+
76
+ test('a feature with no geometry is skipped', () => {
77
+ const map = createFakeMap([drawLayer()])
78
+ const stylesMap = { draw: { stroke: '#000', selectionStroke: '#000', fill: 'transparent', strokeWidth: 2, activeStrokeWidth: 2 } }
79
+ updateHighlightedFeatures(map, [{ layerId: 'draw' }], [], stylesMap)
80
+ expect(getHighlightLayer(map).getSource().getFeatures()).toHaveLength(0)
81
+ })
82
+
83
+ test('a feature whose layerId has no stylesMap entry and no symbol properties renders nothing', () => {
84
+ const map = createFakeMap([drawLayer([['f1', {}]])])
85
+ updateHighlightedFeatures(map, [{ ...selEntry('f1'), geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] } }], [], {})
86
+ expect(getHighlightLayer(map).getSource().getFeatures()).toHaveLength(0)
87
+ })
88
+
89
+ test('plain (non-symbol) Polygon/LineString selection still styles via Stroke/Fill, unaffected by the symbol path', () => {
90
+ const map = createFakeMap([drawLayer([['f1', {}]])])
91
+ const stylesMap = { draw: { stroke: '#000', selectionStroke: '#111', fill: '#222', strokeWidth: 3, activeStrokeWidth: 5 } }
92
+ const feature = { ...selEntry('f1'), geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]]] } }
93
+ updateHighlightedFeatures(map, [feature], [], stylesMap)
94
+ const [hlFeature] = getHighlightLayer(map).getSource().getFeatures()
95
+ const styles = hlFeature.getStyle()
96
+ expect(styles.some(s => s.getFill())).toBe(true) // selected polygons get a fill
97
+ expect(styles.every(s => !s.getImage())).toBe(true) // no Icon involved
98
+ })
99
+
100
+ describe('symbol-styled points (draw)', () => {
101
+ const symbolProperties = (variantImageId, imageId = 'sym-normal') => ({
102
+ symbol: 'pin',
103
+ symbolImageId: imageId,
104
+ symbolSelectedImageId: variantImageId,
105
+ symbolActiveImageId: variantImageId,
106
+ symbolPixelRatio: 2
107
+ })
108
+
109
+ test('renders the selected-variant Icon for a selected symbol point, scaled down by symbolPixelRatio', () => {
110
+ const canvas = getOrCreateSymbolImage('sym-sel', { width: 88, height: 88 })
111
+ const map = createFakeMap([drawLayer([['f1', symbolProperties('sym-sel')]])])
112
+ updateHighlightedFeatures(map, [selEntry('f1')], [], {})
113
+ const [hlFeature] = getHighlightLayer(map).getSource().getFeatures()
114
+ const [style] = hlFeature.getStyle()
115
+ expect(style.getImage()).toBeInstanceOf(Icon)
116
+ expect(style.getImage().getImage(1)).toBe(canvas)
117
+ expect(style.getImage().getScale()).toBe(0.5) // 1 / symbolPixelRatio (2)
118
+ })
119
+
120
+ test('renders the active-variant Icon for the keyboard-cursor item', () => {
121
+ const canvas = getOrCreateSymbolImage('sym-act', { width: 44, height: 44 })
122
+ const map = createFakeMap([drawLayer([['f1', symbolProperties('sym-act')]])])
123
+ updateHighlightedFeatures(map, [], [selEntry('f1')], {})
124
+ const [hlFeature] = getHighlightLayer(map).getSource().getFeatures()
125
+ const [style] = hlFeature.getStyle()
126
+ expect(style.getImage().getImage(1)).toBe(canvas)
127
+ })
128
+
129
+ // The whole point of reading live properties instead of trusting selectedFeatures'
130
+ // captured-at-selection-time snapshot: a map style change re-resolves the point's
131
+ // variants (point/pointSymbolImages.js) and rewrites them onto the live feature — the
132
+ // highlight must pick up the NEW id, not whatever was true when the user first selected.
133
+ test('picks up a re-resolved symbolSelectedImageId after a map style change, not whatever was true at selection time', () => {
134
+ const canvasLight = getOrCreateSymbolImage('sym-sel-light', { width: 44, height: 44 })
135
+ const canvasDark = getOrCreateSymbolImage('sym-sel-dark', { width: 44, height: 44 })
136
+ const layer = drawLayer([['f1', symbolProperties('sym-sel-light')]])
137
+ const map = createFakeMap([layer])
138
+
139
+ updateHighlightedFeatures(map, [selEntry('f1')], [], {})
140
+ const before = getHighlightLayer(map).getSource().getFeatures()[0].getStyle()[0]
141
+ expect(before.getImage().getImage(1)).toBe(canvasLight)
142
+
143
+ // Simulate refreshAllPointSymbols re-resolving the point against a new map style.
144
+ layer.getSource().getFeatureById('f1').set('symbolSelectedImageId', 'sym-sel-dark')
145
+ updateHighlightedFeatures(map, [selEntry('f1')], [], {})
146
+ const after = getHighlightLayer(map).getSource().getFeatures()[0].getStyle()[0]
147
+ expect(after.getImage().getImage(1)).toBe(canvasDark)
148
+ })
149
+
150
+ test('falls back to no highlight (not a crash) when the variant has not been cached yet', () => {
151
+ const map = createFakeMap([drawLayer([['f1', symbolProperties('sym-not-cached')]])])
152
+ updateHighlightedFeatures(map, [selEntry('f1')], [], {})
153
+ expect(getHighlightLayer(map).getSource().getFeatures()).toHaveLength(0)
154
+ })
155
+
156
+ test('falls back to no highlight when the live feature cannot be found (e.g. deleted mid-flight)', () => {
157
+ const map = createFakeMap([drawLayer()]) // empty source — 'f1' doesn't exist
158
+ updateHighlightedFeatures(map, [selEntry('f1')], [], {})
159
+ expect(getHighlightLayer(map).getSource().getFeatures()).toHaveLength(0)
160
+ })
161
+
162
+ test('a plain Point with no symbol config falls through to Stroke/Fill (still no visible highlight)', () => {
163
+ const map = createFakeMap([drawLayer([['f1', {}]])])
164
+ updateHighlightedFeatures(map, [selEntry('f1')], [], {})
165
+ expect(getHighlightLayer(map).getSource().getFeatures()).toHaveLength(0)
166
+ })
167
+ })
168
+ })
169
+
170
+ describe('VectorTileLayer style-wrap (smoke test — pre-existing, undocumented path)', () => {
171
+ test('does not throw when a VT layer is present alongside a draw VectorLayer', () => {
172
+ const vt = new VectorTileLayer({})
173
+ const map = createFakeMap([vt, drawLayer()])
174
+ expect(() => updateHighlightedFeatures(map, [], [], {})).not.toThrow()
175
+ })
176
+ })
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The OL "last mile" for symbolRegistry's rasterised symbol images: turns the ImageData
3
+ * rasteriseSymbolImage() produces into something ol/style/Icon can render (a canvas),
4
+ * and caches it by the same content-hash imageId symbolRegistry already computes — so
5
+ * repeated resolves of the same style/theme/pixel-ratio combination are free after the
6
+ * first. Unlike MapLibre (map.addImage), OL has no map-level image registry of its own,
7
+ * so this cache lives at the module level here instead.
8
+ */
9
+
10
+ const imageCache = new Map() // imageId → HTMLCanvasElement
11
+
12
+ /** Synchronous lookup for style functions (which must be synchronous) — undefined until
13
+ * getOrCreateSymbolImage() has resolved it at least once. */
14
+ export const getCachedSymbolImage = (imageId) => imageCache.get(imageId)
15
+
16
+ /**
17
+ * Rasterise-once-cache-forever: draws the given ImageData onto a canvas the first time
18
+ * imageId is seen, and returns the cached canvas on every subsequent call.
19
+ *
20
+ * @param {string} imageId
21
+ * @param {ImageData} imageData
22
+ * @returns {HTMLCanvasElement}
23
+ */
24
+ export const getOrCreateSymbolImage = (imageId, imageData) => {
25
+ const cached = imageCache.get(imageId)
26
+ if (cached) {
27
+ return cached
28
+ }
29
+ const canvas = document.createElement('canvas')
30
+ canvas.width = imageData.width
31
+ canvas.height = imageData.height
32
+ canvas.getContext('2d').putImageData(imageData, 0, 0)
33
+ imageCache.set(imageId, canvas)
34
+ return canvas
35
+ }
36
+
37
+ /** Mainly for testing — clears every cached image. */
38
+ export const clearSymbolImageCache = () => {
39
+ imageCache.clear()
40
+ }
@@ -0,0 +1,58 @@
1
+ import { getCachedSymbolImage, getOrCreateSymbolImage, clearSymbolImageCache } from './symbolImages.js'
2
+
3
+ const imageData = (width, height) => ({ width, height })
4
+
5
+ beforeEach(() => {
6
+ clearSymbolImageCache()
7
+ // Memoized per-canvas so `canvas.getContext()` returns the same mock object on repeat
8
+ // calls (real canvases do the same) — needed so assertions and the code under test see
9
+ // the same putImageData spy, not two different ones from two different mock calls.
10
+ HTMLCanvasElement.prototype.getContext = jest.fn(function () {
11
+ this._ctx ??= { putImageData: jest.fn() }
12
+ return this._ctx
13
+ })
14
+ })
15
+
16
+ describe('getCachedSymbolImage', () => {
17
+ it('returns undefined for an id that has never been registered', () => {
18
+ expect(getCachedSymbolImage('symbol-unseen')).toBeUndefined()
19
+ })
20
+
21
+ it('returns the cached canvas after getOrCreateSymbolImage has resolved it', () => {
22
+ const canvas = getOrCreateSymbolImage('symbol-a', imageData(20, 30))
23
+ expect(getCachedSymbolImage('symbol-a')).toBe(canvas)
24
+ })
25
+ })
26
+
27
+ describe('getOrCreateSymbolImage', () => {
28
+ it('draws the ImageData onto a canvas sized to match it', () => {
29
+ const canvas = getOrCreateSymbolImage('symbol-b', imageData(44, 60))
30
+ expect(canvas).toBeInstanceOf(HTMLCanvasElement)
31
+ expect(canvas.width).toBe(44)
32
+ expect(canvas.height).toBe(60)
33
+ expect(canvas.getContext().putImageData).toHaveBeenCalledWith(imageData(44, 60), 0, 0)
34
+ })
35
+
36
+ it('rasterises only once — a second call with the same id returns the same canvas without redrawing', () => {
37
+ const first = getOrCreateSymbolImage('symbol-c', imageData(10, 10))
38
+ const contextCallsAfterFirst = HTMLCanvasElement.prototype.getContext.mock.calls.length
39
+ const second = getOrCreateSymbolImage('symbol-c', imageData(10, 10))
40
+ expect(second).toBe(first)
41
+ expect(HTMLCanvasElement.prototype.getContext.mock.calls).toHaveLength(contextCallsAfterFirst)
42
+ })
43
+
44
+ it('different ids get independent canvases', () => {
45
+ const a = getOrCreateSymbolImage('symbol-d', imageData(10, 10))
46
+ const b = getOrCreateSymbolImage('symbol-e', imageData(20, 20))
47
+ expect(a).not.toBe(b)
48
+ })
49
+ })
50
+
51
+ describe('clearSymbolImageCache', () => {
52
+ it('forces the next getOrCreateSymbolImage call to rasterise again', () => {
53
+ const first = getOrCreateSymbolImage('symbol-f', imageData(10, 10))
54
+ clearSymbolImageCache()
55
+ const second = getOrCreateSymbolImage('symbol-f', imageData(10, 10))
56
+ expect(second).not.toBe(first)
57
+ })
58
+ })