@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
@@ -0,0 +1,83 @@
1
+ import { fetchGeoJSON } from './fetchGeoJSON.js'
2
+
3
+ const baseUrl = 'https://api.example.com/features'
4
+ const context = { bbox: [-1, -1, 1, 1], zoom: 10 }
5
+ const signal = { aborted: false }
6
+
7
+ const mockResponse = ({ ok = true, status = 200, statusText = 'OK', body = {} } = {}) => ({
8
+ ok,
9
+ status,
10
+ statusText,
11
+ json: async () => body
12
+ })
13
+
14
+ beforeEach(() => {
15
+ global.fetch = jest.fn()
16
+ })
17
+
18
+ describe('fetchGeoJSON', () => {
19
+ it('calls transformRequest with the base URL and context, then fetches the returned url/headers', async () => {
20
+ const featureCollection = { type: 'FeatureCollection', features: [] }
21
+ global.fetch.mockResolvedValueOnce(mockResponse({ body: featureCollection }))
22
+ const transformRequest = jest.fn(() => ({ url: `${baseUrl}?bbox=${context.bbox.join(',')}`, headers: { Authorization: 'Bearer token' } }))
23
+
24
+ const result = await fetchGeoJSON(baseUrl, context, transformRequest, signal)
25
+
26
+ expect(transformRequest).toHaveBeenCalledWith(baseUrl, context)
27
+ expect(fetch).toHaveBeenCalledWith(
28
+ `${baseUrl}?bbox=-1,-1,1,1`,
29
+ { headers: { Authorization: 'Bearer token' }, signal }
30
+ )
31
+ expect(result).toBe(featureCollection)
32
+ })
33
+
34
+ it('supports transformRequest returning a plain URL string, defaulting headers to {}', async () => {
35
+ global.fetch.mockResolvedValueOnce(mockResponse({ body: { type: 'FeatureCollection', features: [] } }))
36
+ const transformRequest = () => baseUrl
37
+
38
+ await fetchGeoJSON(baseUrl, context, transformRequest, signal)
39
+
40
+ expect(fetch).toHaveBeenCalledWith(baseUrl, { headers: {}, signal })
41
+ })
42
+
43
+ it('defaults headers to {} when transformRequest returns an object without headers', async () => {
44
+ global.fetch.mockResolvedValueOnce(mockResponse({ body: { type: 'FeatureCollection', features: [] } }))
45
+ const transformRequest = () => ({ url: baseUrl })
46
+
47
+ await fetchGeoJSON(baseUrl, context, transformRequest, signal)
48
+
49
+ expect(fetch).toHaveBeenCalledWith(baseUrl, { headers: {}, signal })
50
+ })
51
+
52
+ it('throws when the response is not ok', async () => {
53
+ global.fetch.mockResolvedValueOnce(mockResponse({ ok: false, status: 500, statusText: 'Server Error' }))
54
+
55
+ await expect(fetchGeoJSON(baseUrl, context, () => baseUrl, signal))
56
+ .rejects.toThrow('Failed to fetch GeoJSON: 500 Server Error')
57
+ })
58
+
59
+ it('wraps a single Feature into a FeatureCollection', async () => {
60
+ const feature = { type: 'Feature', geometry: { type: 'Point', coordinates: [0, 0] } }
61
+ global.fetch.mockResolvedValueOnce(mockResponse({ body: feature }))
62
+
63
+ const result = await fetchGeoJSON(baseUrl, context, () => baseUrl, signal)
64
+
65
+ expect(result).toEqual({ type: 'FeatureCollection', features: [feature] })
66
+ })
67
+
68
+ it('wraps a bare array of features into a FeatureCollection', async () => {
69
+ const features = [{ type: 'Feature', geometry: { type: 'Point', coordinates: [0, 0] } }]
70
+ global.fetch.mockResolvedValueOnce(mockResponse({ body: features }))
71
+
72
+ const result = await fetchGeoJSON(baseUrl, context, () => baseUrl, signal)
73
+
74
+ expect(result).toEqual({ type: 'FeatureCollection', features })
75
+ })
76
+
77
+ it('throws for an unrecognized response shape', async () => {
78
+ global.fetch.mockResolvedValueOnce(mockResponse({ body: { type: 'NotGeoJSON' } }))
79
+
80
+ await expect(fetchGeoJSON(baseUrl, context, () => baseUrl, signal))
81
+ .rejects.toThrow('Invalid GeoJSON response')
82
+ })
83
+ })
@@ -0,0 +1,36 @@
1
+ import createPlugin from './index.js'
2
+
3
+ jest.mock('./datasets.scss', () => {})
4
+ jest.mock('./manifest.js', () => ({
5
+ manifest: { InitComponent: jest.fn(), reducer: {} }
6
+ }))
7
+
8
+ describe('createPlugin', () => {
9
+ it('returns a plugin with id "datasets"', () => {
10
+ const plugin = createPlugin()
11
+ expect(plugin.id).toBe('datasets')
12
+ })
13
+
14
+ it('applies default noKeyItemText', () => {
15
+ const plugin = createPlugin()
16
+ expect(plugin.noKeyItemText).toBe('No features displayed')
17
+ })
18
+
19
+ it('merges provided options (options override defaults, id stays fixed)', () => {
20
+ const plugin = createPlugin({ noKeyItemText: 'Custom text', extra: true })
21
+ expect(plugin.noKeyItemText).toBe('Custom text')
22
+ expect(plugin.extra).toBe(true)
23
+ expect(plugin.id).toBe('datasets')
24
+ })
25
+
26
+ it('exposes a load function', () => {
27
+ const plugin = createPlugin()
28
+ expect(typeof plugin.load).toBe('function')
29
+ })
30
+
31
+ it('load() resolves to the manifest', async () => {
32
+ const plugin = createPlugin()
33
+ const result = await plugin.load()
34
+ expect(result).toBeDefined()
35
+ })
36
+ })
@@ -3,15 +3,27 @@ import { useEffect, useRef } from 'react'
3
3
  import { EVENTS } from '../../../../src/config/events.js'
