@defra/interactive-map 0.0.16-alpha → 0.0.18-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 (224) hide show
  1. package/assets/images/slot-map.svg +264 -0
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/im-core.js +1 -1
  4. package/dist/esm/im-shell.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api/context.md +53 -7
  8. package/docs/api/map-style-config.md +41 -2
  9. package/docs/api/marker-config.md +53 -11
  10. package/docs/api/slots.md +16 -15
  11. package/docs/api/symbol-config.md +160 -0
  12. package/docs/api/symbol-registry.md +115 -0
  13. package/docs/api.md +25 -22
  14. package/docs/getting-started.md +4 -1
  15. package/docs/plugins/datasets.md +657 -0
  16. package/docs/plugins/interact.md +68 -43
  17. package/docs/plugins/search.md +15 -3
  18. package/docs/plugins.md +1 -1
  19. package/package.json +2 -2
  20. package/plugins/beta/datasets/dist/css/index.css +103 -15
  21. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  22. package/plugins/beta/datasets/dist/esm/index.js +1 -1
  23. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  24. package/plugins/beta/datasets/dist/umd/index.js +1 -1
  25. package/plugins/beta/datasets/src/DatasetsInit.jsx +29 -9
  26. package/plugins/beta/datasets/src/adapters/maplibre/index.js +18 -0
  27. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +159 -0
  28. package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +75 -0
  29. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +440 -0
  30. package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +27 -0
  31. package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +31 -0
  32. package/plugins/beta/datasets/src/api/addDataset.js +2 -8
  33. package/plugins/beta/datasets/src/api/getOpacity.js +17 -0
  34. package/plugins/beta/datasets/src/api/getStyle.js +13 -0
  35. package/plugins/beta/datasets/src/api/removeDataset.js +2 -44
  36. package/plugins/beta/datasets/src/api/setData.js +10 -0
  37. package/plugins/beta/datasets/src/api/setDatasetVisibility.js +37 -0
  38. package/plugins/beta/datasets/src/api/setFeatureVisibility.js +22 -0
  39. package/plugins/beta/datasets/src/api/setOpacity.js +29 -0
  40. package/plugins/beta/datasets/src/api/setStyle.js +22 -0
  41. package/plugins/beta/datasets/src/components/EmptyKey.jsx +7 -0
  42. package/plugins/beta/datasets/src/components/EmptyKey.test.jsx +21 -0
  43. package/plugins/beta/datasets/src/components/KeySvg.jsx +24 -0
  44. package/plugins/beta/datasets/src/components/KeySvgLine.jsx +19 -0
  45. package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +15 -0
  46. package/plugins/beta/datasets/src/components/KeySvgRect.jsx +22 -0
  47. package/plugins/beta/datasets/src/components/KeySvgSymbol.jsx +16 -0
  48. package/plugins/beta/datasets/src/components/svgProperties.js +20 -0
  49. package/plugins/beta/datasets/src/datasets.js +39 -56
  50. package/plugins/beta/datasets/src/defaults.js +44 -8
  51. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +34 -25
  52. package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +2 -2
  53. package/plugins/beta/datasets/src/index.js +2 -1
  54. package/plugins/beta/datasets/src/manifest.js +25 -17
  55. package/plugins/beta/datasets/src/panels/Key.jsx +51 -51
  56. package/plugins/beta/datasets/src/panels/Key.module.scss +59 -9
  57. package/plugins/beta/datasets/src/panels/Layers.jsx +132 -29
  58. package/plugins/beta/datasets/src/panels/Layers.module.scss +56 -8
  59. package/plugins/beta/datasets/src/reducer.js +134 -9
  60. package/plugins/beta/datasets/src/reducers/keyReducer.js +34 -0
  61. package/plugins/beta/datasets/src/utils/bbox.js +7 -5
  62. package/plugins/beta/datasets/src/utils/filters.js +5 -2
  63. package/plugins/beta/datasets/src/utils/mergeSublayer.js +86 -0
  64. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  65. package/plugins/beta/draw-es/src/DrawInit.jsx +3 -2
  66. package/plugins/beta/draw-ml/dist/css/index.css +21 -1
  67. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  68. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  69. package/plugins/beta/draw-ml/dist/umd/index.js +1 -1
  70. package/plugins/beta/draw-ml/src/DrawInit.jsx +4 -3
  71. package/plugins/beta/draw-ml/src/draw.scss +0 -7
  72. package/plugins/beta/draw-ml/src/manifest.js +16 -16
  73. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
  74. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
  75. package/plugins/beta/frame/src/Frame.jsx +5 -5
  76. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  77. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  78. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  79. package/plugins/beta/map-styles/src/MapStyles.jsx +5 -4
  80. package/plugins/beta/map-styles/src/MapStylesInit.jsx +5 -4
  81. package/plugins/beta/map-styles/src/manifest.js +1 -1
  82. package/plugins/beta/scale-bar/dist/css/index.css +1 -1
  83. package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -1
  84. package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -1
  85. package/plugins/beta/scale-bar/src/index.test.js +3 -3
  86. package/plugins/beta/scale-bar/src/manifest.js +3 -3
  87. package/plugins/beta/scale-bar/src/scaleBar.scss +2 -1
  88. package/plugins/interact/dist/css/index.css +1 -1
  89. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  90. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  91. package/plugins/interact/dist/umd/index.js +1 -1
  92. package/plugins/interact/src/InteractInit.jsx +14 -5
  93. package/plugins/interact/src/InteractInit.test.js +26 -6
  94. package/plugins/interact/src/api/enable.test.js +7 -7
  95. package/plugins/interact/src/defaults.js +4 -6
  96. package/plugins/interact/src/events.js +9 -6
  97. package/plugins/interact/src/events.test.js +28 -4
  98. package/plugins/interact/src/hooks/useHighlightSync.js +3 -3
  99. package/plugins/interact/src/hooks/useHighlightSync.test.js +6 -6
  100. package/plugins/interact/src/hooks/useHoverCursor.js +10 -0
  101. package/plugins/interact/src/hooks/useHoverCursor.test.js +44 -0
  102. package/plugins/interact/src/hooks/useInteractionHandlers.js +111 -69
  103. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +147 -32
  104. package/plugins/interact/src/interact.scss +0 -7
  105. package/plugins/interact/src/manifest.js +14 -18
  106. package/plugins/interact/src/manifest.test.js +3 -1
  107. package/plugins/interact/src/reducer.js +23 -4
  108. package/plugins/interact/src/reducer.test.js +60 -11
  109. package/plugins/interact/src/utils/buildStylesMap.js +17 -4
  110. package/plugins/interact/src/utils/buildStylesMap.test.js +16 -2
  111. package/plugins/interact/src/utils/featureQueries.js +11 -6
  112. package/plugins/interact/src/utils/featureQueries.test.js +8 -1
  113. package/plugins/search/dist/css/index.css +1 -1
  114. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  115. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  116. package/plugins/search/src/Search.jsx +3 -1
  117. package/plugins/search/src/components/Form/Form.module.scss +2 -1
  118. package/plugins/search/src/events/fetchSuggestions.js +6 -4
  119. package/plugins/search/src/events/fetchSuggestions.test.js +26 -4
  120. package/plugins/search/src/events/formHandlers.js +3 -3
  121. package/plugins/search/src/events/formHandlers.test.js +1 -1
  122. package/plugins/search/src/events/suggestionHandlers.js +2 -2
  123. package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
  124. package/plugins/search/src/utils/updateMap.js +3 -3
  125. package/plugins/search/src/utils/updateMap.test.js +3 -3
  126. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  127. package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
  128. package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
  129. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  130. package/providers/maplibre/dist/umd/index.js +1 -1
  131. package/providers/maplibre/src/appEvents.js +7 -0
  132. package/providers/maplibre/src/appEvents.test.js +18 -4
  133. package/providers/maplibre/src/maplibreProvider.js +52 -0
  134. package/providers/maplibre/src/maplibreProvider.test.js +105 -1
  135. package/providers/maplibre/src/utils/highlightFeatures.js +37 -7
  136. package/providers/maplibre/src/utils/highlightFeatures.test.js +153 -95
  137. package/providers/maplibre/src/utils/hoverCursor.js +61 -0
  138. package/providers/maplibre/src/utils/hoverCursor.test.js +130 -0
  139. package/providers/maplibre/src/utils/patternImages.js +70 -0
  140. package/providers/maplibre/src/utils/patternImages.test.js +180 -0
  141. package/providers/maplibre/src/utils/queryFeatures.js +38 -16
  142. package/providers/maplibre/src/utils/queryFeatures.test.js +20 -3
  143. package/providers/maplibre/src/utils/rasteriseToImageData.js +30 -0
  144. package/providers/maplibre/src/utils/rasteriseToImageData.test.js +69 -0
  145. package/providers/maplibre/src/utils/symbolImages.js +147 -0
  146. package/providers/maplibre/src/utils/symbolImages.test.js +248 -0
  147. package/src/App/components/Actions/Actions.jsx +2 -2
  148. package/src/App/components/Actions/Actions.module.scss +0 -7
  149. package/src/App/components/Actions/Actions.test.jsx +1 -1
  150. package/src/App/components/Icon/Icon.jsx +3 -2
  151. package/src/App/components/Icon/Icon.module.scss +4 -0
  152. package/src/App/components/Icon/Icon.test.jsx +43 -4
  153. package/src/App/components/MapButton/MapButton.jsx +42 -17
  154. package/src/App/components/MapButton/MapButton.module.scss +4 -13
  155. package/src/App/components/MapButton/MapButton.test.jsx +27 -3
  156. package/src/App/components/Markers/Markers.jsx +122 -27
  157. package/src/App/components/Markers/Markers.module.scss +0 -10
  158. package/src/App/components/Markers/Markers.test.jsx +246 -0
  159. package/src/App/components/PopupMenu/PopupMenu.jsx +51 -274
  160. package/src/App/components/PopupMenu/PopupMenu.module.scss +14 -7
  161. package/src/App/components/PopupMenu/PopupMenu.test.jsx +70 -1
  162. package/src/App/components/PopupMenu/usePopupMenu.js +258 -0
  163. package/src/App/hooks/useButtonStateEvaluator.js +12 -2
  164. package/src/App/hooks/useButtonStateEvaluator.test.js +38 -4
  165. package/src/App/hooks/useInterfaceAPI.js +6 -0
  166. package/src/App/hooks/useInterfaceAPI.test.js +156 -0
  167. package/src/App/hooks/useLayoutMeasurements.js +84 -18
  168. package/src/App/hooks/useLayoutMeasurements.test.js +124 -17
  169. package/src/App/hooks/useMarkersAPI.js +2 -5
  170. package/src/App/hooks/useMarkersAPI.test.js +4 -4
  171. package/src/App/layout/Layout.jsx +14 -9
  172. package/src/App/layout/Layout.test.jsx +6 -4
  173. package/src/App/layout/layout.module.scss +67 -29
  174. package/src/App/registry/pluginRegistry.js +1 -1
  175. package/src/App/renderer/HtmlElementHost.jsx +2 -1
  176. package/src/App/renderer/HtmlElementHost.test.jsx +7 -7
  177. package/src/App/renderer/mapButtons.js +1 -1
  178. package/src/App/renderer/mapPanels.test.js +2 -2
  179. package/src/App/renderer/slotHelpers.js +2 -2
  180. package/src/App/renderer/slotHelpers.test.js +5 -5
  181. package/src/App/renderer/slots.js +9 -5
  182. package/src/App/store/AppProvider.jsx +3 -1
  183. package/src/App/store/AppProvider.test.jsx +1 -1
  184. package/src/App/store/ServiceProvider.jsx +8 -4
  185. package/src/App/store/appActionsMap.js +16 -0
  186. package/src/App/store/appActionsMap.test.js +27 -0
  187. package/src/App/store/appDispatchMiddleware.js +1 -1
  188. package/src/App/store/appDispatchMiddleware.test.js +2 -2
  189. package/src/App/store/appReducer.js +2 -0
  190. package/src/App/store/mapActionsMap.js +4 -6
  191. package/src/App/store/mapActionsMap.test.js +3 -2
  192. package/src/App/store/mapReducer.js +2 -1
  193. package/src/InteractiveMap/InteractiveMap.js +4 -0
  194. package/src/config/appConfig.js +5 -8
  195. package/src/config/appConfig.test.js +1 -2
  196. package/src/config/defaults.js +0 -2
  197. package/src/config/events.js +28 -0
  198. package/src/config/mapTheme.js +56 -0
  199. package/src/config/patternConfig.js +16 -0
  200. package/src/config/symbolConfig.js +80 -0
  201. package/src/scss/main.scss +1 -0
  202. package/src/scss/settings/_colors.scss +0 -9
  203. package/src/scss/settings/_dimensions.scss +0 -1
  204. package/src/services/patternRegistry.js +40 -0
  205. package/src/services/patternRegistry.test.js +48 -0
  206. package/src/services/symbolRegistry.js +113 -0
  207. package/src/services/symbolRegistry.test.js +262 -0
  208. package/src/types.js +93 -11
  209. package/src/utils/getSafeZoneInset.js +9 -7
  210. package/src/utils/getSafeZoneInset.test.js +10 -10
  211. package/src/utils/patternUtils.js +94 -0
  212. package/src/utils/patternUtils.test.js +160 -0
  213. package/src/utils/symbolUtils.js +85 -0
  214. package/src/utils/symbolUtils.test.js +156 -0
  215. package/webpack.dev.mjs +1 -1
  216. package/docs/api/slot-map.svg +0 -1
  217. package/plugins/beta/datasets/src/api/hideDataset.js +0 -14
  218. package/plugins/beta/datasets/src/api/hideFeatures.js +0 -41
  219. package/plugins/beta/datasets/src/api/showDataset.js +0 -14
  220. package/plugins/beta/datasets/src/api/showFeatures.js +0 -44
  221. package/plugins/beta/datasets/src/handleSetMapStyle.js +0 -54
  222. package/plugins/beta/datasets/src/mapLayers.js +0 -164
  223. /package/src/{utils → services}/logger.js +0 -0
  224. /package/src/{utils → services}/logger.test.js +0 -0
