@defra/interactive-map 0.0.17-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 (140) 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/context.md +53 -7
  7. package/docs/api/map-style-config.md +41 -2
  8. package/docs/api/marker-config.md +53 -11
  9. package/docs/api/symbol-config.md +160 -0
  10. package/docs/api/symbol-registry.md +115 -0
  11. package/docs/api.md +22 -19
  12. package/docs/plugins/datasets.md +105 -9
  13. package/docs/plugins/interact.md +68 -43
  14. package/docs/plugins/search.md +15 -3
  15. package/package.json +1 -1
  16. package/plugins/beta/datasets/dist/css/index.css +32 -14
  17. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  18. package/plugins/beta/datasets/dist/esm/index.js +1 -1
  19. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  20. package/plugins/beta/datasets/dist/umd/index.js +1 -1
  21. package/plugins/beta/datasets/src/DatasetsInit.jsx +9 -4
  22. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +57 -11
  23. package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +14 -8
  24. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +155 -53
  25. package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +27 -0
  26. package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +31 -0
  27. package/plugins/beta/datasets/src/api/addDataset.js +1 -1
  28. package/plugins/beta/datasets/src/api/setData.js +4 -2
  29. package/plugins/beta/datasets/src/api/setStyle.js +2 -2
  30. package/plugins/beta/datasets/src/components/EmptyKey.jsx +7 -0
  31. package/plugins/beta/datasets/src/components/EmptyKey.test.jsx +21 -0
  32. package/plugins/beta/datasets/src/components/KeySvg.jsx +24 -0
  33. package/plugins/beta/datasets/src/components/KeySvgLine.jsx +19 -0
  34. package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +15 -0
  35. package/plugins/beta/datasets/src/components/KeySvgRect.jsx +22 -0
  36. package/plugins/beta/datasets/src/components/KeySvgSymbol.jsx +16 -0
  37. package/plugins/beta/datasets/src/components/svgProperties.js +20 -0
  38. package/plugins/beta/datasets/src/datasets.js +13 -4
  39. package/plugins/beta/datasets/src/defaults.js +4 -2
  40. package/plugins/beta/datasets/src/index.js +2 -1
  41. package/plugins/beta/datasets/src/manifest.js +1 -1
  42. package/plugins/beta/datasets/src/panels/Key.jsx +11 -89
  43. package/plugins/beta/datasets/src/panels/Key.module.scss +24 -13
  44. package/plugins/beta/datasets/src/panels/Layers.module.scss +13 -7
  45. package/plugins/beta/datasets/src/reducer.js +6 -0
  46. package/plugins/beta/datasets/src/reducers/keyReducer.js +34 -0
  47. package/plugins/beta/datasets/src/utils/mergeSublayer.js +8 -0
  48. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  49. package/plugins/beta/draw-es/src/DrawInit.jsx +3 -2
  50. package/plugins/beta/draw-ml/dist/css/index.css +21 -1
  51. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  52. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  53. package/plugins/beta/draw-ml/dist/umd/index.js +1 -1
  54. package/plugins/beta/draw-ml/src/DrawInit.jsx +4 -3
  55. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  56. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  57. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  58. package/plugins/beta/map-styles/src/MapStyles.jsx +5 -4
  59. package/plugins/beta/map-styles/src/MapStylesInit.jsx +5 -4
  60. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  61. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  62. package/plugins/interact/dist/umd/index.js +1 -1
  63. package/plugins/interact/src/InteractInit.jsx +14 -5
  64. package/plugins/interact/src/InteractInit.test.js +26 -6
  65. package/plugins/interact/src/api/enable.test.js +7 -7
  66. package/plugins/interact/src/defaults.js +4 -6
  67. package/plugins/interact/src/events.js +9 -6
  68. package/plugins/interact/src/events.test.js +28 -4
  69. package/plugins/interact/src/hooks/useHighlightSync.js +3 -3
  70. package/plugins/interact/src/hooks/useHighlightSync.test.js +6 -6
  71. package/plugins/interact/src/hooks/useHoverCursor.js +10 -0
  72. package/plugins/interact/src/hooks/useHoverCursor.test.js +44 -0
  73. package/plugins/interact/src/hooks/useInteractionHandlers.js +111 -69
  74. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +147 -32
  75. package/plugins/interact/src/reducer.js +23 -4
  76. package/plugins/interact/src/reducer.test.js +60 -11
  77. package/plugins/interact/src/utils/buildStylesMap.js +17 -4
  78. package/plugins/interact/src/utils/buildStylesMap.test.js +16 -2
  79. package/plugins/interact/src/utils/featureQueries.js +11 -6
  80. package/plugins/interact/src/utils/featureQueries.test.js +8 -1
  81. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  82. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  83. package/plugins/search/src/Search.jsx +3 -1
  84. package/plugins/search/src/events/fetchSuggestions.js +6 -4
  85. package/plugins/search/src/events/fetchSuggestions.test.js +26 -4
  86. package/plugins/search/src/events/formHandlers.js +3 -3
  87. package/plugins/search/src/events/formHandlers.test.js +1 -1
  88. package/plugins/search/src/events/suggestionHandlers.js +2 -2
  89. package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
  90. package/plugins/search/src/utils/updateMap.js +3 -3
  91. package/plugins/search/src/utils/updateMap.test.js +3 -3
  92. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  93. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  94. package/providers/maplibre/dist/umd/index.js +1 -1
  95. package/providers/maplibre/src/appEvents.js +7 -0
  96. package/providers/maplibre/src/appEvents.test.js +18 -4
  97. package/providers/maplibre/src/maplibreProvider.js +52 -0
  98. package/providers/maplibre/src/maplibreProvider.test.js +105 -1
  99. package/providers/maplibre/src/utils/highlightFeatures.js +36 -7
  100. package/providers/maplibre/src/utils/highlightFeatures.test.js +153 -96
  101. package/providers/maplibre/src/utils/hoverCursor.js +61 -0
  102. package/providers/maplibre/src/utils/hoverCursor.test.js +130 -0
  103. package/providers/maplibre/src/utils/patternImages.js +70 -0
  104. package/providers/maplibre/src/utils/patternImages.test.js +180 -0
  105. package/providers/maplibre/src/utils/queryFeatures.js +38 -16
  106. package/providers/maplibre/src/utils/queryFeatures.test.js +20 -3
  107. package/providers/maplibre/src/utils/rasteriseToImageData.js +30 -0
  108. package/providers/maplibre/src/utils/rasteriseToImageData.test.js +69 -0
  109. package/providers/maplibre/src/utils/symbolImages.js +147 -0
  110. package/providers/maplibre/src/utils/symbolImages.test.js +248 -0
  111. package/src/App/components/Markers/Markers.jsx +122 -27
  112. package/src/App/components/Markers/Markers.module.scss +0 -10
  113. package/src/App/components/Markers/Markers.test.jsx +246 -0
  114. package/src/App/hooks/useInterfaceAPI.test.js +156 -0
  115. package/src/App/hooks/useMarkersAPI.js +2 -5
  116. package/src/App/hooks/useMarkersAPI.test.js +4 -4
  117. package/src/App/layout/Layout.jsx +2 -2
  118. package/src/App/layout/Layout.test.jsx +4 -2
  119. package/src/App/store/ServiceProvider.jsx +7 -5
  120. package/src/App/store/mapActionsMap.js +4 -6
  121. package/src/App/store/mapActionsMap.test.js +3 -2
  122. package/src/App/store/mapReducer.js +2 -1
  123. package/src/config/appConfig.js +0 -6
  124. package/src/config/appConfig.test.js +1 -2
  125. package/src/config/defaults.js +0 -2
  126. package/src/config/mapTheme.js +56 -0
  127. package/src/config/patternConfig.js +16 -0
  128. package/src/config/symbolConfig.js +80 -0
  129. package/src/scss/settings/_colors.scss +0 -9
  130. package/src/services/patternRegistry.js +40 -0
  131. package/src/services/patternRegistry.test.js +48 -0
  132. package/src/services/symbolRegistry.js +113 -0
  133. package/src/services/symbolRegistry.test.js +262 -0
  134. package/src/types.js +93 -11
  135. package/src/utils/patternUtils.js +94 -0
  136. package/src/utils/patternUtils.test.js +160 -0
  137. package/src/utils/symbolUtils.js +85 -0
  138. package/src/utils/symbolUtils.test.js +156 -0
  139. package/plugins/beta/datasets/src/adapters/maplibre/patternRegistry.js +0 -48
  140. package/plugins/beta/datasets/src/styles/patterns.js +0 -157