4
4
  import { initialiseDatasets } from './initialiseDatasets.js'
5
5
  import { datasetRegistry } from '../registry/datasetRegistry.js'
6
- import { setMenuState } from '../registry/isVisibleWhen.js'
7
6
  import { attachGlobalState } from '../registry/globalDataset.js'
8
7
  import { loadLayerAdapter, layerAdapter } from '../adapters/loadLayerAdapter.js'
9
8
 
9
+ const pluginConfigHasMenuItems = (pluginConfig) =>
10
+ pluginConfig.datasets
11
+ .some(dataset => dataset.showInMenu || dataset.sublayers?.some(sublayer => sublayer.showInMenu))
12
+
10
13
  export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, mapProvider, services }) {
11
14
  const { dispatch } = pluginState
12
15
  const { eventBus, symbolRegistry, patternRegistry } = services
13
16
  const isBaseMapReady = Boolean(mapProvider?.isBaseMapReady())
14
17
 
18
+ useEffect(() => {
19
+ const hasMenu = pluginConfig.hasMenu !== false && pluginConfigHasMenuItems(pluginConfig)
20
+
21
+ if (!hasMenu) {
22
+ eventBus.emit(EVENTS.APP_REMOVE_PANEL, 'datasetsLayers')
23
+ eventBus.emit(EVENTS.APP_TOGGLE_BUTTON_STATE, { id: 'datasetsLayers', prop: 'hidden', value: true })
24
+ }
25
+ }, [pluginConfig.hasMenu])
26
+
15
27
  // Keep a ref to the latest pluginState so event handlers can access current data
16
28
  const pluginStateRef = useRef(pluginState)
17
29
  pluginStateRef.current = pluginState
@@ -51,12 +63,10 @@ export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, m
51
63
  }, [isBaseMapReady, appState.mode])
52
64
 