@@ -0,0 +1,29 @@
1
+ export const setOpacity = ({ pluginState }, opacity, options) => {
2
+ const { datasetId, sublayerId } = options || {}
3
+
4
+ if (sublayerId) {
5
+ const dataset = pluginState.datasets?.find(d => d.id === datasetId)
6
+ if (!dataset) {
7
+ return
8
+ }
9
+ pluginState.dispatch({ type: 'SET_SUBLAYER_OPACITY', payload: { datasetId, sublayerId, opacity } })
10
+ pluginState.layerAdapter?.setSublayerOpacity(datasetId, sublayerId, opacity)
11
+ return
12
+ }
13
+
14
+ if (datasetId) {
15
+ const dataset = pluginState.datasets?.find(d => d.id === datasetId)
16
+ if (!dataset) {
17
+ return
18
+ }
19
+ pluginState.dispatch({ type: 'SET_OPACITY', payload: { datasetId, opacity } })
20
+ pluginState.layerAdapter?.setOpacity(datasetId, opacity)
21
+ return
22
+ }
23
+
24
+ // Global
25
+ pluginState.dispatch({ type: 'SET_GLOBAL_OPACITY', payload: { opacity } })
26
+ pluginState.datasets?.forEach(d => {
27
+ pluginState.layerAdapter?.setOpacity(d.id, opacity)
28
+ })
29
+ }
@@ -0,0 +1,22 @@
1
+ export const setStyle = ({ pluginState, mapState }, style, { datasetId, sublayerId } = {}) => {
2
+ const dataset = pluginState.datasets?.find(d => d.id === datasetId)
3
+ if (!dataset) {
4
+ return
5
+ }
6
+
7
+ if (sublayerId) {
8
+ pluginState.dispatch({ type: 'SET_SUBLAYER_STYLE', payload: { datasetId, sublayerId, styleChanges: style } })
9
+ const updatedSublayerDataset = {
10
+ ...dataset,
11
+ sublayers: dataset.sublayers?.map(sublayer =>
12
+ sublayer.id === sublayerId ? { ...sublayer, style: { ...sublayer.style, ...style } } : sublayer
13
+ )
14
+ }
15
+ pluginState.layerAdapter?.setSublayerStyle(updatedSublayerDataset, sublayerId, mapState.mapStyle)
16
+ return
17
+ }
18
+
19
+ pluginState.dispatch({ type: 'SET_DATASET_STYLE', payload: { datasetId, styleChanges: style } })
20
+ const updatedDataset = { ...dataset, ...style }
21
+ pluginState.layerAdapter?.setStyle(updatedDataset, mapState.mapStyle)
22
+ }
@@ -0,0 +1,7 @@
1
+ export const EmptyKey = ({ text }) => {
2
+ return (
3
+ <div className='im-c-datasets-key'>
4
+ <p className='im-c-datasets-key__empty-message'>{text}</p>
5
+ </div>
6
+ )
7
+ }
@@ -0,0 +1,21 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { EmptyKey } from './EmptyKey'
3
+
4
+ describe('EmptyKey', () => {
5
+ const text = 'No features available'
6
+
7
+ it('renders the wrapper div with the correct class', async () => {
8
+ const { container } = render(<EmptyKey text={text} />)
9
+ expect(container.querySelector('.im-c-datasets-key')).toBeTruthy()
10
+ })
11
+
12
+ it('renders the empty message paragraph with the correct class', async () => {
13
+ const { container } = render(<EmptyKey text={text} />)
14
+ expect(container.querySelector('.im-c-datasets-key__empty-message')).toBeTruthy()
15
+ })
16
+
17
+ it('renders the provided text', async () => {
18
+ render(<EmptyKey text={text} />)
19
+ expect(screen.getByText(text)).toBeTruthy()
20
+ })
21
+ })
@@ -0,0 +1,24 @@
1
+ import { hasSymbol, getSymbolDef } from '../../../../../src/utils/symbolUtils.js'
2
+ import { hasPattern } from '../../../../../src/utils/patternUtils.js'
3
+ import { KeySvgPattern } from './KeySvgPattern.jsx'
4
+ import { KeySvgSymbol } from './KeySvgSymbol.jsx'
5
+ import { KeySvgLine } from './KeySvgLine.jsx'
6
+ import { KeySvgRect } from './KeySvgRect.jsx'
7
+
8
+ export const KeySvg = (props) => {
9
+ const { symbolRegistry } = props
10
+ const symbolDef = hasSymbol(props) && getSymbolDef(props, symbolRegistry)
11
+ if (symbolDef) {
12
+ return <KeySvgSymbol {...props} symbolDef={symbolDef} />
13
+ }
14
+
15
+ if (hasPattern(props)) {
16
+ return <KeySvgPattern {...props} />
17
+ }
18
+
19
+ if (props.keySymbolShape === 'line') {
20
+ return <KeySvgLine {...props} />
21
+ }
22
+
23
+ return <KeySvgRect {...props} />
24
+ }
@@ -0,0 +1,19 @@
1
+ import { svgProps, SVG_SIZE, SVG_CENTER } from './svgProperties.js'
2
+ import { getValueForStyle } from '../../../../../src/utils/getValueForStyle'
3
+
4
+ export const KeySvgLine = (props) => {
5
+ const { mapStyle } = props
6
+ return (
7
+ <svg {...svgProps}>
8
+ <line
9
+ x1={props.strokeWidth / 2}
10
+ y1={SVG_CENTER}
11
+ x2={SVG_SIZE - props.strokeWidth / 2}
12
+ y2={SVG_CENTER}
13
+ stroke={getValueForStyle(props.stroke, mapStyle.id)}
14
+ strokeWidth={props.strokeWidth}
15
+ strokeLinecap='round'
16
+ />
17
+ </svg>
18
+ )
19
+ }
@@ -0,0 +1,15 @@
1
+ import { getKeyPatternPaths } from '../../../../../src/utils/patternUtils.js'
2
+ import { svgProps } from './svgProperties.js'
3
+ const PATTERN_INSET = 2
4
+
5
+ export const KeySvgPattern = (props) => {
6
+ const { patternRegistry, mapStyle } = props
7
+
8
+ const paths = getKeyPatternPaths(props, mapStyle.id, patternRegistry)
9
+ return (
10
+ <svg {...svgProps}>
11
+ <g dangerouslySetInnerHTML={{ __html: paths.border }} />
12
+ <g transform={`translate(${PATTERN_INSET}, ${PATTERN_INSET})`} dangerouslySetInnerHTML={{ __html: paths.content }} />
13
+ </svg>
14
+ )
15
+ }
@@ -0,0 +1,22 @@
1
+ import { getValueForStyle } from '../../../../../src/utils/getValueForStyle'
2
+ import { svgProps, SVG_SIZE } from './svgProperties.js'
3
+
4
+ export const KeySvgRect = (props) => {
5
+ const { mapStyle } = props
6
+ return (
7
+ <svg {...svgProps}>
8
+ <rect
9
+ x={props.strokeWidth / 2}
10
+ y={props.strokeWidth / 2}
11
+ width={SVG_SIZE - props.strokeWidth}
12
+ height={SVG_SIZE - props.strokeWidth}
13
+ rx={props.strokeWidth}
14
+ ry={props.strokeWidth}
15
+ fill={getValueForStyle(props.fill, mapStyle.id)}
16
+ stroke={getValueForStyle(props.stroke, mapStyle.id)}
17
+ strokeWidth={props.strokeWidth}
18
+ strokeLinejoin='round'
19
+ />
20
+ </svg>
21
+ )
22
+ }
@@ -0,0 +1,16 @@
1
+ import { getSymbolStyleColors, getSymbolViewBox } from '../../../../../src/utils/symbolUtils.js'
2
+ import { svgSymbolProps } from './svgProperties.js'
3
+
4
+ export const KeySvgSymbol = (props) => {
5
+ const { symbolRegistry, mapStyle, symbolDef } = props
6
+ const mapColorScheme = mapStyle?.appColorScheme ?? 'light'
7
+ const keyMapStyle = { ...mapStyle, mapColorScheme }
8
+
9
+ const resolvedSvg = symbolRegistry.resolve(symbolDef, getSymbolStyleColors(props), keyMapStyle)
10
+ const viewBox = getSymbolViewBox(props, symbolDef)
11
+ return (
12
+ <svg {...svgSymbolProps} viewBox={viewBox}>
13
+ <g dangerouslySetInnerHTML={{ __html: resolvedSvg }} />
14
+ </svg>
15
+ )
16
+ }
@@ -0,0 +1,20 @@
1
+ export const SVG_SIZE = 20
2
+ export const SVG_CENTER = SVG_SIZE / 2
3
+ const SVG_SYMBOL_SIZE = 38
4
+
5
+ export const svgProps = {
6
+ xmlns: 'http://www.w3.org/2000/svg',
7
+ width: SVG_SIZE,
8
+ height: SVG_SIZE,
9
+ viewBox: `0 0 ${SVG_SIZE} ${SVG_SIZE}`,
10
+ className: 'am-c-datasets-key-symbol',
11
+ 'aria-hidden': 'true',
12
+ focusable: 'false'
13
+ }
14
+
15
+ export const svgSymbolProps = {
16
+ ...svgProps,
17
+ width: SVG_SYMBOL_SIZE,
18
+ height: SVG_SYMBOL_SIZE,
19
+ className: 'am-c-datasets-key-symbol am-c-datasets-key-symbol--point'
20
+ }
@@ -1,81 +1,71 @@
1
- import { handleSetMapStyle } from './handleSetMapStyle.js'
2
- import { addMapLayers, getSourceId, getLayersUsingSource, isDynamicSource, updateSourceData } from './mapLayers.js'
3
1
  import { createDynamicSource } from './fetch/createDynamicSource.js'