@@ -1,11 +1,12 @@
1
1
  // src/plugins/datasets/datasetsInit.jsx
2
2
  import { useEffect, useRef } from 'react'
3
+ import { EVENTS } from '../../../../src/config/events.js'
3
4
  import { datasetDefaults } from './defaults.js'
4
5
  import { createDatasets } from './datasets.js'
5
6
 
6
7
  export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, mapProvider, services }) {
7
8
  const { dispatch } = pluginState
8
- const { events, eventBus } = services
9
+ const { eventBus, symbolRegistry, patternRegistry } = services
9
10
 
10
11
  const isMapStyleReady = !!mapProvider.map?.getStyle()
11
12
 
@@ -40,7 +41,7 @@ export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, m
40
41
  }
41
42
 
42
43
  const { default: LayerAdapter } = await pluginConfig.layerAdapter.load()
43
- const adapter = new LayerAdapter(mapProvider.map)
44
+ const adapter = new LayerAdapter(mapProvider, symbolRegistry, patternRegistry)
44
45
 
45
46
  dispatch({ type: 'SET_LAYER_ADAPTER', payload: adapter })
46
47
 
@@ -48,9 +49,9 @@ export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, m
48
49
  adapter,
49
50
  pluginConfig,
50
51
  pluginStateRef,
51
- mapStyleId: mapState.mapStyle.id,
52
+ mapStyle: mapState.mapStyle,
52
53
  mapProvider,
53
- events,
54
+ events: EVENTS,
54
55
  eventBus
55
56
  })
56
57
  }
@@ -58,6 +59,10 @@ export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, m
58
59
  initDatasets()
59
60
  }, [isMapStyleReady, appState.mode])
60
61
 
62
+ useEffect(() => {
63
+ dispatch({ type: 'BUILD_KEY_GROUPS', payload: null })
64
+ }, [pluginState.datasets])
65
+
61
66
  // Cleanup only on unmount
