@defra/interactive-map 0.0.15-alpha → 0.0.17-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 (263) hide show
  1. package/assets/css/docusaurus.css +104 -0
  2. package/assets/images/favicon.svg +1 -0
  3. package/assets/images/hero.png +0 -0
  4. package/assets/images/slot-map.svg +264 -0
  5. package/dist/css/index.css +1 -1
  6. package/dist/esm/im-core.js +1 -1
  7. package/dist/esm/im-shell.js +1 -1
  8. package/dist/umd/im-core.js +1 -1
  9. package/dist/umd/index.js +1 -1
  10. package/docs/api/slots.md +90 -6
  11. package/docs/api.md +4 -4
  12. package/docs/architecture.md +3 -1
  13. package/docs/{demo.mdx → examples.mdx} +1 -1
  14. package/docs/getting-started.md +5 -4
  15. package/docs/index.mdx +42 -0
  16. package/docs/plugins/datasets.md +561 -0
  17. package/docs/plugins/interact.md +176 -55
  18. package/docs/plugins/map-styles.md +64 -7
  19. package/docs/plugins/search.md +207 -63
  20. package/docs/plugins.md +8 -16
  21. package/docusaurus.config.cjs +34 -34
  22. package/jest.setup.js +1 -1
  23. package/package.json +6 -5
  24. package/plugins/beta/datasets/dist/css/index.css +85 -15
  25. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  26. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  27. package/plugins/beta/datasets/dist/umd/index.js +1 -1
  28. package/plugins/beta/datasets/src/DatasetsInit.jsx +24 -9
  29. package/plugins/beta/datasets/src/adapters/maplibre/index.js +18 -0
  30. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +113 -0
  31. package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +69 -0
  32. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +338 -0
  33. package/plugins/beta/datasets/src/adapters/maplibre/patternRegistry.js +48 -0
  34. package/plugins/beta/datasets/src/api/addDataset.js +3 -9
  35. package/plugins/beta/datasets/src/api/getOpacity.js +17 -0
  36. package/plugins/beta/datasets/src/api/getStyle.js +13 -0
  37. package/plugins/beta/datasets/src/api/removeDataset.js +3 -45
  38. package/plugins/beta/datasets/src/api/setData.js +8 -0
  39. package/plugins/beta/datasets/src/api/setDatasetVisibility.js +37 -0
  40. package/plugins/beta/datasets/src/api/setFeatureVisibility.js +22 -0
  41. package/plugins/beta/datasets/src/api/setOpacity.js +29 -0
  42. package/plugins/beta/datasets/src/api/setStyle.js +22 -0
  43. package/plugins/beta/datasets/src/datasets.js +33 -59
  44. package/plugins/beta/datasets/src/defaults.js +43 -9
  45. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +39 -30
  46. package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +2 -2
  47. package/plugins/beta/datasets/src/manifest.js +27 -19
  48. package/plugins/beta/datasets/src/panels/Key.jsx +129 -49
  49. package/plugins/beta/datasets/src/panels/Key.module.scss +48 -9
  50. package/plugins/beta/datasets/src/panels/Layers.jsx +131 -29
  51. package/plugins/beta/datasets/src/panels/Layers.module.scss +50 -8
  52. package/plugins/beta/datasets/src/reducer.js +128 -9
  53. package/plugins/beta/datasets/src/styles/patterns.js +157 -0
  54. package/plugins/beta/datasets/src/utils/bbox.js +8 -6
  55. package/plugins/beta/datasets/src/utils/filters.js +5 -2
  56. package/plugins/beta/datasets/src/utils/mergeSublayer.js +78 -0
  57. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  58. package/plugins/beta/draw-es/src/DrawInit.jsx +16 -16
  59. package/plugins/beta/draw-es/src/api/addFeature.js +3 -3
  60. package/plugins/beta/draw-es/src/api/deleteFeature.js +3 -3
  61. package/plugins/beta/draw-es/src/api/editFeature.js +3 -3
  62. package/plugins/beta/draw-es/src/api/newPolygon.js +3 -3
  63. package/plugins/beta/draw-es/src/events.js +52 -20
  64. package/plugins/beta/draw-es/src/events.test.js +301 -0
  65. package/plugins/beta/draw-es/src/graphic.js +1 -1
  66. package/plugins/beta/draw-es/src/manifest.js +4 -4
  67. package/plugins/beta/draw-es/src/reducer.js +1 -1
  68. package/plugins/beta/draw-es/src/sketchViewModel.js +1 -1
  69. package/plugins/beta/draw-ml/dist/css/index.css +1 -1
  70. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  71. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  72. package/plugins/beta/draw-ml/src/DrawInit.jsx +49 -52
  73. package/plugins/beta/draw-ml/src/api/deleteFeature.js +1 -1
  74. package/plugins/beta/draw-ml/src/api/editFeature.js +8 -5
  75. package/plugins/beta/draw-ml/src/api/newLine.js +0 -1
  76. package/plugins/beta/draw-ml/src/api/newPolygon.js +0 -1
  77. package/plugins/beta/draw-ml/src/api/split.js +4 -4
  78. package/plugins/beta/draw-ml/src/defaults.js +1 -1
  79. package/plugins/beta/draw-ml/src/draw.scss +0 -7
  80. package/plugins/beta/draw-ml/src/events.js +8 -6
  81. package/plugins/beta/draw-ml/src/manifest.js +29 -29
  82. package/plugins/beta/draw-ml/src/mapboxDraw.js +1 -1
  83. package/plugins/beta/draw-ml/src/mapboxSnap.js +17 -18
  84. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +31 -31
  85. package/plugins/beta/draw-ml/src/modes/disabledMode.js +1 -1
  86. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +11 -11
  87. package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +7 -7
  88. package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +8 -8
  89. package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +7 -7
  90. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +32 -24
  91. package/plugins/beta/draw-ml/src/reducer.js +1 -1
  92. package/plugins/beta/draw-ml/src/undoStack.js +4 -4
  93. package/plugins/beta/draw-ml/src/utils/snapHelpers.js +12 -12
  94. package/plugins/beta/draw-ml/src/utils/spatial.js +11 -11
  95. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
  96. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
  97. package/plugins/beta/frame/src/Frame.jsx +9 -9
  98. package/plugins/beta/frame/src/FrameInit.jsx +4 -4
  99. package/plugins/beta/frame/src/api/addFrame.js +1 -1
  100. package/plugins/beta/frame/src/api/editFeature.js +1 -1
  101. package/plugins/beta/frame/src/config.js +1 -1
  102. package/plugins/beta/frame/src/manifest.js +3 -3
  103. package/plugins/beta/frame/src/reducer.js +1 -1
  104. package/plugins/beta/frame/src/utils.js +1 -1
  105. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  106. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  107. package/plugins/beta/map-styles/src/MapStyles.jsx +18 -18
  108. package/plugins/beta/map-styles/src/manifest.js +1 -1
  109. package/plugins/beta/scale-bar/dist/css/index.css +1 -1
  110. package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -1
  111. package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -1
  112. package/plugins/beta/scale-bar/src/ScaleBar.jsx +5 -5
  113. package/plugins/beta/scale-bar/src/index.test.js +3 -3
  114. package/plugins/beta/scale-bar/src/manifest.js +3 -3
  115. package/plugins/beta/scale-bar/src/scaleBar.scss +2 -1
  116. package/plugins/beta/use-location/src/UseLocation.jsx +1 -1
  117. package/plugins/beta/use-location/src/defaults.js +1 -1
  118. package/plugins/beta/use-location/src/events.js +3 -3
  119. package/plugins/interact/dist/css/index.css +1 -1
  120. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  121. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  122. package/plugins/interact/src/InteractInit.jsx +1 -2
  123. package/plugins/interact/src/api/enable.js +8 -5
  124. package/plugins/interact/src/api/enable.test.js +2 -2
  125. package/plugins/interact/src/api/selectFeature.js +4 -4
  126. package/plugins/interact/src/api/unselectFeature.js +5 -5
  127. package/plugins/interact/src/defaults.js +0 -1
  128. package/plugins/interact/src/events.test.js +15 -15
  129. package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
  130. package/plugins/interact/src/hooks/useInteractionHandlers.js +2 -2
  131. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +5 -5
  132. package/plugins/interact/src/interact.scss +0 -7
  133. package/plugins/interact/src/manifest.js +15 -19
  134. package/plugins/interact/src/manifest.test.js +6 -5
  135. package/plugins/interact/src/reducer.js +3 -3
  136. package/plugins/interact/src/reducer.test.js +0 -1
  137. package/plugins/interact/src/utils/spatial.js +10 -10
  138. package/plugins/interact/src/utils/spatial.test.js +14 -14
  139. package/plugins/search/dist/css/index.css +1 -1
  140. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  141. package/plugins/search/dist/esm/index.js +1 -1
  142. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  143. package/plugins/search/dist/umd/index.js +1 -1
  144. package/plugins/search/src/Search.jsx +7 -6
  145. package/plugins/search/src/Search.test.jsx +23 -23
  146. package/plugins/search/src/components/CloseButton/CloseButton.jsx +15 -15
  147. package/plugins/search/src/components/CloseButton/CloseButton.test.jsx +2 -2
  148. package/plugins/search/src/components/Form/Form.jsx +14 -14
  149. package/plugins/search/src/components/Form/Form.module.scss +2 -1
  150. package/plugins/search/src/components/Form/Form.test.jsx +11 -11
  151. package/plugins/search/src/components/OpenButton/OpenButton.jsx +16 -15
  152. package/plugins/search/src/components/OpenButton/OpenButton.test.jsx +6 -2
  153. package/plugins/search/src/components/SubmitButton/SubmitButton.jsx +15 -15
  154. package/plugins/search/src/components/Suggestions/Suggestions.jsx +6 -6
  155. package/plugins/search/src/components/Suggestions/Suggestions.test.jsx +4 -4
  156. package/plugins/search/src/datasets.js +12 -13
  157. package/plugins/search/src/datasets.test.js +1 -1
  158. package/plugins/search/src/defaults.js +1 -1
  159. package/plugins/search/src/events/fetchSuggestions.js +3 -3
  160. package/plugins/search/src/events/fetchSuggestions.test.js +1 -1
  161. package/plugins/search/src/events/formHandlers.js +3 -3
  162. package/plugins/search/src/events/formHandlers.test.js +1 -1
  163. package/plugins/search/src/events/index.js +2 -2
  164. package/plugins/search/src/events/index.test.js +2 -2
  165. package/plugins/search/src/events/inputHandlers.js +4 -4
  166. package/plugins/search/src/events/inputHandlers.test.js +1 -1
  167. package/plugins/search/src/events/suggestionHandlers.js +2 -2
  168. package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
  169. package/plugins/search/src/index.js +2 -1
  170. package/plugins/search/src/index.test.js +3 -3
  171. package/plugins/search/src/manifest.js +6 -4
  172. package/plugins/search/src/reducer.js +1 -2
  173. package/plugins/search/src/reducer.test.js +2 -2
  174. package/plugins/search/src/search.scss +10 -3
  175. package/plugins/search/src/utils/parseOsNamesResults.js +1 -2
  176. package/plugins/search/src/utils/parseOsNamesResults.test.js +2 -2
  177. package/plugins/search/src/utils/updateMap.js +1 -1
  178. package/plugins/search/src/utils/updateMap.test.js +5 -5
  179. package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
  180. package/providers/beta/esri/src/esriProvider.js +5 -5
  181. package/providers/beta/esri/src/utils/coords.js +1 -1
  182. package/providers/beta/esri/src/utils/esriFixes.js +1 -1
  183. package/providers/beta/esri/src/utils/query.js +4 -4
  184. package/providers/beta/esri/src/utils/spatial.js +1 -2
  185. package/providers/beta/esri/src/utils/spatial.test.js +4 -1
  186. package/providers/beta/open-names/src/utils/mapToLocationModel.test.js +1 -1
  187. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  188. package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
  189. package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
  190. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  191. package/providers/maplibre/src/appEvents.test.js +1 -1
  192. package/providers/maplibre/src/index.js +1 -1
  193. package/providers/maplibre/src/index.test.js +3 -5
  194. package/providers/maplibre/src/mapEvents.test.js +15 -5
  195. package/providers/maplibre/src/maplibreProvider.test.js +6 -2
  196. package/providers/maplibre/src/utils/calculateLinearTextSize.js +4 -4
  197. package/providers/maplibre/src/utils/calculateLinearTextSize.test.js +3 -3
  198. package/providers/maplibre/src/utils/detectWebgl.test.js +1 -1
  199. package/providers/maplibre/src/utils/highlightFeatures.js +3 -2
  200. package/providers/maplibre/src/utils/highlightFeatures.test.js +13 -6
  201. package/providers/maplibre/src/utils/labels.js +19 -20
  202. package/providers/maplibre/src/utils/labels.test.js +15 -13
  203. package/providers/maplibre/src/utils/maplibreFixes.test.js +1 -1
  204. package/providers/maplibre/src/utils/queryFeatures.js +6 -6
  205. package/providers/maplibre/src/utils/queryFeatures.test.js +13 -13
  206. package/providers/maplibre/src/utils/spatial.js +0 -1
  207. package/providers/maplibre/src/utils/spatial.test.js +26 -27
  208. package/src/App/components/Actions/Actions.jsx +2 -2
  209. package/src/App/components/Actions/Actions.module.scss +0 -7
  210. package/src/App/components/Actions/Actions.test.jsx +1 -1
  211. package/src/App/components/Icon/Icon.jsx +3 -2
  212. package/src/App/components/Icon/Icon.module.scss +4 -0
  213. package/src/App/components/Icon/Icon.test.jsx +43 -4
  214. package/src/App/components/MapButton/MapButton.jsx +42 -17
  215. package/src/App/components/MapButton/MapButton.module.scss +4 -13
  216. package/src/App/components/MapButton/MapButton.test.jsx +27 -3
  217. package/src/App/components/PopupMenu/PopupMenu.jsx +51 -274
  218. package/src/App/components/PopupMenu/PopupMenu.module.scss +14 -7
  219. package/src/App/components/PopupMenu/PopupMenu.test.jsx +70 -1
  220. package/src/App/components/PopupMenu/usePopupMenu.js +258 -0
  221. package/src/App/hooks/useButtonStateEvaluator.js +12 -2
  222. package/src/App/hooks/useButtonStateEvaluator.test.js +38 -4
  223. package/src/App/hooks/useInterfaceAPI.js +6 -0
  224. package/src/App/hooks/useLayoutMeasurements.js +84 -18
  225. package/src/App/hooks/useLayoutMeasurements.test.js +124 -17
  226. package/src/App/layout/Layout.jsx +12 -7
  227. package/src/App/layout/Layout.test.jsx +2 -2
  228. package/src/App/layout/layout.module.scss +67 -29
  229. package/src/App/registry/pluginRegistry.js +17 -0
  230. package/src/App/registry/pluginRegistry.test.js +33 -0
  231. package/src/App/renderer/HtmlElementHost.jsx +2 -1
  232. package/src/App/renderer/HtmlElementHost.test.jsx +7 -7
  233. package/src/App/renderer/mapButtons.js +3 -2
  234. package/src/App/renderer/mapPanels.test.js +2 -2
  235. package/src/App/renderer/slotHelpers.js +2 -2
  236. package/src/App/renderer/slotHelpers.test.js +5 -5
  237. package/src/App/renderer/slots.js +9 -5
  238. package/src/App/store/AppProvider.jsx +3 -1
  239. package/src/App/store/AppProvider.test.jsx +1 -1
  240. package/src/App/store/ServiceProvider.jsx +3 -1
  241. package/src/App/store/appActionsMap.js +16 -0
  242. package/src/App/store/appActionsMap.test.js +27 -0
  243. package/src/App/store/appDispatchMiddleware.js +33 -1
  244. package/src/App/store/appDispatchMiddleware.test.js +250 -222
  245. package/src/App/store/appReducer.js +2 -0
  246. package/src/InteractiveMap/InteractiveMap.js +4 -0
  247. package/src/config/appConfig.js +7 -4
  248. package/src/config/events.js +28 -0
  249. package/src/scss/main.scss +1 -0
  250. package/src/scss/settings/_dimensions.scss +0 -1
  251. package/src/services/logger.js +6 -0
  252. package/src/services/logger.test.js +32 -0
  253. package/src/utils/getSafeZoneInset.js +9 -7
  254. package/src/utils/getSafeZoneInset.test.js +10 -10
  255. package/webpack.dev.mjs +23 -19
  256. package/docs/govuk-prototype.md +0 -23
  257. package/docs/index.md +0 -19
  258. package/plugins/beta/datasets/src/api/hideDataset.js +0 -14
  259. package/plugins/beta/datasets/src/api/hideFeatures.js +0 -41
  260. package/plugins/beta/datasets/src/api/showDataset.js +0 -14
  261. package/plugins/beta/datasets/src/api/showFeatures.js +0 -44
  262. package/plugins/beta/datasets/src/handleSetMapStyle.js +0 -54
  263. package/plugins/beta/datasets/src/mapLayers.js +0 -165