2
+ // NOSONAR: applyDatasetDefaults and datasetDefaults are used in processedDatasets.map
3
+ import { applyDatasetDefaults, datasetDefaults } from './defaults.js'
4
+
5
+ const isDynamicSource = (dataset) =>
6
+ typeof dataset.geojson === 'string' &&
7
+ !!dataset.idProperty &&
8
+ typeof dataset.transformRequest === 'function'
4
9
 
5
10
  export const createDatasets = ({
11
+ adapter,
6
12
  pluginConfig,
7
13
  pluginStateRef,
8
- mapStyleId,
14
+ mapStyle,
9
15
  mapProvider,
10
16
  events,
11
17
  eventBus
12
18
  }) => {
13
- const { map } = mapProvider
14
19
  const { datasets } = pluginConfig
15
20
 
16
- // Track dynamic sources for cleanup
17
21
  const dynamicSources = new Map()
18
22
 
19
23
  const getDatasets = () => pluginStateRef.current.datasets || datasets
20
24
  const getHiddenFeatures = () => pluginStateRef.current.hiddenFeatures || {}
21
25
 
22
- // Initialize all datasets once
23
- datasets.forEach(dataset => {
24
- addMapLayers(map, mapStyleId, dataset)
26
+ // Initialise all datasets via the adapter, then set up dynamic sources
27
+ const processedDatasets = datasets.map(d => applyDatasetDefaults(d, datasetDefaults))
28
+ adapter.init(processedDatasets, mapStyle).then(() => {
29
+ processedDatasets.forEach(dataset => {
30
+ if (!isDynamicSource(dataset)) {
31
+ return
32
+ }
25
33
 
26
- // Initialize dynamic source if applicable
27
- if (isDynamicSource(dataset)) {
28
- const sourceId = getSourceId(dataset)
29
34
  const dynamicSource = createDynamicSource({
30
35
  dataset,
31
- map,
32
- sourceId,
33
- onUpdate: (id, geojson) => updateSourceData(map, id, geojson)
36
+ map: mapProvider.map,
37
+ onUpdate: (datasetId, geojson) => adapter.setData(datasetId, geojson)
34
38
  })
35
39
  dynamicSources.set(dataset.id, dynamicSource)
36
- }
37
- })
40
+ })
38
41
 
39
- // Emit ready event once map has processed the layers
40
- map.once('idle', () => {
41
42
  eventBus.emit('datasets:ready')
42
43
  })
43
44
 
44
- // Handle style changes
45
- const styleHandler = handleSetMapStyle({
46
- map,
47
- events,
48
- eventBus,
49
- getDatasets,
50
- getHiddenFeatures,
51
- getDynamicSources: () => dynamicSources
52
- })
45
+ let currentMapStyle = mapStyle
46
+
47
+ // Handle basemap style changes — delegate entirely to the adapter
48
+ const onSetStyle = (newMapStyle) => {
49
+ currentMapStyle = newMapStyle
50
+ adapter.onStyleChange(getDatasets(), newMapStyle, getHiddenFeatures(), dynamicSources)
51
+ }
52
+
53
+ const onSizeChange = () => {
54
+ adapter.onSizeChange(getDatasets(), currentMapStyle)
55
+ }
56
+
57
+ eventBus.on(events.MAP_SET_STYLE, onSetStyle)
58
+ eventBus.on(events.MAP_SIZE_CHANGE, onSizeChange)
53
59
 
54
60
  return {
55
61
  remove () {
56
- eventBus.off(events.MAP_SET_STYLE, styleHandler)
62
+ eventBus.off(events.MAP_SET_STYLE, onSetStyle)
63
+ eventBus.off(events.MAP_SIZE_CHANGE, onSizeChange)
57
64
 
58
65
  // Clean up dynamic sources
59
66
  dynamicSources.forEach(source => source.destroy())
60
67
  dynamicSources.clear()
61
-
62
- const allDatasets = getDatasets()
63
- const removedSourceIds = new Set()
64
-
65
- // Remove layers and sources
66
- allDatasets.forEach(dataset => {
67
- const sourceId = getSourceId(dataset)
68
- const layers = getLayersUsingSource(map, sourceId)
69
-
70
- // Remove all layers using this source
71
- layers.forEach(id => map.removeLayer(id))
72
-
73
- // Remove the source once
74
- if (!removedSourceIds.has(sourceId) && map.getSource(sourceId)) {
75
- map.removeSource(sourceId)
76
- removedSourceIds.add(sourceId)
77
- }
78
- })
68
+ adapter.destroy(getDatasets())
79
69
  },
80
70
 
81
71
  /**
@@ -83,10 +73,7 @@ export const createDatasets = ({
83
73
  * @param {string} datasetId - Dataset ID to refresh
84
74
  */
85
75
  refreshDataset (datasetId) {
86
- const dynamicSource = dynamicSources.get(datasetId)
87
- if (dynamicSource) {
88
- dynamicSource.refresh()
89
- }
76
+ dynamicSources.get(datasetId)?.refresh()
90
77
  },
91
78
 
92
79
  /**
@@ -94,10 +81,7 @@ export const createDatasets = ({
94
81
  * @param {string} datasetId - Dataset ID to clear
95
82
  */
96
83
  clearDatasetCache (datasetId) {
97
- const dynamicSource = dynamicSources.get(datasetId)
98
- if (dynamicSource) {
99
- dynamicSource.clear()
100
- }
84
+ dynamicSources.get(datasetId)?.clear()
101
85
  },
102
86
 
103
87
  /**
@@ -106,8 +90,7 @@ export const createDatasets = ({
106
90
  * @returns {number|null} Feature count or null if not a dynamic source
107
91
  */
108
92
  getFeatureCount (datasetId) {
109
- const dynamicSource = dynamicSources.get(datasetId)
110
- return dynamicSource ? dynamicSource.getFeatureCount() : null
93
+ return dynamicSources.get(datasetId)?.getFeatureCount() ?? null
111
94
  }
112
95
  }
113
96
  }
@@ -1,15 +1,51 @@
1
1
  const datasetDefaults = {
2
- stroke: '#d4351c',
3
- strokeWidth: 2,
4
- fill: 'transparent',
5
- symbolDescription: 'red outline',
6
2
  minZoom: 6,
7
3
  maxZoom: 24,
8
4
  showInKey: false,
9
- showInLayers: false,
10
- visibility: 'visible'
5
+ toggleVisibility: false,
6
+ visibility: 'visible',
7
+ style: {
8
+ stroke: '#d4351c',
9
+ strokeWidth: 2,
10
+ fill: 'transparent',
11
+ symbolDescription: 'red outline'
12
+ }
11
13
  }
12
14
 
13
- export {
14
- datasetDefaults
15
+ // All properties considered style properties — must be provided via dataset.style, not at the top level.
16
+ const STYLE_PROPS = [
17
+ 'stroke', 'strokeWidth', 'strokeDashArray',
18
+ 'fill', 'fillPattern', 'fillPatternSvgContent', 'fillPatternForegroundColor', 'fillPatternBackgroundColor',
19
+ 'opacity', 'symbolDescription', 'keySymbolShape',
20
+ 'symbol', 'symbolSvgContent', 'symbolViewBox', 'symbolAnchor',
21
+ 'symbolBackgroundColor', 'symbolForegroundColor', 'symbolHaloWidth', 'symbolGraphic'
22
+ ]
23
+
24
+ // Props whose presence in a style object indicates a custom visual style.
25
+ // When any are set, the default symbolDescription is not appropriate.
26
+ const VISUAL_STYLE_PROPS = ['stroke', 'fill', 'fillPattern', 'fillPatternSvgContent', 'symbol', 'symbolSvgContent']
27
+
28
+ const hasCustomVisualStyle = (style) =>
29
+ VISUAL_STYLE_PROPS.some(prop => prop in style)
30
+
31
+ /**
32
+ * Merge a dataset config with defaults, flattening the nested `style` object.
33
+ * Style properties must be provided via dataset.style — top-level occurrences are ignored.
34
+ * symbolDescription from defaults.style is dropped when custom visual styles
35
+ * are present and the dataset doesn't explicitly set its own symbolDescription.
36
+ */
37
+ const applyDatasetDefaults = (dataset, defaults) => {
38
+ const style = dataset.style || {}
39
+ const mergedStyle = { ...defaults.style, ...style }
40
+ if (!('symbolDescription' in style) && hasCustomVisualStyle(style)) {
41
+ delete mergedStyle.symbolDescription
42
+ }
43
+ const topLevel = { ...dataset }
44
+ delete topLevel.style
45
+ STYLE_PROPS.forEach(prop => delete topLevel[prop])
46
+ const topLevelDefaults = { ...defaults }
47
+ delete topLevelDefaults.style
48
+ return { ...topLevelDefaults, ...topLevel, ...mergedStyle }
15
49
  }
50
+
51
+ export { datasetDefaults, hasCustomVisualStyle, applyDatasetDefaults }
@@ -14,17 +14,17 @@ const EVICTION_THRESHOLD = 1.2 // Trigger eviction at 120% of maxFeatures
14
14
  * @param {Function} options.onUpdate - Callback when source data should be updated
15
15
  * @returns {Object} { destroy, clear, refresh }
16
16
  */
17
- export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
17
+ export const createDynamicSource = ({ dataset, map, onUpdate }) => {
18
18
  const { geojson: baseUrl, idProperty, transformRequest, maxFeatures, minZoom = 0 } = dataset
19
19
 
20
- // Feature cache: id → { feature, bbox, addedAt }
20
+ // Feature cache: id → { feature, bbox, lastSeenAt }
21
21
  const features = new Map()
22
22
 
23
23
  // Track the bbox we've fetched data for
24
24
  let fetchedBbox = null
25
25
 
26
- // Loading state
27
- let isLoading = false
26
+ // Abort controller for the in-flight request
27
+ let currentController = null
28
28
 
29
29
  /**
30
30
  * Convert features Map to FeatureCollection
@@ -63,25 +63,25 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
63
63
  if (bboxIntersects(data.bbox, currentBbox)) {
64
64
  inView.push(id)
65
65
  } else {
66
- outOfView.push({ id, addedAt: data.addedAt })
66
+ outOfView.push({ id, lastSeenAt: data.lastSeenAt })
67
67
  }
68
68
  }
69
69
 
70
- // Sort out-of-view by insertion time (oldest first)
71
- outOfView.sort((a, b) => a.addedAt - b.addedAt)
70
+ // Sort out-of-view by last seen time (least recently seen first)
71
+ outOfView.sort((a, b) => a.lastSeenAt - b.lastSeenAt)
72
72
 
73
- // Evict oldest out-of-view features until under target
73
+ // Evict least-recently-seen out-of-view features until under target
74
74
  const toEvict = features.size - targetSize
75
75
  for (let i = 0; i < toEvict && i < outOfView.length; i++) {
76
76
  features.delete(outOfView[i].id)
77
77
  }
78
78
 
79
- // If still over target (viewport has too many), evict oldest in-view
79
+ // If still over target (viewport has too many), evict least recently seen in-view
80
80
  if (features.size > targetSize) {
81
81
  const remaining = features.size - targetSize
82
82
  const inViewSorted = inView
83
- .map(id => ({ id, addedAt: features.get(id).addedAt }))
84
- .sort((a, b) => a.addedAt - b.addedAt)
83
+ .map(id => ({ id, lastSeenAt: features.get(id).lastSeenAt }))
84
+ .sort((a, b) => a.lastSeenAt - b.lastSeenAt)
85
85
 
86
86
  for (let i = 0; i < remaining && i < inViewSorted.length; i++) {
87
87
  features.delete(inViewSorted[i].id)
@@ -105,19 +105,19 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
105
105
  return
106
106
  }
107
107
 
108
- if (isLoading) {
109
- return
108
+ // Abort any in-flight request — new viewport takes priority
109
+ if (currentController) {
110
+ currentController.abort()
110
111
  }
111
-
112
- isLoading = true
112
+ currentController = new AbortController()
113
113
 
114
114
  try {
115
115
  const context = { bbox: currentBbox, zoom, dataset }
116
- const data = await fetchGeoJSON(baseUrl, context, transformRequest)
116
+ const data = await fetchGeoJSON(baseUrl, context, transformRequest, currentController.signal)
117
117
 
118
118
  const now = Date.now()
119
119
 
120
- // Add/update features with deduplication
120
+ // Add/update features with deduplication, refreshing lastSeenAt on each fetch
121
121
  data.features.forEach(feature => {
122
122
  const id = getFeatureId(feature)
123
123
  if (id == null) {
@@ -128,22 +128,28 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
128
128
  features.set(id, {
129
129
  feature,
130
130
  bbox: getGeometryBbox(feature.geometry),
131
- addedAt: features.has(id) ? features.get(id).addedAt : now
131
+ lastSeenAt: now
132
132
  })
133
133
  })
134
134
 
135
135
  // Expand tracked bbox
136
136
  fetchedBbox = expandBbox(fetchedBbox, currentBbox)
137
137
 
138
- // Evict if over limit
138
+ // Evict if over limit; if features were removed, fetchedBbox no longer
139
+ // covers those regions — reset to current viewport to force re-fetch on return
140
+ const sizeBeforeEviction = features.size
139
141
  evictIfNeeded(currentBbox)
142
+ if (features.size < sizeBeforeEviction) {
143
+ fetchedBbox = currentBbox
144
+ }
140
145
 
141
146
  // Update map source
142
- onUpdate(sourceId, toFeatureCollection())
147
+ onUpdate(dataset.id, toFeatureCollection())
143
148
  } catch (error) {
149
+ if (error.name === 'AbortError') {
150
+ return
151
+ }
144
152
  console.error(`Failed to fetch dynamic GeoJSON for ${dataset.id}:`, error)
145
- } finally {
146
- isLoading = false
147
153
  }
148
154
  }
149
155
 
@@ -162,11 +168,14 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
162
168
 
163
169
  return {
164
170
  /**
165
- * Clean up event listeners
171
+ * Clean up event listeners and cancel any in-flight request
166
172
  */
167
173
  destroy () {
168
174
  map.off('moveend', handleMoveEnd)
169
175
  debouncedFetch.cancel()
176
+ if (currentController) {
177
+ currentController.abort()
178
+ }
170
179
  },
171
180
 
172
181
  /**
@@ -175,7 +184,7 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
175
184
  clear () {
176
185
  features.clear()
177
186
  fetchedBbox = null
178
- onUpdate(sourceId, { type: 'FeatureCollection', features: [] })
187
+ onUpdate(dataset.id, { type: 'FeatureCollection', features: [] })
179
188
  },
180
189
 
181
190
  /**
@@ -199,7 +208,7 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
199
208
  */
200
209
  reapply () {
201
210
  if (features.size > 0) {
202
- onUpdate(sourceId, toFeatureCollection())
211
+ onUpdate(dataset.id, toFeatureCollection())
203
212
  }
204
213
  }
205
214
  }
@@ -5,14 +5,14 @@
5
5
  * @param {Function} transformRequest - Function to transform the request (builds URL with bbox, adds headers)
6
6
  * @returns {Promise<Object>} GeoJSON FeatureCollection
7
7
  */
8
- export const fetchGeoJSON = async (baseUrl, context, transformRequest) => {
8
+ export const fetchGeoJSON = async (baseUrl, context, transformRequest, signal) => {
9
9
  const result = transformRequest(baseUrl, context)
10
10
 
11
11
  // Handle both string and object return values
12
12
  const config = typeof result === 'string' ? { url: result } : result
13
13
  const { url, headers = {} } = config
14
14
 
15
- const response = await fetch(url, { headers })
15
+ const response = await fetch(url, { headers, signal })
16
16
 
17
17
  if (!response.ok) {
18
18
  throw new Error(`Failed to fetch GeoJSON: ${response.status} ${response.statusText}`)
@@ -1,8 +1,9 @@
1
1
  // /plugins/datasets/index.js
2
2
  import './datasets.scss'
3
3
 
4
- export default function createPlugin (options = {}) {
4
+ export default function createPlugin (options = { }) {
5
5
  const plugin = {
6
+ noKeyItemText: 'No features displayed',
6
7
  ...options,
7
8
  id: 'datasets',
8
9
  load: async () => {