62
67
  useEffect(() => {
63
68
  return () => {
@@ -1,7 +1,8 @@
1
1
  import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle.js'
2
- import { hasPattern, getPatternImageId } from '../../styles/patterns.js'
2
+ import { hasPattern, getPatternImageId } from './patternImages.js'
3
3
  import { mergeSublayer } from '../../utils/mergeSublayer.js'
4
4
  import { getSourceId, getLayerIds, getSublayerLayerIds, isDynamicSource, MAX_TILE_ZOOM } from './layerIds.js'
5
+ import { hasSymbol, getSymbolDef, getSymbolAnchor, anchorToMaplibre, getSymbolImageId } from './symbolImages.js'
5
6
 
6
7
  // ─── Source ───────────────────────────────────────────────────────────────────
7
8
 
@@ -28,14 +29,14 @@ export const addSource = (map, dataset, sourceId) => {
28
29
 
29
30
  // ─── Fill layer ───────────────────────────────────────────────────────────────
30
31
 
31
- export const addFillLayer = (map, config, layerId, sourceId, sourceLayer, visibility, mapStyleId) => {
32
+ export const addFillLayer = (map, config, layerId, sourceId, sourceLayer, visibility, { mapStyleId, patternRegistry }) => {
32
33
  if (!layerId || map.getLayer(layerId)) {
33
34
  return
34
35
  }
35
36
  if (!config.fill && !hasPattern(config)) {
36
37
  return
37
38
  }
38
- const patternImageId = hasPattern(config) ? getPatternImageId(config, mapStyleId) : null
39
+ const patternImageId = hasPattern(config) ? getPatternImageId(config, mapStyleId, patternRegistry) : null
39
40
  const paint = patternImageId
40
41
  ? { 'fill-pattern': patternImageId, 'fill-opacity': config.opacity || 1 }
41
42
  : { 'fill-color': getValueForStyle(config.fill, mapStyleId), 'fill-opacity': config.opacity || 1 }
@@ -44,6 +45,8 @@ export const addFillLayer = (map, config, layerId, sourceId, sourceLayer, visibi
44
45
  type: 'fill',
45
46
  source: sourceId,
46
47
  'source-layer': sourceLayer,
48
+ minzoom: config.minZoom,
49
+ maxzoom: config.maxZoom,
47
50
  layout: { visibility },
48
51
  paint,
49
52
  ...(config.filter ? { filter: config.filter } : {})
@@ -61,6 +64,8 @@ export const addStrokeLayer = (map, config, layerId, sourceId, sourceLayer, visi
61
64
  type: 'line',
62
65
  source: sourceId,
63
66
  'source-layer': sourceLayer,
67
+ minzoom: config.minZoom,
68
+ maxzoom: config.maxZoom,
64
69
  layout: { visibility },
65
70
  paint: {
66
71
  'line-color': getValueForStyle(config.stroke, mapStyleId),
@@ -72,15 +77,46 @@ export const addStrokeLayer = (map, config, layerId, sourceId, sourceLayer, visi
72
77
  })
73
78
  }
74
79
 
80
+ // ─── Symbol layer ─────────────────────────────────────────────────────────────
81
+
82
+ export const addSymbolLayer = (map, dataset, layerId, sourceId, sourceLayer, visibility, { mapStyle, symbolRegistry, pixelRatio }) => {
83
+ if (!layerId || map.getLayer(layerId)) { return }
84
+ const symbolDef = getSymbolDef(dataset, symbolRegistry)
85
+ if (!symbolDef) { return }
86
+ const imageId = getSymbolImageId(dataset, mapStyle, symbolRegistry, false, pixelRatio)
87
+ if (!imageId) { return }
88
+ const anchor = getSymbolAnchor(dataset, symbolDef)
89
+ map.addLayer({
90
+ id: layerId,
91
+ type: 'symbol',
92
+ source: sourceId,
93
+ 'source-layer': sourceLayer,
94
+ minzoom: dataset.minZoom,
95
+ maxzoom: dataset.maxZoom,
96
+ layout: {
97
+ visibility,
98
+ 'icon-image': imageId,
99
+ 'icon-anchor': anchorToMaplibre(anchor),
100
+ 'icon-allow-overlap': true
101
+ },
102
+ ...(dataset.filter ? { filter: dataset.filter } : {})
103
+ })
104
+ }
105
+
75
106
  // ─── Dataset layers ───────────────────────────────────────────────────────────
76
107
 
77
- export const addSublayerLayers = (map, dataset, sublayer, sourceId, sourceLayer, mapStyleId) => {
108
+ export const addSublayerLayers = (map, dataset, sublayer, sourceId, sourceLayer, { mapStyle, symbolRegistry, patternRegistry, pixelRatio }) => {
109
+ const mapStyleId = mapStyle.id
78
110
  const merged = mergeSublayer(dataset, sublayer)
79
- const { fillLayerId, strokeLayerId } = getSublayerLayerIds(dataset.id, sublayer.id)
111
+ const { fillLayerId, strokeLayerId, symbolLayerId } = getSublayerLayerIds(dataset.id, sublayer.id)
80
112
  const parentHidden = dataset.visibility === 'hidden'
81
113
  const sublayerHidden = dataset.sublayerVisibility?.[sublayer.id] === 'hidden'
82
114
  const visibility = (parentHidden || sublayerHidden) ? 'none' : 'visible'
83
- addFillLayer(map, merged, fillLayerId, sourceId, sourceLayer, visibility, mapStyleId)
115
+ if (hasSymbol(merged) && symbolRegistry) {
116
+ addSymbolLayer(map, merged, symbolLayerId, sourceId, sourceLayer, visibility, { mapStyle, symbolRegistry, pixelRatio })
117
+ return
118
+ }
119
+ addFillLayer(map, merged, fillLayerId, sourceId, sourceLayer, visibility, { mapStyleId, patternRegistry })
84
120
  addStrokeLayer(map, merged, strokeLayerId, sourceId, sourceLayer, visibility, mapStyleId)
85
121
  }
86
122
 
@@ -89,10 +125,14 @@ export const addSublayerLayers = (map, dataset, sublayer, sourceId, sourceLayer,
89
125
  * Returns the sourceId so the caller can track the datasetId → sourceId mapping.
90
126
  * @param {Object} map - MapLibre map instance
91
127
  * @param {Object} dataset
92
- * @param {string} mapStyleId
128
+ * @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor)
129
+ * @param {Object} [symbolRegistry]
130
+ * @param {Object} [patternRegistry]
131
+ * @param {number} [pixelRatio] - Device pixel ratio × map size scale factor
93
132
  * @returns {string} sourceId
94
133
  */
95
- export const addDatasetLayers = (map, dataset, mapStyleId) => {
134
+ export const addDatasetLayers = (map, dataset, mapStyle, symbolRegistry, patternRegistry, pixelRatio) => {
135
+ const mapStyleId = mapStyle.id
96
136
  const sourceId = getSourceId(dataset)
97
137
  addSource(map, dataset, sourceId)
98
138
 
@@ -100,14 +140,20 @@ export const addDatasetLayers = (map, dataset, mapStyleId) => {
100
140
 
101
141
  if (dataset.sublayers?.length) {
102
142
  dataset.sublayers.forEach(sublayer => {
103
- addSublayerLayers(map, dataset, sublayer, sourceId, sourceLayer, mapStyleId)
143
+ addSublayerLayers(map, dataset, sublayer, sourceId, sourceLayer, { mapStyle, symbolRegistry, patternRegistry, pixelRatio })
104
144
  })
105
145
  return sourceId
106
146
  }
107
147
 
108
- const { fillLayerId, strokeLayerId } = getLayerIds(dataset)
148
+ const { fillLayerId, strokeLayerId, symbolLayerId } = getLayerIds(dataset)
109
149
  const visibility = dataset.visibility === 'hidden' ? 'none' : 'visible'
110
- addFillLayer(map, dataset, fillLayerId, sourceId, sourceLayer, visibility, mapStyleId)
150
+
151
+ if (hasSymbol(dataset) && symbolRegistry) {
152
+ addSymbolLayer(map, dataset, symbolLayerId, sourceId, sourceLayer, visibility, { mapStyle, symbolRegistry, pixelRatio })
153
+ return sourceId
154
+ }
155
+
156
+ addFillLayer(map, dataset, fillLayerId, sourceId, sourceLayer, visibility, { mapStyleId, patternRegistry })
111
157
  addStrokeLayer(map, dataset, strokeLayerId, sourceId, sourceLayer, visibility, mapStyleId)
112
158
  return sourceId
113
159
  }
@@ -1,7 +1,9 @@
1
- import { hasPattern } from '../../styles/patterns.js'
1
+ import { hasPattern } from '../../../../../../src/utils/patternUtils.js'
2
2
 
3
3
  // ─── Internal helpers ─────────────────────────────────────────────────────────
4
4
 
5
+ const hasSymbol = (dataset) => !!dataset.symbol
6
+
5
7
  export const isDynamicSource = (dataset) =>
6
8
  typeof dataset.geojson === 'string' &&
7
9
  !!dataset.idProperty &&
@@ -42,28 +44,32 @@ export const getSourceId = (dataset) => {
42
44
  // ─── Layer IDs ────────────────────────────────────────────────────────────────
43
45
 
44
46
  export const getLayerIds = (dataset) => {
45
- const hasFill = !!dataset.fill || hasPattern(dataset)
47
+ if (hasSymbol(dataset)) {
48
+ return { fillLayerId: null, strokeLayerId: null, symbolLayerId: dataset.id }
49
+ }
50
+ const hasFill = (!!dataset.fill && dataset.fill !== 'transparent') || hasPattern(dataset)
46
51
  const hasStroke = !!dataset.stroke
47
52
  const fillLayerId = hasFill ? dataset.id : null
48
53
  let strokeLayerId = null
49
54
  if (hasStroke) {
50
55
  strokeLayerId = hasFill ? `${dataset.id}-stroke` : dataset.id
51
56
  }
52
- return { fillLayerId, strokeLayerId }
57
+ return { fillLayerId, strokeLayerId, symbolLayerId: null }
53
58
  }
54
59
 
55
60
  export const getSublayerLayerIds = (datasetId, sublayerId) => ({
56
61
  fillLayerId: `${datasetId}-${sublayerId}`,
57
- strokeLayerId: `${datasetId}-${sublayerId}-stroke`
62
+ strokeLayerId: `${datasetId}-${sublayerId}-stroke`,
63
+ symbolLayerId: `${datasetId}-${sublayerId}-symbol`
58
64
  })
59
65
 
60
66
  export const getAllLayerIds = (dataset) => {
61
67
  if (dataset.sublayers?.length) {
62
68
  return dataset.sublayers.flatMap(sublayer => {
63
- const { fillLayerId, strokeLayerId } = getSublayerLayerIds(dataset.id, sublayer.id)
64
- return [strokeLayerId, fillLayerId]
69
+ const { fillLayerId, strokeLayerId, symbolLayerId } = getSublayerLayerIds(dataset.id, sublayer.id)
70
+ return [strokeLayerId, fillLayerId, symbolLayerId]
65
71
  })
66
72
  }
67
- const { fillLayerId, strokeLayerId } = getLayerIds(dataset)
68
- return [strokeLayerId, fillLayerId].filter(Boolean)
73
+ const { fillLayerId, strokeLayerId, symbolLayerId } = getLayerIds(dataset)
74
+ return [strokeLayerId, fillLayerId, symbolLayerId].filter(Boolean)
69
75
  }
@@ -1,7 +1,10 @@
1
1
  import { applyExclusionFilter } from '../../utils/filters.js'
2
2
  import { getSourceId, getLayerIds, getSublayerLayerIds, getAllLayerIds } from './layerIds.js'
3
3
  import { addDatasetLayers, addSublayerLayers } from './layerBuilders.js'
4
- import { registerPatterns } from './patternRegistry.js'
4
+ import { getPatternConfigs } from './patternImages.js'
5
+ import { getSymbolConfigs, getSymbolImageId } from './symbolImages.js'
6
+ import { mergeSublayer } from '../../utils/mergeSublayer.js'
7
+ import { scaleFactor } from '../../../../../../src/config/appConfig.js'
5
8
 
6
9
  /**
7
10
  * MapLibre GL JS implementation of the LayerAdapter interface for the datasets plugin.
@@ -11,12 +14,25 @@ import { registerPatterns } from './patternRegistry.js'
11
14
  * - Pattern image registration (delegated to patternRegistry)
12
15
  * - Visibility toggling, feature filtering, style changes
13
16
  * - Style-change recovery (re-adding layers after basemap swap)
17
+ *
18
+ * Symbol image rasterisation is delegated to the map provider via
19
+ * `mapProvider.registerSymbols()`, keeping this adapter free of provider internals.
14
20
  */
15
21
  export default class MaplibreLayerAdapter {
16
- constructor (map) {
17
- this._map = map
22
+ /**
23
+ * @param {Object} mapProvider - Map provider instance (e.g. MapLibreProvider)
24
+ * @param {Object} symbolRegistry
25
+ * @param {Object} patternRegistry
26
+ */
27
+ constructor (mapProvider, symbolRegistry, patternRegistry) {
28
+ this._mapProvider = mapProvider
29
+ this._map = mapProvider.map
30
+ this._symbolRegistry = symbolRegistry
31
+ this._patternRegistry = patternRegistry
18
32
  // datasetId → sourceId, used by setData to update the correct source
19
33
  this._datasetSourceMap = new Map()
34
+ // Tracks all active symbol-type layer IDs so non-symbol layers can be kept below them
35
+ this._symbolLayerIds = new Set()
20
36
  }
21
37
 
22
38
  // ─── Lifecycle ──────────────────────────────────────────────────────────────
@@ -24,12 +40,18 @@ export default class MaplibreLayerAdapter {
24
40
  /**
25
41
  * Initialise all datasets: register patterns, add layers, then wait for idle.
26
42
  * @param {Object[]} datasets
27
- * @param {string} mapStyleId
43
+ * @param {Object} mapStyle
28
44
  * @returns {Promise<void>} Resolves once the map has processed all layers.
29
45
  */
30
- async init (datasets, mapStyleId) {
31
- await registerPatterns(this._map, datasets, mapStyleId)
32
- datasets.forEach(dataset => this._addLayers(dataset, mapStyleId))
46
+ async init (datasets, mapStyle) {
47
+ const mapStyleId = mapStyle.id
48
+ const pixelRatio = this._pixelRatio
49
+ await Promise.all([
50
+ this._mapProvider.registerPatterns(getPatternConfigs(datasets, this._patternRegistry), mapStyleId, this._patternRegistry),
51
+ this._mapProvider.registerSymbols(getSymbolConfigs(datasets), mapStyle, this._symbolRegistry)
52
+ ])
53
+ this._symbolLayerIds.clear()
54
+ datasets.forEach(dataset => this._addLayers(dataset, mapStyle, pixelRatio))
33
55
  await new Promise(resolve => this._map.once('idle', resolve))
34
56
  }
35
57
 
@@ -58,17 +80,23 @@ export default class MaplibreLayerAdapter {
58
80
  * Re-register patterns and re-add all layers after a basemap style change,
59
81
  * then reapply cached dynamic source data and hidden-feature filters.
60
82
  * @param {Object[]} datasets
61
- * @param {string} newStyleId
83
+ * @param {Object} newMapStyle
62
84
  * @param {Object} hiddenFeatures - pluginState.hiddenFeatures
63
85
  * @param {Map} dynamicSources - datasetId → dynamic source instance
64
86
  * @returns {Promise<void>}
65
87
  */
66
- async onStyleChange (datasets, newStyleId, hiddenFeatures, dynamicSources) {
88
+ async onStyleChange (datasets, newMapStyle, hiddenFeatures, dynamicSources) {
67
89
  // MapLibre wipes all sources/layers on style change — must wait for idle first
68
90
  await new Promise(resolve => this._map.once('idle', resolve))
69
91
 
70
- await registerPatterns(this._map, datasets, newStyleId)
71
- datasets.forEach(dataset => this._addLayers(dataset, newStyleId))
92
+ const newStyleId = newMapStyle.id
93
+ const pixelRatio = this._pixelRatio
94
+ await Promise.all([
95
+ this._mapProvider.registerPatterns(getPatternConfigs(datasets, this._patternRegistry), newStyleId, this._patternRegistry),
96
+ this._mapProvider.registerSymbols(getSymbolConfigs(datasets), newMapStyle, this._symbolRegistry)
97
+ ])
98
+ this._symbolLayerIds.clear()
99
+ datasets.forEach(dataset => this._addLayers(dataset, newMapStyle, pixelRatio))
72
100
 
73
101
  // Re-push cached data for dynamic sources
74
102
  dynamicSources.forEach(source => source.reapply())
@@ -83,15 +111,45 @@ export default class MaplibreLayerAdapter {
83
111
  })
84
112
  }
85
113
 
114
+ /**
115
+ * Re-register symbols at the new pixel ratio and update icon-image on all symbol layers.
116
+ * Called when the map size changes so symbols are rasterised at the correct resolution.
117
+ * @param {Object[]} datasets
118
+ * @param {Object} mapStyle
119
+ * @returns {Promise<void>}
120
+ */
121
+ async onSizeChange (datasets, mapStyle) {
122
+ await this._mapProvider.registerSymbols(getSymbolConfigs(datasets), mapStyle, this._symbolRegistry)
123
+ const pixelRatio = this._pixelRatio
124
+ datasets.forEach(dataset => {
125
+ getAllLayerIds(dataset).forEach(layerId => {
126
+ if (!this._symbolLayerIds.has(layerId) || !this._map.getLayer(layerId)) { return }
127
+ const imageId = getSymbolImageId(dataset, mapStyle, this._symbolRegistry, false, pixelRatio)
128
+ if (imageId) {
129
+ this._map.setLayoutProperty(layerId, 'icon-image', imageId)
130
+ }
131
+ })
132
+ dataset.sublayers?.forEach(sublayer => {
133
+ const { symbolLayerId } = getSublayerLayerIds(dataset.id, sublayer.id)
134
+ if (!this._map.getLayer(symbolLayerId)) { return }
135
+ const merged = mergeSublayer(dataset, sublayer)
136
+ const imageId = getSymbolImageId(merged, mapStyle, this._symbolRegistry, false, pixelRatio)
137
+ if (imageId) {
138
+ this._map.setLayoutProperty(symbolLayerId, 'icon-image', imageId)
139
+ }
140
+ })
141
+ })
142
+ }
143
+
86
144
  // ─── Dataset operations ─────────────────────────────────────────────────────
87
145
 
88
146
  /**
89
147
  * Add a single dataset's source and layers to the map.
90
148
  * @param {Object} dataset
91
- * @param {string} mapStyleId
149
+ * @param {Object} mapStyle
92
150
  */
93
- addDataset (dataset, mapStyleId) {
94
- this._addLayers(dataset, mapStyleId)
151
+ addDataset (dataset, mapStyle) {
152
+ this._addLayers(dataset, mapStyle, this._pixelRatio)
95
153
  }
96
154
 
97
155
  /**
@@ -108,6 +166,7 @@ export default class MaplibreLayerAdapter {
108
166
  if (this._map.getLayer(layerId)) {
109
167
  this._map.removeLayer(layerId)
110
168
  }
169
+ this._symbolLayerIds.delete(layerId)
111
170
  })
112
171
 
113
172
  const sourceIsShared = allDatasets.some(d => d.id !== dataset.id && getSourceId(d) === sourceId)
@@ -141,13 +200,12 @@ export default class MaplibreLayerAdapter {
141
200
  * @param {string} sublayerId
142
201
  */
143
202
  showSublayer (datasetId, sublayerId) {
144
- const { fillLayerId, strokeLayerId } = getSublayerLayerIds(datasetId, sublayerId)
145
- if (this._map.getLayer(fillLayerId)) {
146
- this._map.setLayoutProperty(fillLayerId, 'visibility', 'visible')
147
- }
148
- if (this._map.getLayer(strokeLayerId)) {
149
- this._map.setLayoutProperty(strokeLayerId, 'visibility', 'visible')
150
- }
203
+ const { fillLayerId, strokeLayerId, symbolLayerId } = getSublayerLayerIds(datasetId, sublayerId)
204
+ ;[fillLayerId, strokeLayerId, symbolLayerId].forEach(layerId => {
205
+ if (this._map.getLayer(layerId)) {
206
+ this._map.setLayoutProperty(layerId, 'visibility', 'visible')
207
+ }
208
+ })
151
209
  }
152
210
 
153
211
  /**
@@ -156,13 +214,12 @@ export default class MaplibreLayerAdapter {
156
214
  * @param {string} sublayerId
157
215
  */
158
216
  hideSublayer (datasetId, sublayerId) {
159
- const { fillLayerId, strokeLayerId } = getSublayerLayerIds(datasetId, sublayerId)
160
- if (this._map.getLayer(fillLayerId)) {
161
- this._map.setLayoutProperty(fillLayerId, 'visibility', 'none')
162
- }
163
- if (this._map.getLayer(strokeLayerId)) {
164
- this._map.setLayoutProperty(strokeLayerId, 'visibility', 'none')
165
- }
217
+ const { fillLayerId, strokeLayerId, symbolLayerId } = getSublayerLayerIds(datasetId, sublayerId)
218
+ ;[fillLayerId, strokeLayerId, symbolLayerId].forEach(layerId => {
219
+ if (this._map.getLayer(layerId)) {
220
+ this._map.setLayoutProperty(layerId, 'visibility', 'none')
221
+ }
222
+ })
166
223
  }
167
224
 
168
225
  // ─── Feature operations ─────────────────────────────────────────────────────
@@ -190,42 +247,54 @@ export default class MaplibreLayerAdapter {
190
247
  /**
191
248
  * Update a dataset's style and re-render all its layers.
192
249
  * @param {Object} dataset - Updated dataset (style changes already merged in)
193
- * @param {string} mapStyleId
250
+ * @param {Object} mapStyle
194
251
  * @returns {Promise<void>}
195
252
  */
196
- async setStyle (dataset, mapStyleId) {
253
+ async setStyle (dataset, mapStyle) {
254
+ const mapStyleId = mapStyle.id
255
+ const pixelRatio = this._pixelRatio
197
256
  getAllLayerIds(dataset).forEach(layerId => {
198
257
  if (this._map.getLayer(layerId)) {
199
258
  this._map.removeLayer(layerId)
200
259
  }
260
+ this._symbolLayerIds.delete(layerId)
201
261
  })
202
- await registerPatterns(this._map, [dataset], mapStyleId)
203
- this._addLayers(dataset, mapStyleId)
262
+ await Promise.all([
263
+ this._mapProvider.registerPatterns(getPatternConfigs([dataset], this._patternRegistry), mapStyleId, this._patternRegistry),
264
+ this._mapProvider.registerSymbols(getSymbolConfigs([dataset]), mapStyle, this._symbolRegistry)
265
+ ])
266
+ this._addLayers(dataset, mapStyle, pixelRatio)
204
267
  }
205
268
 
206
269
  /**
207
270
  * Update a single sublayer's style and re-render its layers.
208
271
  * @param {Object} dataset - Updated dataset (sublayer style changes already merged in)
209
272
  * @param {string} sublayerId
210
- * @param {string} mapStyleId
273
+ * @param {Object} mapStyle
211
274
  * @returns {Promise<void>}
212
275
  */
213
- async setSublayerStyle (dataset, sublayerId, mapStyleId) {
214
- const { fillLayerId, strokeLayerId } = getSublayerLayerIds(dataset.id, sublayerId)
215
- if (this._map.getLayer(fillLayerId)) {
216
- this._map.removeLayer(fillLayerId)
217
- }
218
- if (this._map.getLayer(strokeLayerId)) {
219
- this._map.removeLayer(strokeLayerId)
220
- }
276
+ async setSublayerStyle (dataset, sublayerId, mapStyle) {
277
+ const mapStyleId = mapStyle.id
278
+ const pixelRatio = this._pixelRatio
279
+ const { fillLayerId, strokeLayerId, symbolLayerId } = getSublayerLayerIds(dataset.id, sublayerId)
280
+ ;[fillLayerId, strokeLayerId, symbolLayerId].forEach(layerId => {
281
+ if (this._map.getLayer(layerId)) {
282
+ this._map.removeLayer(layerId)
283
+ }
284
+ this._symbolLayerIds.delete(layerId)
285
+ })
221
286
  const sublayer = dataset.sublayers?.find(s => s.id === sublayerId)
222
287
  if (!sublayer) {
223
288
  return
224
289
  }
225
- await registerPatterns(this._map, [dataset], mapStyleId)
290
+ await Promise.all([
291
+ this._mapProvider.registerPatterns(getPatternConfigs([dataset], this._patternRegistry), mapStyleId, this._patternRegistry),
292
+ this._mapProvider.registerSymbols(getSymbolConfigs([dataset]), mapStyle, this._symbolRegistry)
293
+ ])
226
294
  const sourceId = this._datasetSourceMap.get(dataset.id)
227
295
  const sourceLayer = dataset.tiles?.length ? dataset.sourceLayer : undefined
228
- addSublayerLayers(this._map, dataset, sublayer, sourceId, sourceLayer, mapStyleId)
296
+ addSublayerLayers(this._map, dataset, sublayer, sourceId, sourceLayer, { mapStyle, symbolRegistry: this._symbolRegistry, patternRegistry: this._patternRegistry, pixelRatio })
297
+ this._maintainSymbolOrdering(dataset)
229
298
  }
230
299
 
231
300
  /**
@@ -275,9 +344,43 @@ export default class MaplibreLayerAdapter {
275
344
 
276
345
  // ─── Private ─────────────────────────────────────────────────────────────────
277
346
 
278
- _addLayers (dataset, mapStyleId) {
279
- const sourceId = addDatasetLayers(this._map, dataset, mapStyleId)
347
+ get _pixelRatio () {
348
+ return this._mapProvider.map.getPixelRatio() * (scaleFactor[this._mapProvider.mapSize] || 1)
349
+ }
350
+
351
+ _addLayers (dataset, mapStyle, pixelRatio) {
352
+ const sourceId = addDatasetLayers(this._map, dataset, mapStyle, this._symbolRegistry, this._patternRegistry, pixelRatio)
280
353
  this._datasetSourceMap.set(dataset.id, sourceId)
354
+ this._maintainSymbolOrdering(dataset)
355
+ }
356
+
357
+ _getFirstSymbolLayerId () {
358
+ const style = this._map.getStyle()
359
+ if (!style?.layers) {
360
+ return null
361
+ }
362
+ const layer = style.layers.find(l => this._symbolLayerIds.has(l.id))
363
+ return layer?.id ?? null
364
+ }
365
+
366
+ _maintainSymbolOrdering (dataset) {
367
+ const layerIds = getAllLayerIds(dataset).filter(id => id && this._map.getLayer(id))
368
+ layerIds.forEach(id => {
369
+ if (this._map.getLayer(id)?.type === 'symbol') {
370
+ this._symbolLayerIds.add(id)
371
+ } else {
372
+ this._symbolLayerIds.delete(id)
373
+ }
374
+ })
375
+ const firstSymbolId = this._getFirstSymbolLayerId()
376
+ if (!firstSymbolId) {
377
+ return
378
+ }
379
+ layerIds.forEach(id => {
380
+ if (!this._symbolLayerIds.has(id)) {
381
+ this._map.moveLayer(id, firstSymbolId)
382
+ }
383
+ })
281
384
  }
282
385
 
283
386
  _setDatasetVisibility (datasetId, visibility) {
@@ -307,14 +410,12 @@ export default class MaplibreLayerAdapter {
307
410
  })
308
411
  return
309
412
  }
310
- const { fillLayerId, strokeLayerId } = getLayerIds(dataset)
413
+ const { fillLayerId, strokeLayerId, symbolLayerId } = getLayerIds(dataset)
311
414
  const originalFilter = dataset.filter || null
312
- if (fillLayerId) {
313
- applyExclusionFilter(this._map, fillLayerId, originalFilter, idProperty, excludeIds)
314
- }
315
- if (strokeLayerId) {
316
- applyExclusionFilter(this._map, strokeLayerId, originalFilter, idProperty, excludeIds)
317
- }
415
+ const layerIds = [fillLayerId, strokeLayerId, symbolLayerId].filter(Boolean)
416
+ layerIds.forEach(layerId => {
417
+ applyExclusionFilter(this._map, layerId, originalFilter, idProperty, excludeIds)
418
+ })
318
419
  }
319
420
 
320
421
  _setPaintOpacity (layerId, opacity) {
@@ -322,7 +423,8 @@ export default class MaplibreLayerAdapter {
322
423
  if (!layer) {
323
424
  return
324
425
  }
325
- const prop = layer.type === 'line' ? 'line-opacity' : 'fill-opacity'
426
+ const opacityProps = { line: 'line-opacity', symbol: 'icon-opacity' }
427
+ const prop = opacityProps[layer.type] || 'fill-opacity'
326
428
  this._map.setPaintProperty(layerId, prop, opacity)
327
429
  }
328
430
 
@@ -0,0 +1,27 @@
1
+ // Re-export pure pattern utilities from core — no map engine dependency.
2
+ import { hasPattern } from '../../../../../../src/utils/patternUtils.js'
3
+ import { mergeSublayer } from '../../utils/mergeSublayer.js'
4
+
5
+ export { hasPattern, getPatternInnerContent, getPatternImageId, getKeyPatternPaths, injectColors } from '../../../../../../src/utils/patternUtils.js'
6
+
7
+ /**
8
+ * Returns a flat list of datasets and merged sublayers that require pattern images.
9
+ * Handles sublayer merging so callers (e.g. mapProvider.registerPatterns) receive ready-to-use configs.
10
+ *
11
+ * @param {Object[]} datasets
12
+ * @param {Object} patternRegistry
13
+ * @returns {Object[]}
14
+ */
15
+ export const getPatternConfigs = (datasets, patternRegistry) =>
16
+ datasets.flatMap(dataset => {
17
+ const configs = hasPattern(dataset) ? [dataset] : []
18
+ if (dataset.sublayers?.length) {
19
+ dataset.sublayers.forEach(sublayer => {
20
+ const merged = mergeSublayer(dataset, sublayer)
21
+ if (hasPattern(merged)) {
22
+ configs.push(merged)
23
+ }
24
+ })
25
+ }
26
+ return configs
27
+ })
@@ -0,0 +1,31 @@
1
+ // Re-export pure symbol resolution utilities from core — no map engine dependency.
2
+ import { hasSymbol } from '../../../../../../src/utils/symbolUtils.js'
3
+ import { mergeSublayer } from '../../utils/mergeSublayer.js'
4
+
5
+ export { hasSymbol, getSymbolDef, getSymbolStyleColors, getSymbolViewBox, getSymbolAnchor } from '../../../../../../src/utils/symbolUtils.js'
6
+
7
+ // Re-export MapLibre-specific symbol utilities from the provider.
8
+ // This is the single cross-boundary import in the adapter; in a separate-package
9
+ // setup this would be: '@interactive-map/maplibre-provider/utils/symbolImages'
10
+ export { anchorToMaplibre, getSymbolImageId } from '../../../../../../providers/maplibre/src/utils/symbolImages.js'
11
+
12
+ /**
13
+ * Returns a flat list of datasets and merged sublayers that require symbol images.
14
+ * Handles sublayer merging so callers (e.g. mapProvider.registerSymbols) receive ready-to-use configs.
15
+ *
16
+ * @param {Object[]} datasets
17
+ * @returns {Object[]}
18
+ */
19
+ export const getSymbolConfigs = (datasets) =>
20
+ datasets.flatMap(dataset => {
21
+ const configs = hasSymbol(dataset) ? [dataset] : []
22
+ if (dataset.sublayers?.length) {
23
+ dataset.sublayers.forEach(sublayer => {
24
+ const merged = mergeSublayer(dataset, sublayer)
25
+ if (hasSymbol(merged)) {
26
+ configs.push(merged)
27
+ }
28
+ })
29
+ }
30
+ return configs
31
+ })
@@ -1,6 +1,6 @@
1
1
  import { datasetDefaults } from '../defaults.js'
2
2
 
3
3
  export const addDataset = ({ pluginState, mapState }, dataset) => {
4
- pluginState.layerAdapter?.addDataset({ ...datasetDefaults, ...dataset }, mapState.mapStyle.id)
4
+ pluginState.layerAdapter?.addDataset({ ...datasetDefaults, ...dataset }, mapState.mapStyle)
5
5
  pluginState.dispatch({ type: 'ADD_DATASET', payload: { dataset, datasetDefaults } })
6
6
  }
@@ -1,7 +1,9 @@
1
- export const setData = ({ pluginState, services }, geojson, { datasetId }) => {
1
+ import { logger } from '../../../../../src/services/logger.js'
2
+
3
+ export const setData = ({ pluginState }, geojson, { datasetId }) => {
2
4
  const dataset = pluginState.datasets?.find(d => d.id === datasetId)
3
5
  if (dataset?.tiles) {
4
- services.logger.warn(`setData called on vector tile dataset "${datasetId}" — has no effect`)
6
+ logger.warn(`setData called on vector tile dataset "${datasetId}" — has no effect`)
5
7
  return
6
8
  }
7
9
  pluginState.layerAdapter?.setData(datasetId, geojson)
@@ -12,11 +12,11 @@ export const setStyle = ({ pluginState, mapState }, style, { datasetId, sublayer
12
12
  sublayer.id === sublayerId ? { ...sublayer, style: { ...sublayer.style, ...style } } : sublayer
13
13
  )
14
14
  }
15
- pluginState.layerAdapter?.setSublayerStyle(updatedSublayerDataset, sublayerId, mapState.mapStyle.id)
15
+ pluginState.layerAdapter?.setSublayerStyle(updatedSublayerDataset, sublayerId, mapState.mapStyle)
16
16
  return
17
17
  }
18
18
 
19
19
  pluginState.dispatch({ type: 'SET_DATASET_STYLE', payload: { datasetId, styleChanges: style } })
20
20
  const updatedDataset = { ...dataset, ...style }
21
- pluginState.layerAdapter?.setStyle(updatedDataset, mapState.mapStyle.id)
21
+ pluginState.layerAdapter?.setStyle(updatedDataset, mapState.mapStyle)
22
22
  }