53
65
  useEffect(() => {
54
- setMenuState(pluginState.menuState)
55
- datasetRegistry.invalidateKeyItems()
56
- }, [pluginState.menuState])
57
-
58
- useEffect(() => datasetRegistry.attach(pluginState.mappedDatasets, pluginState.orderedDatasets),
59
- [pluginState.mappedDatasets, pluginState.orderedDatasets])
66
+ datasetRegistry.attach(pluginState.mappedDatasets, pluginState.orderedDatasets)
67
+ eventBus.emit('datasets:changed')
68
+ },
69
+ [pluginState.mappedDatasets, pluginState.orderedDatasets])
60
70
 
61
71
  useEffect(() => {
62
72
  datasetRegistry.attachMapStyle(mapState.mapStyle)
@@ -0,0 +1,262 @@
1
+ import { render, act } from '@testing-library/react'
2
+ import { DatasetsInit } from './DatasetsInit.jsx'
3
+ import { datasetRegistry } from '../registry/datasetRegistry.js'
4
+ import { attachGlobalState } from '../registry/globalDataset.js'
5
+ import { loadLayerAdapter, layerAdapter } from '../adapters/loadLayerAdapter.js'
6
+ import { initialiseDatasets } from './initialiseDatasets.js'
7
+ import { EVENTS } from '../../../../src/config/events.js'
8
+
9
+ jest.mock('../registry/datasetRegistry.js', () => ({
10
+ datasetRegistry: {
11
+ attach: jest.fn(),
12
+ attachMapStyle: jest.fn(),
13
+ attachCreateDataset: jest.fn()
14
+ }
15
+ }))
16
+ jest.mock('../registry/globalDataset.js', () => ({
17
+ attachGlobalState: jest.fn()
18
+ }))
19
+ jest.mock('../adapters/loadLayerAdapter.js', () => ({
20
+ loadLayerAdapter: jest.fn(),
21
+ layerAdapter: {}
22
+ }))
23
+ jest.mock('./initialiseDatasets.js')
24
+
25
+ const makeEventBus = () => ({
26
+ emit: jest.fn(),
27
+ on: jest.fn(),
28
+ off: jest.fn()
29
+ })
30
+
31
+ const makeAdapter = () => ({
32
+ init: jest.fn().mockResolvedValue(undefined)
33
+ })
34
+
35
+ const makePluginState = (overrides = {}) => ({
36
+ dispatch: jest.fn(),
37
+ mappedDatasets: {},
38
+ orderedDatasets: [],
39
+ globals: { visible: true, opacity: 1 },
40
+ actionsArray: [],
41
+ ...overrides
42
+ })
43
+
44
+ const makeProps = (overrides = {}) => ({
45
+ pluginConfig: {
46
+ datasets: [{ id: 'roads', label: 'Roads', showInMenu: true }]
47
+ },
48
+ pluginState: makePluginState(),
49
+ appState: { mode: 'default' },
50
+ mapState: { mapStyle: { id: 'outdoor' } },
51
+ mapProvider: { isBaseMapReady: jest.fn().mockReturnValue(true) },
52
+ services: {
53
+ eventBus: makeEventBus(),
54
+ symbolRegistry: {},
55
+ patternRegistry: {}
56
+ },
57
+ ...overrides
58
+ })
59
+
60
+ describe('DatasetsInit', () => {
61
+ let adapter
62
+
63
+ beforeEach(() => {
64
+ jest.clearAllMocks()
65
+ adapter = makeAdapter()
66
+ loadLayerAdapter.mockResolvedValue(adapter)
67
+ initialiseDatasets.mockReturnValue({ remove: jest.fn() })
68
+ })
69
+
70
+ it('renders null', () => {
71
+ const { container } = render(<DatasetsInit {...makeProps()} />)
72
+ expect(container.firstChild).toBeNull()
73
+ })
74
+
75
+ // ─── hasMenu / panel removal ─────────────────────────────────────────────────
76
+
77
+ describe('menu panel management', () => {
78
+ it('removes the datasetsLayers panel when no datasets have showInMenu', async () => {
79
+ const props = makeProps({
80
+ pluginConfig: { datasets: [{ id: 'roads', label: 'Roads' }] }
81
+ })
82
+ await render(<DatasetsInit {...props} />)
83
+ expect(props.services.eventBus.emit).toHaveBeenCalledWith(
84
+ EVENTS.APP_REMOVE_PANEL, 'datasetsLayers'
85
+ )
86
+ expect(props.services.eventBus.emit).toHaveBeenCalledWith(
87
+ EVENTS.APP_TOGGLE_BUTTON_STATE, { id: 'datasetsLayers', prop: 'hidden', value: true }
88
+ )
89
+ })
90
+
91
+ it('does not remove the panel when at least one dataset has showInMenu', async () => {
92
+ const props = makeProps()
93
+ await render(<DatasetsInit {...props} />)
94
+ expect(props.services.eventBus.emit).not.toHaveBeenCalledWith(
95
+ EVENTS.APP_REMOVE_PANEL, 'datasetsLayers'
96
+ )
97
+ })
98
+
99
+ it('removes the panel when hasMenu is explicitly false', async () => {
100
+ const props = makeProps({
101
+ pluginConfig: { hasMenu: false, datasets: [{ id: 'roads', label: 'Roads', showInMenu: true }] }
102
+ })
103
+ await render(<DatasetsInit {...props} />)
104
+ expect(props.services.eventBus.emit).toHaveBeenCalledWith(
105
+ EVENTS.APP_REMOVE_PANEL, 'datasetsLayers'
106
+ )
107
+ })
108
+
109
+ it('includes datasets with sublayer showInMenu when checking for menu items', async () => {
110
+ const props = makeProps({
111
+ pluginConfig: {
112
+ datasets: [{
113
+ id: 'parent',
114
+ label: 'Parent',
115
+ sublayers: [{ id: 'child', showInMenu: true }]
116
+ }]
117
+ }
118
+ })
119
+ await render(<DatasetsInit {...props} />)
120
+ expect(props.services.eventBus.emit).not.toHaveBeenCalledWith(
121
+ EVENTS.APP_REMOVE_PANEL, 'datasetsLayers'
122
+ )
123
+ })
124
+ })
125
+
126
+ // ─── initialisation ──────────────────────────────────────────────────────────
127
+
128
+ describe('initialisation', () => {
129
+ it('calls loadLayerAdapter and initialiseDatasets when base map is ready', async () => {
130
+ const props = makeProps()
131
+ await render(<DatasetsInit {...props} />)
132
+ expect(loadLayerAdapter).toHaveBeenCalledWith(
133
+ props.mapProvider, props.services.symbolRegistry, props.services.patternRegistry
134
+ )
135
+ expect(initialiseDatasets).toHaveBeenCalled()
136
+ })
137
+
138
+ it('does not initialise when base map is not ready', async () => {
139
+ const props = makeProps({
140
+ mapProvider: { isBaseMapReady: jest.fn().mockReturnValue(false) }
141
+ })
142
+ await render(<DatasetsInit {...props} />)
143
+ expect(loadLayerAdapter).not.toHaveBeenCalled()
144
+ })
145
+
146
+ it('does not initialise when mode is not in includeModes', async () => {
147
+ const props = makeProps({
148
+ pluginConfig: { datasets: [], includeModes: ['edit'] },
149
+ appState: { mode: 'default' }
150
+ })
151
+ await render(<DatasetsInit {...props} />)
152
+ expect(loadLayerAdapter).not.toHaveBeenCalled()
153
+ })
154
+
155
+ it('does not initialise when mode is in excludeModes', async () => {
156
+ const props = makeProps({
157
+ pluginConfig: { datasets: [], excludeModes: ['default'] },
158
+ appState: { mode: 'default' }
159
+ })
160
+ await render(<DatasetsInit {...props} />)
161
+ expect(loadLayerAdapter).not.toHaveBeenCalled()
162
+ })
163
+
164
+ it('does not initialise twice when re-rendered', async () => {
165
+ const props = makeProps()
166
+ const { rerender } = render(<DatasetsInit {...props} />)
167
+ await act(async () => {})
168
+ rerender(<DatasetsInit {...props} mapState={{ mapStyle: { id: 'dark' } }} />)
169
+ await act(async () => {})
170
+ expect(loadLayerAdapter).toHaveBeenCalledTimes(1)
171
+ })
172
+
173
+ it('skips init when datasetsInstanceRef is already populated (mode change)', async () => {
174
+ const props = makeProps()
175
+ const { rerender } = render(<DatasetsInit {...props} />)
176
+ await act(async () => {})
177
+ // Change appState.mode — that's in the effect deps, so the effect re-runs
178
+ await act(async () => {
179
+ rerender(<DatasetsInit {...props} appState={{ mode: 'edit' }} />)
180
+ })
181
+ // loadLayerAdapter should only be called once despite the re-run
182
+ expect(loadLayerAdapter).toHaveBeenCalledTimes(1)
183
+ })
184
+
185
+ it('calls remove on cleanup when unmounted after init', async () => {
186
+ const removeFn = jest.fn()
187
+ initialiseDatasets.mockReturnValue({ remove: removeFn })
188
+ const props = makeProps()
189
+ const { unmount } = render(<DatasetsInit {...props} />)
190
+ await act(async () => {})
191
+ unmount()
192
+ expect(removeFn).toHaveBeenCalled()
193
+ })
194
+ })
195
+
196
+ // ─── registry / map style effects ────────────────────────────────────────────
197
+
198
+ describe('datasetRegistry attach effects', () => {
199
+ it('attaches mappedDatasets and orderedDatasets to the registry', async () => {
200
+ const mappedDatasets = { roads: { id: 'roads' } }
201
+ const orderedDatasets = ['roads']
202
+ const props = makeProps({
203
+ pluginState: makePluginState({ mappedDatasets, orderedDatasets })
204
+ })
205
+ await render(<DatasetsInit {...props} />)
206
+ expect(datasetRegistry.attach).toHaveBeenCalledWith(mappedDatasets, orderedDatasets)
207
+ })
208
+
209
+ it('attaches the map style to the registry', async () => {
210
+ const props = makeProps()
211
+ await render(<DatasetsInit {...props} />)
212
+ expect(datasetRegistry.attachMapStyle).toHaveBeenCalledWith(props.mapState.mapStyle)
213
+ })
214
+
215
+ it('calls layerAdapter.onMapStyleChange when available', async () => {
216
+ const onMapStyleChange = jest.fn()
217
+ Object.assign(layerAdapter, { onMapStyleChange })
218
+ const props = makeProps()
219
+ await render(<DatasetsInit {...props} />)
220
+ expect(onMapStyleChange).toHaveBeenCalled()
221
+ delete layerAdapter.onMapStyleChange
222
+ })
223
+ })
224
+
225
+ // ─── globals effect ──────────────────────────────────────────────────────────
226
+
227
+ describe('globals effect', () => {
228
+ it('calls attachGlobalState with the current globals', async () => {
229
+ const globals = { visible: false, opacity: 0.5 }
230
+ const props = makeProps({ pluginState: makePluginState({ globals }) })
231
+ await render(<DatasetsInit {...props} />)
232
+ expect(attachGlobalState).toHaveBeenCalledWith(globals)
233
+ })
234
+ })
235
+
236
+ // ─── actionsArray effect ─────────────────────────────────────────────────────
237
+
238
+ describe('actionsArray effect', () => {
239
+ it('calls each queued adapter method and dispatches REMOVE_ADAPTER_ACTIONS', async () => {
240
+ const applyStyle = jest.fn()
241
+ Object.assign(layerAdapter, { applyStyle })
242
+ const actions = [{ method: 'applyStyle', parameters: ['roads', {}] }]
243
+ const dispatch = jest.fn()
244
+ const props = makeProps({
245
+ pluginState: makePluginState({ actionsArray: actions, dispatch })
246
+ })
247
+ await render(<DatasetsInit {...props} />)
248
+ expect(applyStyle).toHaveBeenCalledWith('roads', {})
249
+ expect(dispatch).toHaveBeenCalledWith({ type: 'REMOVE_ADAPTER_ACTIONS', payload: actions })
250
+ delete layerAdapter.applyStyle
251
+ })
252
+
253
+ it('skips dispatching when actionsArray is empty', async () => {
254
+ const dispatch = jest.fn()
255
+ const props = makeProps({ pluginState: makePluginState({ actionsArray: [], dispatch }) })
256
+ await render(<DatasetsInit {...props} />)
257
+ expect(dispatch).not.toHaveBeenCalledWith(
258
+ expect.objectContaining({ type: 'REMOVE_ADAPTER_ACTIONS' })
259
+ )
260
+ })
261
+ })
262
+ })
@@ -3,8 +3,7 @@ import { createDynamicSource } from '../fetch/createDynamicSource.js'
3
3
  import { applyDatasetDefaults, datasetDefaults } from './defaults.js'
4
4
  import { mappedDatasetsReducer } from '../reducers/mappedDatasetsReducer.js'
5
5
  import { datasetRegistry } from '../registry/datasetRegistry.js'
6
- import { setMenuState } from '../registry/isVisibleWhen.js'
7
- import { buildMenuState } from '../reducers/menuStateReducer.js'
6
+ import { attachMenuStateRef } from '../registry/isVisibleWhen.js'
8
7
  import { datasetsToMenu } from '../reducers/datasetsToMenu.js'
9
8
 
10
9
  export const initialiseDatasets = ({
@@ -30,8 +29,10 @@ export const initialiseDatasets = ({
30
29
  datasetRegistry.attachCreateDataset(adapter.createDataset)
31
30
  }
32
31
  datasetRegistry.attach(mappedDatasets, orderedDatasets, mapStyle)
33
- const menu = pluginConfig.menu || datasetsToMenu({ datasets: processedDatasets })
34
- setMenuState(buildMenuState(menu)) // Must be called before adapter.init so that menuState is set before any datasets are checked for visibility
32
+ const menu = datasetsToMenu({ datasets: processedDatasets })
33
+
34
+ eventBus.requestOnce('menu:state', attachMenuStateRef) // Request the menu state from the menu plugin
35
+
35
36
  dispatch({ type: 'SET_MENU', payload: { menu } })
36
37
  dispatch({ type: 'SET_DATASETS', payload: { mappedDatasets, orderedDatasets } })
37
38
 
@@ -58,9 +59,20 @@ export const initialiseDatasets = ({
58
59
 
59
60
  eventBus.on(events.MAP_SIZE_CHANGE, onMapSizeChange)
60
61
 
62
+ // Add a listener for the datasets:registry event, which will be emitted
63
+ // whenever requested by other plugins, e.g. the menu and the mapKey plugin,
64
+ // so they can access the dataset registry.
65
+ const removeRegistryListener = eventBus.emitWhenRequested('datasets:registry', datasetRegistry)
66
+
67
+ eventBus.on('menu:changed', (menuState) => {
68
+ datasetRegistry.invalidateKeyItemsOnMenuStateChange(menuState)
69
+ adapter.applyGlobalVisibility()
70
+ })
71
+
61
72
  return {
62
73
  remove () {
63
74
  eventBus.off(events.MAP_SIZE_CHANGE, onMapSizeChange)
75
+ removeRegistryListener()
64
76
 
65
77
  // Clean up dynamic sources
66
78
  dynamicSources.forEach(source => source.destroy())
@@ -8,6 +8,7 @@ jest.mock('../registry/datasetRegistry.js', () => ({
8
8
  attach: jest.fn(),
9
9
  attachCreateDataset: jest.fn(),
10
10
  forEachDataset: jest.fn(),
11
+ invalidateKeyItemsOnMenuStateChange: jest.fn(),
11
12
  _invalidateCache: jest.fn()
12
13
  }
13
14
  }))
@@ -20,6 +21,7 @@ const makeAdapter = (overrides = {}) => ({
20
21
  onMapStyleChange: jest.fn(),
21
22
  onMapSizeChange: jest.fn(),
22
23
  setData: jest.fn(),
24
+ applyGlobalVisibility: jest.fn(),
23
25
  destroy: jest.fn(),
24
26
  createDataset: jest.fn(),
25
27
  ...overrides
@@ -29,6 +31,8 @@ const makeEventBus = () => {
29
31
  const handlers = {}
30
32
  return {
31
33
  on: jest.fn((event, handler) => { handlers[event] = handler }),
34
+ requestOnce: jest.fn((event, handler) => { handlers[event] = handler }),
35
+ emitWhenRequested: jest.fn(() => () => {}),
32
36
  off: jest.fn(),
33
37
  emit: jest.fn(),
34
38
  _handlers: handlers
@@ -253,3 +257,41 @@ describe('returned API', () => {
253
257
  expect(() => instance.clearDatasetCache('unknown')).not.toThrow()
254
258
  })
255
259
  })
260
+
261
+ // ─── onUpdate callback ────────────────────────────────────────────────────────
262
+
263
+ describe('onUpdate callback', () => {
264
+ it('calls adapter.setData when the onUpdate callback fires', async () => {
265
+ const mockSource = { destroy: jest.fn(), refresh: jest.fn(), clear: jest.fn(), getFeatureCount: jest.fn() }
266
+ let capturedOnUpdate
267
+ createDynamicSource.mockImplementation(({ onUpdate }) => {
268
+ capturedOnUpdate = onUpdate
269
+ return mockSource
270
+ })
271
+ const mockDataset = { id: 'parcels', hasDynamicGeoJSON: true, dynamicGeoJSON: {} }
272
+ datasetRegistry.forEachDataset.mockImplementation(cb => cb(mockDataset))
273
+
274
+ const args = makeArgs()
275
+ initialiseDatasets(args)
276
+ await Promise.resolve()
277
+
278
+ const geojson = { type: 'FeatureCollection', features: [] }
279
+ capturedOnUpdate('parcels', geojson)
280
+ expect(args.adapter.setData).toHaveBeenCalledWith('parcels', geojson)
281
+ })
282
+ })
283
+
284
+ // ─── menu:changed handler ─────────────────────────────────────────────────────
285
+
286
+ describe('menu:changed handler', () => {
287
+ it('calls invalidateKeyItemsOnMenuStateChange and applyGlobalVisibility', () => {
288
+ const args = makeArgs()
289
+ initialiseDatasets(args)
290
+
291
+ const menuState = { items: [{ id: 'roads', visible: true }] }
292
+ args.eventBus._handlers['menu:changed'](menuState)
293
+
294
+ expect(datasetRegistry.invalidateKeyItemsOnMenuStateChange).toHaveBeenCalledWith(menuState)
295
+ expect(args.adapter.applyGlobalVisibility).toHaveBeenCalled()
296
+ })
297
+ })
@@ -2,7 +2,6 @@
2
2
  import { initialState, actions } from './reducers/pluginState.js'
3
3
  import { DatasetsInit } from './initialise/DatasetsInit.jsx'
4
4
  import { LayersMenu } from './components/LayersMenu/LayersMenu.jsx'
5
- import { Key } from './components/Key/Key.jsx'
6
5
  import { addDataset } from './api/addDataset.js'
7
6
  import { removeDataset } from './api/removeDataset.js'
8
7
  import { setDatasetVisibility } from './api/setDatasetVisibility.js'
@@ -44,22 +43,6 @@ export const manifest = {
44
43
  width: '280px'
45
44
  },
46
45
  render: LayersMenu
47
- }, {
48
- id: 'datasetsKey',
49
- label: 'Key',
50
- mobile: {
51
- slot: 'drawer',
52
- modal: true
53
- },
54
- tablet: {
55
- slot: 'left-top',
56
- width: '260px'
57
- },
58
- desktop: {
59
- slot: 'left-top',
60
- width: '280px'
61
- },
62
- render: Key
63
46
  }],
64
47
 
65
48
  buttons: [{
@@ -67,9 +50,6 @@ export const manifest = {
67
50
  label: 'Layers',
68
51
  panelId: 'datasetsLayers',
69
52
  iconId: 'layers',
70
- excludeWhen: ({ pluginConfig }) => !pluginConfig.menu && !pluginConfig.datasets.some(l =>
71
- l.showInMenu || l.sublayers?.some(r => r.showInMenu)
72
- ),
73
53
  mobile: {
74
54
  slot: 'top-left',
75
55
  showLabel: true
@@ -82,24 +62,6 @@ export const manifest = {
82
62
  slot: 'top-left',
83
63
  showLabel: true
84
64
  }
85
- }, {
86
- id: 'datasetsKey',
87
- label: 'Key',
88
- panelId: 'datasetsKey',
89
- iconId: 'key',
90
- excludeWhen: ({ pluginConfig }) => !pluginConfig.datasets.some(l => l.showInKey),
91
- mobile: {
92
- slot: 'top-left',
93
- showLabel: false
94
- },
95
- tablet: {
96
- slot: 'top-left',
97
- showLabel: true
98
- },
99
- desktop: {
100
- slot: 'top-left',
101
- showLabel: true
102
- }
103
65
  }],
104
66
 
105
67
  icons: [{
@@ -0,0 +1,49 @@
1
+ import { manifest } from './manifest.js'
2
+
3
+ jest.mock('./reducers/pluginState.js', () => ({ initialState: {}, actions: {} }))
4
+ jest.mock('./initialise/DatasetsInit.jsx', () => ({ DatasetsInit: jest.fn() }))
5
+ jest.mock('./components/LayersMenu/LayersMenu.jsx', () => ({ LayersMenu: jest.fn() }))
6
+ jest.mock('./api/addDataset.js', () => ({ addDataset: jest.fn() }))
7
+ jest.mock('./api/removeDataset.js', () => ({ removeDataset: jest.fn() }))
8
+ jest.mock('./api/setDatasetVisibility.js', () => ({ setDatasetVisibility: jest.fn() }))
9
+ jest.mock('./api/setFeatureVisibility.js', () => ({ setFeatureVisibility: jest.fn() }))
10
+ jest.mock('./api/setStyle.js', () => ({ setStyle: jest.fn() }))
11
+ jest.mock('./api/getStyle.js', () => ({ getStyle: jest.fn() }))
12
+ jest.mock('./api/setOpacity.js', () => ({ setOpacity: jest.fn() }))
13
+ jest.mock('./api/getOpacity.js', () => ({ getOpacity: jest.fn() }))
14
+ jest.mock('./api/setData.js', () => ({ setData: jest.fn() }))
15
+ jest.mock('./api/setGlobals.js', () => ({ setGlobals: jest.fn() }))
16
+
17
+ describe('manifest', () => {
18
+ it('exports a manifest object', () => {
19
+ expect(manifest).toBeDefined()
20
+ expect(typeof manifest).toBe('object')
21
+ })
22
+
23
+ it('has an InitComponent', () => {
24
+ expect(manifest.InitComponent).toBeDefined()
25
+ })
26
+
27
+ it('has a reducer with initialState and actions', () => {
28
+ expect(manifest.reducer).toBeDefined()
29
+ expect(manifest.reducer.initialState).toBeDefined()
30
+ expect(manifest.reducer.actions).toBeDefined()
31
+ })
32
+
33
+ it('has panels with a datasetsLayers panel', () => {
34
+ expect(Array.isArray(manifest.panels)).toBe(true)
35
+ expect(manifest.panels.find(p => p.id === 'datasetsLayers')).toBeDefined()
36
+ })
37
+
38
+ it('has a buttons array', () => {
39
+ expect(Array.isArray(manifest.buttons)).toBe(true)
40
+ })
41
+
42
+ it('has an api with all expected methods', () => {
43
+ expect(manifest.api.addDataset).toBeDefined()
44
+ expect(manifest.api.removeDataset).toBeDefined()
45
+ expect(manifest.api.setDatasetVisibility).toBeDefined()
46
+ expect(manifest.api.setOpacity).toBeDefined()
47
+ expect(manifest.api.getOpacity).toBeDefined()
48
+ })
49
+ })