@@ -1,60 +1,140 @@
1
- import React from "react"
1
+ import React from 'react'
2
2
  import { getValueForStyle } from '../../../../../src/utils/getValueForStyle'
3
+ import { hasPattern, getKeyPatternPaths } from '../styles/patterns.js'
4
+ import { mergeSublayer } from '../utils/mergeSublayer.js'
5
+
6
+ const SVG_SIZE = 20
7
+ const SVG_CENTER = SVG_SIZE / 2
8
+ const PATTERN_INSET = 2
9
+
10
+ const buildKeyGroups = (datasets) => {
11
+ const seenGroups = new Set()
12
+ const items = []
13
+ datasets.forEach(dataset => {
14
+ if (dataset.sublayers?.length) {
15
+ items.push({ type: 'sublayers', dataset })
16
+ return
17
+ }
18
+ if (dataset.groupLabel) {
19
+ if (seenGroups.has(dataset.groupLabel)) {
20
+ return
21
+ }
22
+ seenGroups.add(dataset.groupLabel)
23
+ items.push({
24
+ type: 'group',
25
+ groupLabel: dataset.groupLabel,
26
+ datasets: datasets.filter(d => !d.sublayers?.length && d.groupLabel === dataset.groupLabel)
27
+ })
28
+ return
29
+ }
30
+ items.push({ type: 'flat', dataset })
31
+ })
32
+ return items
33
+ }
3
34
 
4
35
  export const Key = ({ mapState, pluginState }) => {
5
36
  const { mapStyle } = mapState
6
37
 
7
- const itemSymbol = (dataset) => (
8
- <svg
9
- xmlns='http://www.w3.org/2000/svg'
10
- width='20'
11
- height='20'
12
- viewBox='0 0 20 20'
13
- aria-hidden='true'
14
- focusable='false'
15
- >
16
- {dataset.keySymbolShape === 'line' ? (
17
- <line
18
- x1={dataset.strokeWidth / 2}
19
- y1="10"
20
- x2={20 - dataset.strokeWidth / 2}
21
- y2="10"
22
- stroke={getValueForStyle(dataset.stroke, mapStyle.id)}
23
- strokeWidth={dataset.strokeWidth}
24
- strokeLinecap="round"
25
- />
26
- ) : (
27
- <rect
28
- x={dataset.strokeWidth / 2}
29
- y={dataset.strokeWidth / 2}
30
- width={20 - dataset.strokeWidth}
31
- height={20 - dataset.strokeWidth}
32
- rx={dataset.strokeWidth}
33
- ry={dataset.strokeWidth}
34
- fill={getValueForStyle(dataset.fill, mapStyle.id)}
35
- stroke={getValueForStyle(dataset.stroke, mapStyle.id)}
36
- strokeWidth={dataset.strokeWidth}
37
- strokeLinejoin="round"
38
- />
39
- )}
40
- </svg>
38
+ const itemSymbol = (config) => {
39
+ const svgProps = {
40
+ xmlns: 'http://www.w3.org/2000/svg',
41
+ width: SVG_SIZE,
42
+ height: SVG_SIZE,
43
+ viewBox: `0 0 ${SVG_SIZE} ${SVG_SIZE}`,
44
+ 'aria-hidden': 'true',
45
+ focusable: 'false'
46
+ }
47
+
48
+ if (hasPattern(config)) {
49
+ const paths = getKeyPatternPaths(config, mapStyle.id)
50
+ return (
51
+ <svg {...svgProps}>
52
+ <g dangerouslySetInnerHTML={{ __html: paths.border }} />
53
+ <g transform={`translate(${PATTERN_INSET}, ${PATTERN_INSET})`} dangerouslySetInnerHTML={{ __html: paths.content }} />
54
+ </svg>
55
+ )
56
+ }
57
+
58
+ return (
59
+ <svg {...svgProps}>
60
+ {config.keySymbolShape === 'line'
61
+ ? (
62
+ <line
63
+ x1={config.strokeWidth / 2}
64
+ y1={SVG_CENTER}
65
+ x2={SVG_SIZE - config.strokeWidth / 2}
66
+ y2={SVG_CENTER}
67
+ stroke={getValueForStyle(config.stroke, mapStyle.id)}
68
+ strokeWidth={config.strokeWidth}
69
+ strokeLinecap='round'
70
+ />
71
+ )
72
+ : (
73
+ <rect
74
+ x={config.strokeWidth / 2}
75
+ y={config.strokeWidth / 2}
76
+ width={SVG_SIZE - config.strokeWidth}
77
+ height={SVG_SIZE - config.strokeWidth}
78
+ rx={config.strokeWidth}
79
+ ry={config.strokeWidth}
80
+ fill={getValueForStyle(config.fill, mapStyle.id)}
81
+ stroke={getValueForStyle(config.stroke, mapStyle.id)}
82
+ strokeWidth={config.strokeWidth}
83
+ strokeLinejoin='round'
84
+ />
85
+ )}
86
+ </svg>
87
+ )
88
+ }
89
+
90
+ const renderEntry = (key, config) => (
91
+ <dl key={key} className='im-c-datasets-key__item'>
92
+ <dt className='im-c-datasets-key__item-symbol'>{itemSymbol(config)}</dt>
93
+ <dd className='im-c-datasets-key__item-label'>
94
+ {config.label}
95
+ {config.symbolDescription && (
96
+ <span className='govuk-visually-hidden'>
97
+ ({getValueForStyle(config.symbolDescription, mapStyle.id)})
98
+ </span>
99
+ )}
100
+ </dd>
101
+ </dl>
41
102
  )
42
103
 
104
+ const visibleDatasets = (pluginState.datasets || [])
105
+ .filter(dataset => dataset.showInKey && dataset.visibility !== 'hidden')
106
+
107
+ const keyGroups = buildKeyGroups(visibleDatasets)
108
+ const hasGroups = keyGroups.some(item => item.type === 'sublayers' || item.type === 'group')
109
+ const containerClass = `im-c-datasets-key${hasGroups ? ' im-c-datasets-key--has-groups' : ''}`
110
+
43
111
  return (
44
- <div className="im-c-datasets-key">
45
- {(pluginState.datasets || []).filter(dataset => dataset.showInKey && dataset.visibility !== 'hidden').map(dataset => (
46
- <div key={dataset.id} className="im-c-datasets-key__item">
47
- <div className="im-c-datasets-key__item-label">
48
- {itemSymbol(dataset)}
49
- {dataset.label}
50
- {dataset.symbolDescription && (
51
- <span className="govuk-visually-hidden">
52
- ({getValueForStyle(dataset.symbolDescription, mapStyle.id)})
53
- </span>
54
- )}
55
- </div>
56
- </div>
57
- ))}
112
+ <div className={containerClass}>
113
+ {keyGroups.map(item => {
114
+ if (item.type === 'sublayers') {
115
+ const headingId = `key-heading-${item.dataset.id}`
116
+ return (
117
+ <section key={item.dataset.id} className='im-c-datasets-key__group' aria-labelledby={headingId}>
118
+ <h3 id={headingId} className='im-c-datasets-key__group-heading'>{item.dataset.label}</h3>
119
+ {item.dataset.sublayers
120
+ .filter(sublayer => item.dataset.sublayerVisibility?.[sublayer.id] !== 'hidden')
121
+ .map(sublayer => renderEntry(`${item.dataset.id}-${sublayer.id}`, mergeSublayer(item.dataset, sublayer)))}
122
+ </section>
123
+ )
124
+ }
125
+
126
+ if (item.type === 'group') {
127
+ const headingId = `key-heading-${item.groupLabel.toLowerCase().replaceAll(/\s+/g, '-')}`
128
+ return (
129
+ <section key={item.groupLabel} className='im-c-datasets-key__group' aria-labelledby={headingId}>
130
+ <h3 id={headingId} className='im-c-datasets-key__group-heading'>{item.groupLabel}</h3>
131
+ {item.datasets.map(dataset => renderEntry(dataset.id, dataset))}
132
+ </section>
133
+ )
134
+ }
135
+
136
+ return renderEntry(item.dataset.id, item.dataset)
137
+ })}
58
138
  </div>
59
139
  )
60
140
  }
@@ -1,19 +1,58 @@
1
- .im-c-datasets-key {
1
+ // When groups are present, every direct child gets a border-top
2
+ .im-c-datasets-key--has-groups > * {
3
+ border-top: 1px solid var(--button-hover-color);
4
+ }
5
+
6
+ // When no groups, only the first child gets a border-top
7
+ .im-c-datasets-key:not(.im-c-datasets-key--has-groups) > *:first-child {
8
+ border-top: 1px solid var(--button-hover-color);
9
+ }
2
10
 
11
+ .im-c-datasets-key__group:not(:last-child) {
12
+ padding-bottom: 5px;
3
13
  }
4
14
 
5
- .im-c-datasets-key__item-label {
15
+ .im-c-datasets-key__group-heading {
16
+ padding-top: 15px;
17
+ padding-bottom: 10px;
18
+ margin: 0;
19
+ font-size: 1rem;
20
+ font-weight: bold;
21
+ color: var(--foreground-color);
22
+ }
23
+
24
+ .im-c-datasets-key__item {
6
25
  display: flex;
7
26
  align-items: start;
8
- padding-top: 12px;
9
- padding-bottom: 12px;
10
- align-self: auto;
27
+ padding-top: 10px;
28
+ padding-bottom: 10px;
11
29
  font-size: 1rem;
12
- line-height: 1.2;
30
+
31
+ // When mixed with groups, flat items match group heading spacing
32
+ .im-c-datasets-key--has-groups > & {
33
+ padding-top: 15px;
34
+ padding-bottom: 15px;
35
+ }
36
+
37
+ // First item inside a group — heading already provides top spacing
38
+ .im-c-datasets-key__group &:first-child {
39
+ padding-top: 0;
40
+ }
41
+ }
42
+
43
+ // No-groups: first item needs 15px below the single border
44
+ .im-c-datasets-key:not(.im-c-datasets-key--has-groups) > .im-c-datasets-key__item:first-child {
45
+ padding-top: 15px;
13
46
  }
14
47
 
15
- .im-c-datasets-key__item-label svg {
48
+ // Last item in all scenarios — flat or inside the last group
49
+ .im-c-datasets-key > .im-c-datasets-key__item:last-child,
50
+ .im-c-datasets-key__group:last-child .im-c-datasets-key__item:last-child {
51
+ padding-bottom: 5px;
52
+ }
53
+
54
+ .im-c-datasets-key__item svg {
16
55
  position: relative;
17
56
  flex-shrink: 0;
18
- margin: 0px 13px 0 2px;
19
- }
57
+ margin: 0 13px 0 2px;
58
+ }
@@ -1,39 +1,141 @@
1
1
  import React from 'react'
2
- import { showDataset } from '../api/showDataset'
3
- import { hideDataset } from '../api/hideDataset'
2
+ import { setDatasetVisibility } from '../api/setDatasetVisibility'
4
3
 
5
- export const Layers = ({ pluginState, mapProvider }) => {
4
+ const CHECKBOX_LABEL_CLASS = 'im-c-datasets-layers__item-label govuk-label govuk-checkboxes__label'
6
5
 
7
- const handleChange = (e) => {
8
- const { value, checked } = e.target
9
- if (checked) {
10
- showDataset({ mapProvider, pluginState }, value)
11
- } else {
12
- hideDataset({ mapProvider, pluginState }, value)
6
+ const hasToggleableSublayers = (dataset) => dataset.sublayers?.some(sublayer => sublayer.toggleVisibility)
7
+
8
+ /**
9
+ * Collapse the filtered dataset list into ordered render items:
10
+ * { type: 'sublayers', dataset } dataset with sublayers (takes precedence)
11
+ * { type: 'group', groupLabel, datasets } — datasets sharing a groupLabel
12
+ * { type: 'flat', dataset } — standalone dataset
13
+ */
14
+ const buildRenderItems = (datasets) => {
15
+ const seenGroups = new Set()
16
+ const items = []
17
+ datasets.forEach(dataset => {
18
+ if (hasToggleableSublayers(dataset)) {
19
+ items.push({ type: 'sublayers', dataset })
20
+ return
13
21
  }
22
+ if (dataset.groupLabel) {
23
+ if (seenGroups.has(dataset.groupLabel)) {
24
+ return
25
+ }
26
+ seenGroups.add(dataset.groupLabel)
27
+ items.push({
28
+ type: 'group',
29
+ groupLabel: dataset.groupLabel,
30
+ datasets: datasets.filter(d => !hasToggleableSublayers(d) && d.groupLabel === dataset.groupLabel)
31
+ })
32
+ return
33
+ }
34
+ items.push({ type: 'flat', dataset })
35
+ })
36
+ return items
37
+ }
38
+
39
+ export const Layers = ({ pluginState }) => {
40
+ const handleDatasetChange = (e) => {
41
+ const { value, checked } = e.target
42
+ setDatasetVisibility({ pluginState }, checked, { datasetId: value })
14
43
  }
15
44
 
16
- return (
17
- <div className="im-c-datasets-layers">
18
- <div className="govuk-form-group">
19
- <fieldset className="govuk-fieldset">
20
- <legend className="govuk-visually-hidden">
21
- Layers
22
- </legend>
23
- <div className="govuk-checkboxes govuk-checkboxes--small" data-module="govuk-checkboxes">
24
- {(pluginState.datasets || []).filter(dataset => dataset.showInLayers).map(dataset => (
25
- <div key={dataset.id} className={`im-c-datasets-layers__item${dataset.visibility !== 'hidden' ? ' im-c-datasets-layers__item--checked' : ''}`}>
26
- <div className="govuk-checkboxes__item">
27
- <input className="govuk-checkboxes__input" id={dataset.id} name="layers" type="checkbox" value={dataset.id} checked={dataset.visibility !== 'hidden'} onChange={handleChange} />
28
- <label className="im-c-datasets-layers__item-label govuk-label govuk-checkboxes__label" htmlFor={dataset.id}>
29
- {dataset.label}
30
- </label>
31
- </div>
32
- </div>
33
- ))}
34
- </div>
35
- </fieldset>
45
+ const handleSublayerChange = (e) => {
46
+ const { checked } = e.target
47
+ const datasetId = e.target.dataset.datasetId
48
+ const sublayerId = e.target.dataset.sublayerId
49
+ setDatasetVisibility({ pluginState }, checked, { datasetId, sublayerId })
50
+ }
51
+
52
+ const renderDatasetItem = (dataset) => {
53
+ const itemClass = `im-c-datasets-layers__item govuk-checkboxes govuk-checkboxes--small${dataset.visibility === 'hidden' ? '' : ' im-c-datasets-layers__item--checked'}`
54
+ return (
55
+ <div key={dataset.id} className={itemClass} data-module='govuk-checkboxes'>
56
+ <div className='govuk-checkboxes__item'>
57
+ <input
58
+ className='govuk-checkboxes__input'
59
+ id={dataset.id}
60
+ name='layers'
61
+ type='checkbox'
62
+ value={dataset.id}
63
+ checked={dataset.visibility !== 'hidden'}
64
+ onChange={handleDatasetChange}
65
+ />
66
+ <label className={CHECKBOX_LABEL_CLASS} htmlFor={dataset.id}>
67
+ {dataset.label}
68
+ </label>
69
+ </div>
36
70
  </div>
71
+ )
72
+ }
73
+
74
+ const visibleDatasets = (pluginState.datasets || [])
75
+ .filter(dataset => dataset.toggleVisibility || hasToggleableSublayers(dataset))
76
+
77
+ const renderItems = buildRenderItems(visibleDatasets)
78
+ const hasGroups = renderItems.some(item => item.type === 'sublayers' || item.type === 'group')
79
+ const containerClass = `im-c-datasets-layers${hasGroups ? ' im-c-datasets-layers--has-groups' : ''}`
80
+
81
+ return (
82
+ <div className={containerClass}>
83
+ {renderItems.map(item => {
84
+ if (item.type === 'sublayers') {
85
+ const { dataset } = item
86
+ const anySublayerChecked = dataset.sublayers
87
+ .filter(sublayer => sublayer.toggleVisibility)
88
+ .some(sublayer => dataset.sublayerVisibility?.[sublayer.id] !== 'hidden')
89
+ const wrapperClass = `govuk-form-group im-c-datasets-layers-group${anySublayerChecked ? ' im-c-datasets-layers-group--items-checked' : ''}`
90
+ return (
91
+ <div key={dataset.id} className={wrapperClass}>
92
+ <fieldset className='im-c-datasets-layers-group__fieldset'>
93
+ <legend className='im-c-datasets-layers-group__legend'>{dataset.label}</legend>
94
+ {dataset.sublayers
95
+ .filter(sublayer => sublayer.toggleVisibility)
96
+ .map(sublayer => {
97
+ const sublayerVisible = dataset.sublayerVisibility?.[sublayer.id] !== 'hidden'
98
+ const inputId = `${dataset.id}-${sublayer.id}`
99
+ const itemClass = `im-c-datasets-layers__item govuk-checkboxes govuk-checkboxes--small${sublayerVisible ? ' im-c-datasets-layers__item--checked' : ''}`
100
+ return (
101
+ <div key={sublayer.id} className={itemClass} data-module='govuk-checkboxes'>
102
+ <div className='govuk-checkboxes__item'>
103
+ <input
104
+ className='govuk-checkboxes__input'
105
+ id={inputId}
106
+ type='checkbox'
107
+ checked={sublayerVisible}
108
+ data-dataset-id={dataset.id}
109
+ data-sublayer-id={sublayer.id}
110
+ onChange={handleSublayerChange}
111
+ />
112
+ <label className={CHECKBOX_LABEL_CLASS} htmlFor={inputId}>
113
+ {sublayer.label}
114
+ </label>
115
+ </div>
116
+ </div>
117
+ )
118
+ })}
119
+ </fieldset>
120
+ </div>
121
+ )
122
+ }
123
+
124
+ if (item.type === 'group') {
125
+ const anyDatasetChecked = item.datasets.some(d => d.visibility !== 'hidden')
126
+ const wrapperClass = `govuk-form-group im-c-datasets-layers-group${anyDatasetChecked ? ' im-c-datasets-layers-group--items-checked' : ''}`
127
+ return (
128
+ <div key={item.groupLabel} className={wrapperClass}>
129
+ <fieldset className='im-c-datasets-layers-group__fieldset'>
130
+ <legend className='im-c-datasets-layers-group__legend'>{item.groupLabel}</legend>
131
+ {item.datasets.map(dataset => renderDatasetItem(dataset))}
132
+ </fieldset>
133
+ </div>
134
+ )
135
+ }
136
+
137
+ return renderDatasetItem(item.dataset)
138
+ })}
37
139
  </div>
38
140
  )
39
141
  }
@@ -1,8 +1,37 @@
1
+ // When groups are present, every direct child (groups and flat items) gets a border-top
2
+ .im-c-datasets-layers--has-groups > * {
3
+ border-top: 1px solid var(--button-hover-color);
4
+ }
5
+
6
+ // When no groups, only the first child gets a border-top with 15px padding
7
+ .im-c-datasets-layers:not(.im-c-datasets-layers--has-groups) > *:first-child {
8
+ border-top: 1px solid var(--button-hover-color);
9
+ padding-top: 10px;
10
+ }
11
+
12
+ .im-c-datasets-layers > *:last-child {
13
+ margin-bottom: -5px;
14
+ }
15
+
16
+ .im-c-datasets-layers-group:not(:last-child) {
17
+ padding-bottom: 5px;
18
+ }
19
+
1
20
  .im-c-datasets-layers__item {
2
- border: 1px solid var(--button-hover-color);
3
- padding-left: 10px;
4
- &:not(last-child) {
5
- margin-bottom: 5px;
21
+ padding-bottom: 5px;
22
+
23
+ .im-c-datasets-layers--has-groups > & {
24
+ padding-top: 5px;
25
+ }
26
+
27
+ // Items inside a group have no individual padding or spacing between them
28
+ .im-c-datasets-layers-group & {
29
+ padding-top: 0;
30
+ padding-bottom: 0;
31
+
32
+ &:not(:last-child) {
33
+ margin-bottom: 0;
34
+ }
6
35
  }
7
36
  }
8
37
 
@@ -19,10 +48,6 @@
19
48
  color: var(--foreground-color);
20
49
  }
21
50
 
22
- .im-c-datasets-layers__item--checked {
23
- border-color: var(--app-border-color);
24
- }
25
-
26
51
  // GovUK style overide
27
52
  .im-c-datasets-layers__item .govuk-checkboxes__item {
28
53
  flex-wrap: nowrap;
@@ -31,3 +56,20 @@
31
56
  margin-left: -3px;
32
57
  }
33
58
  }
59
+
60
+ .im-c-datasets-layers-group__fieldset {
61
+ // Reset browser fieldset defaults
62
+ border: none;
63
+ padding: 0;
64
+ margin: 0;
65
+ min-width: 0;
66
+ }
67
+
68
+ .im-c-datasets-layers-group__legend {
69
+ padding-top: 15px;
70
+ padding-bottom: 10px;
71
+ padding-inline: 0;
72
+ font-size: 1rem;
73
+ font-weight: bold;
74
+ color: var(--foreground-color);
75
+ }
@@ -1,16 +1,27 @@
1
+ import { applyDatasetDefaults } from './defaults.js'
2
+
1
3
  const initialState = {
2
4
  datasets: null,
3
- hiddenFeatures: {} // { [layerId]: { idProperty: string, ids: string[] } }
5
+ hiddenFeatures: {}, // { [layerId]: { idProperty: string, ids: string[] } }
6
+ layerAdapter: null
7
+ }
8
+
9
+ const initSublayerVisibility = (dataset) => {
10
+ if (!dataset.sublayers?.length) {
11
+ return dataset
12
+ }
13
+ const sublayerVisibility = {}
14
+ dataset.sublayers.forEach(sublayer => {
15
+ sublayerVisibility[sublayer.id] = 'visible'
16
+ })
17
+ return { ...dataset, sublayerVisibility }
4
18
  }
5
19
 
6
20
  const setDatasets = (state, payload) => {
7
21
  const { datasets, datasetDefaults } = payload
8
22
  return {
9
23
  ...state,
10
- datasets: datasets.map(dataset => ({
11
- ...datasetDefaults,
12
- ...dataset
13
- }))
24
+ datasets: datasets.map(dataset => initSublayerVisibility(applyDatasetDefaults(dataset, datasetDefaults)))
14
25
  }
15
26
  }
16
27
 
@@ -20,7 +31,7 @@ const addDataset = (state, payload) => {
20
31
  ...state,
21
32
  datasets: [
22
33
  ...(state.datasets || []),
23
- { ...datasetDefaults, ...dataset }
34
+ initSublayerVisibility(applyDatasetDefaults(dataset, datasetDefaults))
24
35
  ]
25
36
  }
26
37
  }
@@ -43,6 +54,14 @@ const setDatasetVisibility = (state, payload) => {
43
54
  }
44
55
  }
45
56
 
57
+ const setGlobalVisibility = (state, payload) => {
58
+ const { visibility } = payload
59
+ return {
60
+ ...state,
61
+ datasets: state.datasets?.map(dataset => ({ ...dataset, visibility }))
62
+ }
63
+ }
64
+
46
65
  const hideFeatures = (state, payload) => {
47
66
  const { layerId, idProperty, featureIds } = payload
48
67
  const existing = state.hiddenFeatures[layerId]
@@ -61,12 +80,15 @@ const hideFeatures = (state, payload) => {
61
80
  const showFeatures = (state, payload) => {
62
81
  const { layerId, featureIds } = payload
63
82
  const existing = state.hiddenFeatures[layerId]
64
- if (!existing) return state
83
+ if (!existing) {
84
+ return state
85
+ }
65
86
 
66
87
  const newIds = existing.ids.filter(id => !featureIds.includes(id))
67
88
 
68
89
  if (newIds.length === 0) {
69
- const { [layerId]: _, ...rest } = state.hiddenFeatures
90
+ const rest = { ...state.hiddenFeatures }
91
+ delete rest[layerId]
70
92
  return { ...state, hiddenFeatures: rest }
71
93
  }
72
94
 
@@ -79,13 +101,110 @@ const showFeatures = (state, payload) => {
79
101
  }
80
102
  }
81
103
 
104
+ const setSublayerVisibility = (state, payload) => {
105
+ const { datasetId, sublayerId, visibility } = payload
106
+ return {
107
+ ...state,
108
+ datasets: state.datasets?.map(dataset => {
109
+ if (dataset.id !== datasetId) {
110
+ return dataset
111
+ }
112
+ return {
113
+ ...dataset,
114
+ sublayerVisibility: {
115
+ ...dataset.sublayerVisibility,
116
+ [sublayerId]: visibility
117
+ }
118
+ }
119
+ })
120
+ }
121
+ }
122
+
123
+ const setDatasetStyle = (state, payload) => {
124
+ const { datasetId, styleChanges } = payload
125
+ return {
126
+ ...state,
127
+ datasets: state.datasets?.map(dataset =>
128
+ dataset.id === datasetId ? { ...dataset, ...styleChanges } : dataset
129
+ )
130
+ }
131
+ }
132
+
133
+ const setSublayerStyle = (state, payload) => {
134
+ const { datasetId, sublayerId, styleChanges } = payload
135
+ return {
136
+ ...state,
137
+ datasets: state.datasets?.map(dataset => {
138
+ if (dataset.id !== datasetId) {
139
+ return dataset
140
+ }
141
+ return {
142
+ ...dataset,
143
+ sublayers: dataset.sublayers?.map(sublayer =>
144
+ sublayer.id === sublayerId
145
+ ? { ...sublayer, style: { ...sublayer.style, ...styleChanges } }
146
+ : sublayer
147
+ )
148
+ }
149
+ })
150
+ }
151
+ }
152
+
153
+ const setOpacity = (state, payload) => {
154
+ const { datasetId, opacity } = payload
155
+ return {
156
+ ...state,
157
+ datasets: state.datasets?.map(dataset =>
158
+ dataset.id === datasetId ? { ...dataset, opacity } : dataset
159
+ )
160
+ }
161
+ }
162
+
163
+ const setGlobalOpacity = (state, payload) => {
164
+ const { opacity } = payload
165
+ return {
166
+ ...state,
167
+ datasets: state.datasets?.map(dataset => ({ ...dataset, opacity }))
168
+ }
169
+ }
170
+
171
+ const setSublayerOpacity = (state, payload) => {
172
+ const { datasetId, sublayerId, opacity } = payload
173
+ return {
174
+ ...state,
175
+ datasets: state.datasets?.map(dataset => {
176
+ if (dataset.id !== datasetId) {
177
+ return dataset
178
+ }
179
+ return {
180
+ ...dataset,
181
+ sublayers: dataset.sublayers?.map(sublayer =>
182
+ sublayer.id === sublayerId
183
+ ? { ...sublayer, style: { ...sublayer.style, opacity } }
184
+ : sublayer
185
+ )
186
+ }
187
+ })
188
+ }
189
+ }
190
+
191
+ const setLayerAdapter = (state, payload) => ({ ...state, layerAdapter: payload })
192
+
82
193
  const actions = {
83
194
  SET_DATASETS: setDatasets,
84
195
  ADD_DATASET: addDataset,
85
196
  REMOVE_DATASET: removeDataset,
86
197
  SET_DATASET_VISIBILITY: setDatasetVisibility,
198
+ SET_GLOBAL_VISIBILITY: setGlobalVisibility,
199
+ SET_SUBLAYER_VISIBILITY: setSublayerVisibility,
200
+ SET_DATASET_STYLE: setDatasetStyle,
201
+ SET_SUBLAYER_STYLE: setSublayerStyle,
202
+ SET_OPACITY: setOpacity,
203
+ SET_GLOBAL_OPACITY: setGlobalOpacity,
204
+ SET_SUBLAYER_OPACITY: setSublayerOpacity,
87
205
  HIDE_FEATURES: hideFeatures,
88
- SHOW_FEATURES: showFeatures
206
+ SHOW_FEATURES: showFeatures,
207
+ SET_LAYER_ADAPTER: setLayerAdapter
89
208
  }
90
209
 
91
210
  export {