From 688b4e23fd98c986decb597aa3725f43329a1028 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 12 Jun 2026 16:13:10 +0100 Subject: [PATCH 01/53] IM-360 added isBaseMapReady to providers - OL is a TODO --- .../beta/datasets/src/initialise/DatasetsInit.jsx | 6 +++--- providers/beta/esri/src/esriProvider.js | 13 ++++++++++++- providers/beta/esri/src/mapEvents.js | 1 + providers/beta/openlayers/src/openlayersProvider.js | 4 +++- providers/mapProvider.js | 5 +++++ providers/maplibre/src/maplibreProvider.js | 9 +++++++-- 6 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 providers/mapProvider.js diff --git a/plugins/beta/datasets/src/initialise/DatasetsInit.jsx b/plugins/beta/datasets/src/initialise/DatasetsInit.jsx index e1968af5c..418ed7dea 100755 --- a/plugins/beta/datasets/src/initialise/DatasetsInit.jsx +++ b/plugins/beta/datasets/src/initialise/DatasetsInit.jsx @@ -9,7 +9,7 @@ import { loadLayerAdapter, layerAdapter } from '../adapters/loadLayerAdapter.js' export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, mapProvider, services }) { const { dispatch } = pluginState const { eventBus, symbolRegistry, patternRegistry } = services - const isMapStyleReady = !!mapProvider.map?.getStyle() + const isBaseMapReady = Boolean(mapProvider?.isBaseMapReady()) // Keep a ref to the latest pluginState so event handlers can access current data const pluginStateRef = useRef(pluginState) @@ -22,7 +22,7 @@ export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, m const inModeWhitelist = pluginConfig.includeModes?.includes(appState.mode) ?? true const inExcludeModes = pluginConfig.excludeModes?.includes(appState.mode) ?? false - if (!isMapStyleReady || !inModeWhitelist || inExcludeModes) { + if (!isBaseMapReady || !inModeWhitelist || inExcludeModes) { return } @@ -47,7 +47,7 @@ export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, m } initDatasets() - }, [isMapStyleReady, appState.mode]) + }, [isBaseMapReady, appState.mode]) useEffect(() => datasetRegistry.attach(pluginState.mappedDatasets, pluginState.orderedDatasets), [pluginState.mappedDatasets, pluginState.orderedDatasets]) diff --git a/providers/beta/esri/src/esriProvider.js b/providers/beta/esri/src/esriProvider.js index 384a063b2..047bf47eb 100644 --- a/providers/beta/esri/src/esriProvider.js +++ b/providers/beta/esri/src/esriProvider.js @@ -1,4 +1,5 @@ // src/plugins/mapStyles/EsriProvider.jsx +import { MapProvider } from '../../../mapProvider.js' import './esriProvider.scss' import esriConfig from '@arcgis/core/config.js' import TileInfo from '@arcgis/core/layers/support/TileInfo.js' @@ -14,8 +15,9 @@ import { queryVectorTileFeatures } from './utils/query.js' import { getExtentFromFlatCoords, getPointFromFlatCoords, getBboxFromGeoJSON } from './utils/coords.js' import { cleanDOM } from './utils/esriFixes.js' -export default class EsriProvider { +export default class EsriProvider extends MapProvider { constructor ({ mapProviderConfig = {}, events, eventBus }) { + super() this.events = events this.eventBus = eventBus this.capabilities = { @@ -102,6 +104,15 @@ export default class EsriProvider { this.baseTileLayer = baseTileLayer } + _isBaseMapReady = false + setBaseMapReady (ready) { + this._isBaseMapReady = ready + } + + isBaseMapReady () { + return this._isBaseMapReady + } + destroyMap () { this.mapEvents?.remove() this.appEvents?.remove() diff --git a/providers/beta/esri/src/mapEvents.js b/providers/beta/esri/src/mapEvents.js index a790113d4..ea0288443 100644 --- a/providers/beta/esri/src/mapEvents.js +++ b/providers/beta/esri/src/mapEvents.js @@ -54,6 +54,7 @@ export function attachMapEvents ({ // ready once(() => view.ready).then(() => { if (!destroyed) { + mapProvider.setBaseMapReady(true) eventBus.emit(events.MAP_READY, { map: mapProvider.map, view: mapProvider.view, diff --git a/providers/beta/openlayers/src/openlayersProvider.js b/providers/beta/openlayers/src/openlayersProvider.js index decfa66c6..54ddd624d 100644 --- a/providers/beta/openlayers/src/openlayersProvider.js +++ b/providers/beta/openlayers/src/openlayersProvider.js @@ -1,3 +1,4 @@ +import { MapProvider } from '../../../mapProvider.js' import OlMap from 'ol/Map.js' import View from 'ol/View.js' import { defaults as defaultInteractions } from 'ol/interaction/defaults.js' @@ -30,8 +31,9 @@ const toPaddingArray = (padding) => { proj4.defs(CRS, '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs') register(proj4) -export default class OpenLayersProvider { +export default class OpenLayersProvider extends MapProvider { constructor ({ mapProviderConfig = {}, events, eventBus }) { + super() this.events = events this.eventBus = eventBus this.capabilities = { diff --git a/providers/mapProvider.js b/providers/mapProvider.js new file mode 100644 index 000000000..19a55201e --- /dev/null +++ b/providers/mapProvider.js @@ -0,0 +1,5 @@ +export class MapProvider { + isBaseMapReady () { + throw new Error(this.name + ' must implement isBaseMapReady()') + } +} diff --git a/providers/maplibre/src/maplibreProvider.js b/providers/maplibre/src/maplibreProvider.js index 9140914a3..f69eb04c2 100755 --- a/providers/maplibre/src/maplibreProvider.js +++ b/providers/maplibre/src/maplibreProvider.js @@ -2,7 +2,7 @@ * @typedef {import('../../../src/types.js').MapProvider} MapProvider * @typedef {import('../../../src/types.js').MapProviderConfig} MapProviderConfig */ - +import { MapProvider } from '../../mapProvider.js' import { DEFAULTS, supportedShortcuts } from './defaults.js' import { cleanCanvas, applyPreventDefaultFix } from './utils/maplibreFixes.js' import { attachMapEvents } from './mapEvents.js' @@ -20,7 +20,7 @@ import { addPatternsToMap } from './utils/patternImages.js' * * @implements {MapProvider} */ -export default class MapLibreProvider { +export default class MapLibreProvider extends MapProvider { /** * @param {Object} options - Constructor options. * @param {any} options.mapFramework - The MapLibre GL JS module. @@ -29,6 +29,7 @@ export default class MapLibreProvider { * @param {Object} options.eventBus - Event emitter for publishing map events. */ constructor ({ mapFramework, mapProviderConfig = {}, events, eventBus }) { + super() this.maplibreModule = mapFramework this.events = events this.eventBus = eventBus @@ -118,6 +119,10 @@ export default class MapLibreProvider { }) } + isBaseMapReady () { + return this.map?.getStyle() + } + /** Destroy the map and clean up resources. */ destroyMap () { this.setHoverCursor([]) From 28a2415e272025d39ad488fd20989bd1bdae0b64 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 12 Jun 2026 16:14:50 +0100 Subject: [PATCH 02/53] IM-360 Added a stub implementation of EsriLayerAdapter --- .../src/adapters/esri/esriLayerAdapter.js | 53 +++++++++++++++++++ .../datasets/src/adapters/loadLayerAdapter.js | 8 ++- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js new file mode 100644 index 000000000..16ca3decf --- /dev/null +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -0,0 +1,53 @@ +import { datasetRegistry } from '../../registry/datasetRegistry.js' + +export default class EsriLayerAdapter { + async init (mapStyle) { + console.log('EsriLayerAdapter init', mapStyle) + // datasetRegistry.forEachDataset(registryDataset => this._addLayers(registryDataset, mapStyle)) + // await new Promise(resolve => this._map.once('idle', resolve)) + } + + async removeDataset (...args) { + console.log('ESRI: removeDataset', args) + } + + async setData (...args) { + console.log('ESRI: setData', args) + } + + async applyStyle (...args) { + console.log('ESRI: applyStyle', args) + } + + async applyDatasetVisibility (...args) { + console.log('ESRI: applyDatasetVisibility', args) + } + + async applyGlobalVisibility (...args) { + console.log('ESRI: applyGlobalVisibility', args) + } + + async applyDatasetOpacity (...args) { + console.log('ESRI: applyDatasetOpacity', args) + } + + async applyGlobalOpacity (...args) { + console.log('ESRI: applyGlobalOpacity', args) + } + + async addDataset (...args) { + console.log('ESRI: addDataset', args) + } + + async applyFeatureFilter (...args) { + console.log('ESRI: applyFeatureFilter', args) + } + + async onMapStyleChange (...args) { + console.log('ESRI: onMapStyleChange', args) + } + + async onMapSizeChange (...args) { + console.log('ESRI: onMapSizeChange', args) + } +} diff --git a/plugins/beta/datasets/src/adapters/loadLayerAdapter.js b/plugins/beta/datasets/src/adapters/loadLayerAdapter.js index 1f24d8981..78dd8e3a0 100644 --- a/plugins/beta/datasets/src/adapters/loadLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/loadLayerAdapter.js @@ -4,7 +4,11 @@ const importLayerAdapter = async (mapProvider) => { const { default: LayerAdapter } = await import(/* webpackChunkName: "im-datasets-ml-adapter" */ './maplibre/maplibreLayerAdapter.js') return LayerAdapter } - // TODO: add cases for EsriProvider, OpenLayersProvider and potentially LeafletProvider + case 'EsriProvider': { + const { default: LayerAdapter } = await import(/* webpackChunkName: "im-datasets-esri-adapter" */ './esri/esriLayerAdapter.js') + return LayerAdapter + } + // TODO: add cases for OpenLayersProvider and potentially LeafletProvider default: { throw new Error(`No layer adapter available for map provider ${mapProvider.name}. Please provide a compatible layer adapter.`) } @@ -28,6 +32,8 @@ export const loadLayerAdapter = async (mapProvider, symbolRegistry, patternRegis layerAdapter.applyGlobalOpacity = _layerAdapter.applyGlobalOpacity.bind(_layerAdapter) layerAdapter.addDataset = _layerAdapter.addDataset.bind(_layerAdapter) layerAdapter.applyFeatureFilter = _layerAdapter.applyFeatureFilter.bind(_layerAdapter) + layerAdapter.onMapStyleChange = _layerAdapter.onMapStyleChange?.bind(_layerAdapter) + layerAdapter.onMapSizeChange = _layerAdapter.onMapSizeChange?.bind(_layerAdapter) return _layerAdapter } From 541c9731918b30b8cab373f6e193e2e74e7e75a4 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Mon, 15 Jun 2026 09:21:29 +0100 Subject: [PATCH 03/53] IM-360 added an esri-datasets demo --- demo/esri-datasets.html | 15 ++++++++++++++ demo/js/esri-datasets.js | 45 ++++++++++++++++++++++++++++++++++++++++ webpack.dev.mjs | 1 + 3 files changed, 61 insertions(+) create mode 100644 demo/esri-datasets.html create mode 100644 demo/js/esri-datasets.js diff --git a/demo/esri-datasets.html b/demo/esri-datasets.html new file mode 100644 index 000000000..7174c3d31 --- /dev/null +++ b/demo/esri-datasets.html @@ -0,0 +1,15 @@ + + + + + + React TypeScript Webpack App + + + + + +
+ + + \ No newline at end of file diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js new file mode 100644 index 000000000..d9b5a4362 --- /dev/null +++ b/demo/js/esri-datasets.js @@ -0,0 +1,45 @@ +import InteractiveMap from '../../src/index.js' +import esriProvider from '/providers/beta/esri/src/index.js' +// Plugins +import mapStylesPlugin from '/plugins/beta/map-styles/src/index.js' +import createDatasetsPlugin from '/plugins/beta/datasets/src/index.js' +// Setup +import { vtsMapStyles27700 } from './mapStyles.js' +import { transformGeocodeRequest, transformVtsRequest3857, setupEsriConfig } from './auth.js' + +const datasetsPlugin = createDatasetsPlugin({ + globals: { + opacityMode: 'global', // 'dataset', 'global' or 'multiply' + opacity: 0.75, + visible: true + }, + datasets: [] +}) + +const interactiveMap = new InteractiveMap('map', { + behaviour: 'mapOnly', + mapProvider: esriProvider({ setupConfig: setupEsriConfig }), + minZoom: 6, + maxZoom: 20, + autoColorScheme: true, + center: [481146,484971], + zoom: 13, + plugins: [ + datasetsPlugin, + mapStylesPlugin({ + mapStyles: vtsMapStyles27700, + manifest: { + buttons: [{ + id: 'mapStyles', + desktop: { slot: 'right-top', showLabel: true } + }], + panels: [ + { + id: 'mapStyles', + desktop: { slot: 'map-styles-button', width: '400px', modal: true } + } + ] + } + }) + ] +}) \ No newline at end of file diff --git a/webpack.dev.mjs b/webpack.dev.mjs index d315afa65..2e4bd9be7 100755 --- a/webpack.dev.mjs +++ b/webpack.dev.mjs @@ -25,6 +25,7 @@ export default { 'planning-ol': path.join(__dirname, 'demo/js/planning-ol.js'), gep: path.join(__dirname, 'demo/js/gep.js'), 'ml-datasets': path.join(__dirname, 'demo/js/ml-datasets.js'), + 'esri-datasets': path.join(__dirname, 'demo/js/esri-datasets.js'), esm: path.join(__dirname, 'demo/js/esm.js'), multimap: path.join(__dirname, 'demo/js/multimap.js') }, From 6271a7f47359e5bd3f47879e24ceb18d598507de Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Mon, 15 Jun 2026 12:10:55 +0100 Subject: [PATCH 04/53] IM-360 Very basic flood zone layer rendering --- demo/js/esri-datasets.js | 43 +++++++++++++++++-- .../src/adapters/esri/esriLayerAdapter.js | 20 ++++++++- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index d9b5a4362..7591a5128 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -7,15 +7,52 @@ import createDatasetsPlugin from '/plugins/beta/datasets/src/index.js' import { vtsMapStyles27700 } from './mapStyles.js' import { transformGeocodeRequest, transformVtsRequest3857, setupEsriConfig } from './auth.js' +const datasets = [ + { + id: 'flood-zones', + label: 'Flood Zones', + groupLabel: 'Datasets', + tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_NON_PRODUCTION/VectorTileServer`, + showInKey: true, + showInMenu: true, + sublayers: [ + { + id: 'flood-zone-2', + label: 'Flood Zone 2', + styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 2/1', + showInKey: true, + showInMenu: false, + style: { + fill: { outdoor: '#1d70b8', dark: '#7fcdbb' }, + stroke: 'none' + }, + }, + { + id: 'flood-zone-3', + label: 'Flood Zone 3', + styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 3/1', + showInKey: true, + showInMenu: false, + style: { + fill: { outdoor: '#003078', dark: '#e5f5e0' }, + stroke: 'none' + }, + } + ] + } +] + const datasetsPlugin = createDatasetsPlugin({ globals: { opacityMode: 'global', // 'dataset', 'global' or 'multiply' opacity: 0.75, visible: true }, - datasets: [] + datasets }) + + const interactiveMap = new InteractiveMap('map', { behaviour: 'mapOnly', mapProvider: esriProvider({ setupConfig: setupEsriConfig }), @@ -29,8 +66,8 @@ const interactiveMap = new InteractiveMap('map', { mapStylesPlugin({ mapStyles: vtsMapStyles27700, manifest: { - buttons: [{ - id: 'mapStyles', + buttons: [{ + id: 'mapStyles', desktop: { slot: 'right-top', showLabel: true } }], panels: [ diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index 16ca3decf..94526656c 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -1,10 +1,26 @@ +import VectorTileLayer from '@arcgis/core/layers/VectorTileLayer.js' import { datasetRegistry } from '../../registry/datasetRegistry.js' export default class EsriLayerAdapter { + constructor (mapProvider, symbolRegistry, patternRegistry) { + this._mapProvider = mapProvider + this._map = mapProvider.map + // TODO: Implement symbolRegistry and patternRegistry usage in the adapter + } + async init (mapStyle) { console.log('EsriLayerAdapter init', mapStyle) - // datasetRegistry.forEachDataset(registryDataset => this._addLayers(registryDataset, mapStyle)) - // await new Promise(resolve => this._map.once('idle', resolve)) + datasetRegistry.forEachDataset(registryDataset => this._addLayers(registryDataset, mapStyle)) + } + + _addLayers (registryDataset, mapStyle) { + const vectorTileLayer = new VectorTileLayer({ + id: registryDataset.id, + url: registryDataset.tiles, + opacity: 1, + visible: registryDataset.visible + }) + this._map.add(vectorTileLayer) } async removeDataset (...args) { From 792f4effc1041028477e1cec632f7b2dc8efd340 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Mon, 15 Jun 2026 14:08:44 +0100 Subject: [PATCH 05/53] IM-360 added flood-zones-cc --- demo/js/esri-datasets.js | 35 +++++++++++++++++++ .../src/adapters/esri/esriLayerAdapter.js | 3 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 7591a5128..62c27f537 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -8,10 +8,45 @@ import { vtsMapStyles27700 } from './mapStyles.js' import { transformGeocodeRequest, transformVtsRequest3857, setupEsriConfig } from './auth.js' const datasets = [ + { + id: 'flood-zones-cc', + label: 'Flood Zones Climate Change', + groupLabel: 'Datasets', + groupId: 'flood-zones', + tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_CCP1_NON_PRODUCTION/VectorTileServer`, + showInKey: true, + showInMenu: true, + sublayers: [ + { + id: 'flood-zones', + label: 'Climate change (2070 to 2125)', + styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Flood Zones plus climate change/1', + showInKey: true, + showInMenu: false, + style: { + fill: { outdoor: '#F4A582', dark: '#BF3D4A' }, + stroke: 'none' + }, + }, + { + id: 'data-unavailable', + label: 'Climate change data unavailable', + styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/1', + showInKey: true, + showInMenu: false, + style: { + fillPattern: 'dot', + fillPatternForegroundColor: { outdoor: '#000000', dark: '#ffffff' }, + stroke: { outdoor: '#000000', dark: '#ffffff' }, + }, + } + ] + }, { id: 'flood-zones', label: 'Flood Zones', groupLabel: 'Datasets', + groupId: 'flood-zones', tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_NON_PRODUCTION/VectorTileServer`, showInKey: true, showInMenu: true, diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index 94526656c..fa9d1b63c 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -9,11 +9,12 @@ export default class EsriLayerAdapter { } async init (mapStyle) { - console.log('EsriLayerAdapter init', mapStyle) + // TODO - move some of this into a super LayerAdapter class that this extends datasetRegistry.forEachDataset(registryDataset => this._addLayers(registryDataset, mapStyle)) } _addLayers (registryDataset, mapStyle) { + console.log('Creating Esri VectorTileLayer for dataset', registryDataset.id) const vectorTileLayer = new VectorTileLayer({ id: registryDataset.id, url: registryDataset.tiles, From e2b7f9d0e7723b62885a528182044ffa64078404 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Tue, 16 Jun 2026 12:07:20 +0100 Subject: [PATCH 06/53] IM-360 wip - setting esri styles at runTime --- demo/js/esri-datasets.js | 16 ++++- .../src/adapters/esri/esriLayerAdapter.js | 60 ++++++++++++++++++- .../maplibre/registry/mapLibreDataset.js | 2 - plugins/beta/datasets/src/registry/dataset.js | 3 + 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 62c27f537..8280d061b 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -16,14 +16,17 @@ const datasets = [ tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_CCP1_NON_PRODUCTION/VectorTileServer`, showInKey: true, showInMenu: true, + removeStyles: true, + sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea CCP1', sublayers: [ { - id: 'flood-zones', + id: 'climate-change', label: 'Climate change (2070 to 2125)', styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Flood Zones plus climate change/1', showInKey: true, showInMenu: false, style: { + filter: ['==', '_symbol', 0], fill: { outdoor: '#F4A582', dark: '#BF3D4A' }, stroke: 'none' }, @@ -35,8 +38,10 @@ const datasets = [ showInKey: true, showInMenu: false, style: { + filter: ['==', '_symbol', 1], fillPattern: 'dot', fillPatternForegroundColor: { outdoor: '#000000', dark: '#ffffff' }, + fill: { outdoor: '#0000ff', dark: '#BF3D4A' }, stroke: { outdoor: '#000000', dark: '#ffffff' }, }, } @@ -50,6 +55,7 @@ const datasets = [ tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_NON_PRODUCTION/VectorTileServer`, showInKey: true, showInMenu: true, + sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea', sublayers: [ { id: 'flood-zone-2', @@ -58,7 +64,9 @@ const datasets = [ showInKey: true, showInMenu: false, style: { - fill: { outdoor: '#1d70b8', dark: '#7fcdbb' }, + filter: ['==', 'flood_zone', 'FZ2'], + fill: { outdoor: '#ff0000', dark: '#7fcdbb' }, + // fill: { outdoor: '#1d70b8', dark: '#7fcdbb' }, stroke: 'none' }, }, @@ -69,7 +77,9 @@ const datasets = [ showInKey: true, showInMenu: false, style: { - fill: { outdoor: '#003078', dark: '#e5f5e0' }, + filter: ['==', 'flood_zone', 'FZ3'], + fill: { outdoor: '#00ff00', dark: '#e5f5e0' }, + // fill: { outdoor: '#003078', dark: '#e5f5e0' }, stroke: 'none' }, } diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index fa9d1b63c..d28cd5ad2 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -1,11 +1,13 @@ import VectorTileLayer from '@arcgis/core/layers/VectorTileLayer.js' import { datasetRegistry } from '../../registry/datasetRegistry.js' +import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle.js' export default class EsriLayerAdapter { constructor (mapProvider, symbolRegistry, patternRegistry) { this._mapProvider = mapProvider this._map = mapProvider.map // TODO: Implement symbolRegistry and patternRegistry usage in the adapter + this._mapLayers = {} } async init (mapStyle) { @@ -13,7 +15,7 @@ export default class EsriLayerAdapter { datasetRegistry.forEachDataset(registryDataset => this._addLayers(registryDataset, mapStyle)) } - _addLayers (registryDataset, mapStyle) { + async _addLayers (registryDataset, mapStyle) { console.log('Creating Esri VectorTileLayer for dataset', registryDataset.id) const vectorTileLayer = new VectorTileLayer({ id: registryDataset.id, @@ -21,6 +23,48 @@ export default class EsriLayerAdapter { opacity: 1, visible: registryDataset.visible }) + await vectorTileLayer.load() + await vectorTileLayer.when() + console.log('vectorTileLayer.style', vectorTileLayer.style) + // console.log('VectorTileLayer loaded', vectorTileLayer) + // console.log('VectorTileLayer currentStyleInfo', vectorTileLayer.currentStyleInfo) + const { styleRepository = {} } = vectorTileLayer + // const { layers: styleLayers = [] } = styleRepository + const styleLayers = JSON.parse(JSON.stringify(styleRepository.layers)) + console.log('VectorTileLayer styleLayers', registryDataset.id, JSON.parse(JSON.stringify(styleLayers))) + styleLayers.forEach((styleLayer) => { + console.log('Deleting VectorTileLayer styleLayer', styleLayer.id) + vectorTileLayer.deleteStyleLayer(styleLayer.id) + }) + + registryDataset.sublayers.forEach(sublayer => { + // const style = vectorTileLayer.getStyleLayer(sublayer.styleLayerId) + // console.log('Sublayer', sublayer.id, sublayer.styleLayerId, style) + // vectorTileLayer.deleteStyleLayer(sublayer.styleLayerId) + const newStyle = { + id: sublayer.id, + type: 'fill', + paint: { + 'fill-color': getValueForStyle(sublayer.style.fill, mapStyle.id) + }, + source: 'esri', + 'source-layer': sublayer.sourceLayer, + filter: sublayer.style.filter + } + vectorTileLayer.setStyleLayer(newStyle) + const addedStyle = vectorTileLayer.getStyleLayer(newStyle.id) + console.log('Added style layer', addedStyle) + }) + + console.log('VectorTileLayer styleLayers', JSON.parse(JSON.stringify(styleRepository.layers))) + + // const styleUrl = + // `${registryDataset.tiles}/resources/styles/root.json` + + // const style = await fetch(styleUrl).then(r => r.json()) + // console.log('style', style) + + this._mapLayers[registryDataset.id] = vectorTileLayer this._map.add(vectorTileLayer) } @@ -36,8 +80,18 @@ export default class EsriLayerAdapter { console.log('ESRI: applyStyle', args) } - async applyDatasetVisibility (...args) { - console.log('ESRI: applyDatasetVisibility', args) + async applyDatasetVisibility (datasetId) { + console.log('ESRI: applyDatasetVisibility', datasetId) + const registryDataset = datasetRegistry.getDataset(datasetId) + const { id, isSublayer } = registryDataset + if (isSublayer) { + const { parent, styleLayerId } = registryDataset + const parentLayer = this._mapLayers[parent.id] + parentLayer.setStyleLayerVisibility(styleLayerId, registryDataset.visibility) + return + } + this._mapLayers[id].visible = registryDataset.visible + // this._map.getLayer(id).visible = registryDataset.visible } async applyGlobalVisibility (...args) { diff --git a/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.js b/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.js index 0bd1e77db..a274fe376 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.js +++ b/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.js @@ -5,8 +5,6 @@ import { logger } from '../../../../../../../src/services/logger.js' const MAX_TILE_ZOOM = 22 export class MapLibreDataset extends Dataset { - get visibility () { return this.visible ? 'visible' : 'none' } - get fillLayerId () { if (this.hasSublayers) { return null diff --git a/plugins/beta/datasets/src/registry/dataset.js b/plugins/beta/datasets/src/registry/dataset.js index 2793ca96b..14ea0ab77 100644 --- a/plugins/beta/datasets/src/registry/dataset.js +++ b/plugins/beta/datasets/src/registry/dataset.js @@ -83,6 +83,9 @@ export class Dataset { return this._datasetDefinition.visible && getGlobalVisibility() } + get styleLayerId () { return this._datasetDefinition.styleLayerId } + get visibility () { return this.visible ? 'visible' : 'none' } + get symbolAnchor () { if (this.style?.symbolAnchor) { return this.style.symbolAnchor From 301ba8d90563afb717b4105654bdb4f55de59bc3 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Wed, 17 Jun 2026 10:07:05 +0100 Subject: [PATCH 07/53] IM-360 adding vtls with some styling --- .../src/adapters/esri/esriLayerAdapter.js | 78 +++++++++---------- .../datasets/src/components/Key/KeyItem.jsx | 3 + .../datasets/src/initialise/DatasetsInit.jsx | 4 +- .../src/initialise/initialiseDatasets.js | 2 +- plugins/beta/datasets/src/registry/dataset.js | 16 +++- .../datasets/src/registry/datasetRegistry.js | 7 +- 6 files changed, 63 insertions(+), 47 deletions(-) diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index d28cd5ad2..63ddd40ab 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -12,7 +12,10 @@ export default class EsriLayerAdapter { async init (mapStyle) { // TODO - move some of this into a super LayerAdapter class that this extends - datasetRegistry.forEachDataset(registryDataset => this._addLayers(registryDataset, mapStyle)) + console.log('adding layers') + const topLevelDatasets = datasetRegistry.topLevelDatasets() + await Promise.all(topLevelDatasets.map(registryDataset => this._addLayers(registryDataset, mapStyle))) + await Promise.all(topLevelDatasets.map(registryDataset => this.applyDatasetVisibility(registryDataset.id))) } async _addLayers (registryDataset, mapStyle) { @@ -21,51 +24,32 @@ export default class EsriLayerAdapter { id: registryDataset.id, url: registryDataset.tiles, opacity: 1, - visible: registryDataset.visible + visible: false }) await vectorTileLayer.load() - await vectorTileLayer.when() - console.log('vectorTileLayer.style', vectorTileLayer.style) - // console.log('VectorTileLayer loaded', vectorTileLayer) - // console.log('VectorTileLayer currentStyleInfo', vectorTileLayer.currentStyleInfo) - const { styleRepository = {} } = vectorTileLayer - // const { layers: styleLayers = [] } = styleRepository - const styleLayers = JSON.parse(JSON.stringify(styleRepository.layers)) - console.log('VectorTileLayer styleLayers', registryDataset.id, JSON.parse(JSON.stringify(styleLayers))) - styleLayers.forEach((styleLayer) => { - console.log('Deleting VectorTileLayer styleLayer', styleLayer.id) - vectorTileLayer.deleteStyleLayer(styleLayer.id) - }) registryDataset.sublayers.forEach(sublayer => { - // const style = vectorTileLayer.getStyleLayer(sublayer.styleLayerId) - // console.log('Sublayer', sublayer.id, sublayer.styleLayerId, style) - // vectorTileLayer.deleteStyleLayer(sublayer.styleLayerId) - const newStyle = { - id: sublayer.id, - type: 'fill', - paint: { - 'fill-color': getValueForStyle(sublayer.style.fill, mapStyle.id) - }, - source: 'esri', - 'source-layer': sublayer.sourceLayer, - filter: sublayer.style.filter + const { styleLayerId, style, hasStroke, hasFill } = sublayer + if (!styleLayerId) { + return + } + const layerPaintProperties = vectorTileLayer.getPaintProperties(styleLayerId) + if (layerPaintProperties) { + console.log('VectorTileLayer styleLayer paint properties', styleLayerId, layerPaintProperties) + if (hasStroke) { + layerPaintProperties['line-color'] = getValueForStyle(style.stroke, mapStyle.id) + } + if (hasFill) { + layerPaintProperties['fill-color'] = getValueForStyle(style.fill, mapStyle.id) + } + console.log('VectorTileLayer updated paint properties', styleLayerId, layerPaintProperties) + vectorTileLayer.setPaintProperties(styleLayerId, layerPaintProperties) } - vectorTileLayer.setStyleLayer(newStyle) - const addedStyle = vectorTileLayer.getStyleLayer(newStyle.id) - console.log('Added style layer', addedStyle) }) - - console.log('VectorTileLayer styleLayers', JSON.parse(JSON.stringify(styleRepository.layers))) - - // const styleUrl = - // `${registryDataset.tiles}/resources/styles/root.json` - - // const style = await fetch(styleUrl).then(r => r.json()) - // console.log('style', style) - this._mapLayers[registryDataset.id] = vectorTileLayer this._map.add(vectorTileLayer) + console.log('finished adding layers') + return vectorTileLayer.when() } async removeDataset (...args) { @@ -83,15 +67,25 @@ export default class EsriLayerAdapter { async applyDatasetVisibility (datasetId) { console.log('ESRI: applyDatasetVisibility', datasetId) const registryDataset = datasetRegistry.getDataset(datasetId) - const { id, isSublayer } = registryDataset + const { id, isSublayer, visible } = registryDataset if (isSublayer) { const { parent, styleLayerId } = registryDataset - const parentLayer = this._mapLayers[parent.id] - parentLayer.setStyleLayerVisibility(styleLayerId, registryDataset.visibility) + const vectorTileLayer = this._mapLayers[parent.id] + console.log('SUBLAYER setStyleLayerVisibility', vectorTileLayer.id, styleLayerId) + vectorTileLayer.setStyleLayerVisibility(styleLayerId, registryDataset.visibility) return + } else if (visible) { + const vectorTileLayer = this._mapLayers[datasetId] + registryDataset.sublayers.forEach(sublayer => { + const { styleLayerId } = sublayer + if (!styleLayerId) { + return + } + console.log('setStyleLayerVisibility', datasetId, styleLayerId) + vectorTileLayer.setStyleLayerVisibility(styleLayerId, sublayer.visibility) + }) } this._mapLayers[id].visible = registryDataset.visible - // this._map.getLayer(id).visible = registryDataset.visible } async applyGlobalVisibility (...args) { diff --git a/plugins/beta/datasets/src/components/Key/KeyItem.jsx b/plugins/beta/datasets/src/components/Key/KeyItem.jsx index f931a7dc0..b47a1ccc5 100644 --- a/plugins/beta/datasets/src/components/Key/KeyItem.jsx +++ b/plugins/beta/datasets/src/components/Key/KeyItem.jsx @@ -2,6 +2,9 @@ import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle.j import { KeySvg } from './KeySvg.jsx' export const KeyItem = ({ registryDataset, symbolRegistry, patternRegistry, mapStyle }) => { + if (!registryDataset.showInKey) { + return null + } return (
diff --git a/plugins/beta/datasets/src/initialise/DatasetsInit.jsx b/plugins/beta/datasets/src/initialise/DatasetsInit.jsx index 418ed7dea..0f45026a4 100755 --- a/plugins/beta/datasets/src/initialise/DatasetsInit.jsx +++ b/plugins/beta/datasets/src/initialise/DatasetsInit.jsx @@ -49,8 +49,8 @@ export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, m initDatasets() }, [isBaseMapReady, appState.mode]) - useEffect(() => datasetRegistry.attach(pluginState.mappedDatasets, pluginState.orderedDatasets), - [pluginState.mappedDatasets, pluginState.orderedDatasets]) + useEffect(() => datasetRegistry.attach(pluginState.mappedDatasets, pluginState.orderedDatasets, mapState.mapStyle), + [pluginState.mappedDatasets, pluginState.orderedDatasets, mapState.mapStyle]) useEffect(() => attachGlobalState(pluginState.globals), [pluginState.globals]) diff --git a/plugins/beta/datasets/src/initialise/initialiseDatasets.js b/plugins/beta/datasets/src/initialise/initialiseDatasets.js index 069c16837..7772866a5 100644 --- a/plugins/beta/datasets/src/initialise/initialiseDatasets.js +++ b/plugins/beta/datasets/src/initialise/initialiseDatasets.js @@ -27,7 +27,7 @@ export const initialiseDatasets = ({ if (adapter.createDataset) { datasetRegistry.attachCreateDataset(adapter.createDataset) } - datasetRegistry.attach(mappedDatasets) + datasetRegistry.attach(mappedDatasets, orderedDatasets, mapStyle) adapter.init(mapStyle).then(() => { datasetRegistry.forEachDataset(registryDataset => { if (!registryDataset.hasDynamicGeoJSON) { diff --git a/plugins/beta/datasets/src/registry/dataset.js b/plugins/beta/datasets/src/registry/dataset.js index 14ea0ab77..c3aede2b0 100644 --- a/plugins/beta/datasets/src/registry/dataset.js +++ b/plugins/beta/datasets/src/registry/dataset.js @@ -22,12 +22,20 @@ export class Dataset { get parentId () { return this._datasetDefinition.parentId } get minZoom () { return this._datasetDefinition.minZoom || this.parent?.minZoom } get maxZoom () { return this._datasetDefinition.maxZoom || this.parent?.maxZoom } + get showInKey () { const own = this._datasetDefinition.showInKey if (own !== undefined) { return own } return this.parent?.showInKey ?? false } + get visibleWhen () { + if (this._datasetDefinition.visibleWhen === undefined) { + return this.parent?.visibleWhen + } + return this._datasetDefinition.visibleWhen + } + get groupLabel () { return this._datasetDefinition.groupLabel } get opacity () { @@ -84,7 +92,13 @@ export class Dataset { } get styleLayerId () { return this._datasetDefinition.styleLayerId } - get visibility () { return this.visible ? 'visible' : 'none' } + get visibility () { + const visible = this.visible + if (visible && this.visibleWhen?.mapStyleId) { + return this.visibleWhen?.mapStyleId.includes(datasetRegistry.mapStyle.id) ? 'visible' : 'none' + } + return visible ? 'visible' : 'none' + } get symbolAnchor () { if (this.style?.symbolAnchor) { diff --git a/plugins/beta/datasets/src/registry/datasetRegistry.js b/plugins/beta/datasets/src/registry/datasetRegistry.js index a8fcbfd8b..5766efb54 100644 --- a/plugins/beta/datasets/src/registry/datasetRegistry.js +++ b/plugins/beta/datasets/src/registry/datasetRegistry.js @@ -2,9 +2,10 @@ import { createDataset } from './createDataset.js' import { DatasetDefinitionCache } from './datasetDefinitionCache.js' const datasetRegistry = { - attach (datasetsRef, orderedDatasetsRef) { + attach (datasetsRef, orderedDatasetsRef, mapStyle) { this._datasets = datasetsRef this._orderedDatasets = orderedDatasetsRef + this._mapStyle = mapStyle this._invalidateChangedDatasets() }, _definitionCache: new DatasetDefinitionCache(), @@ -23,6 +24,10 @@ const datasetRegistry = { attachCreateDataset (createDataset) { this._createDataset = createDataset }, _createDataset: (datasetDefinition) => createDataset(datasetDefinition), + get mapStyle () { + return this._mapStyle + }, + // getDataset retrieves a dataset by id, creating a new Dataset instance that wraps the definition getDataset (id) { const definition = this.datasets[id] From b52d56c920784c9373e747af65e5e940198c4c2d Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Wed, 17 Jun 2026 12:16:08 +0100 Subject: [PATCH 08/53] IM-360 refactored to use an esriDataset adapter --- demo/js/esri-datasets.js | 44 +++++++++++++------ .../src/adapters/esri/esriLayerAdapter.js | 38 ++++++++-------- .../src/adapters/esri/registry/esriDataset.js | 16 +++++++ 3 files changed, 65 insertions(+), 33 deletions(-) create mode 100644 plugins/beta/datasets/src/adapters/esri/registry/esriDataset.js diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 8280d061b..508296a1a 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -17,6 +17,7 @@ const datasets = [ showInKey: true, showInMenu: true, removeStyles: true, + visible: true, sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea CCP1', sublayers: [ { @@ -26,7 +27,6 @@ const datasets = [ showInKey: true, showInMenu: false, style: { - filter: ['==', '_symbol', 0], fill: { outdoor: '#F4A582', dark: '#BF3D4A' }, stroke: 'none' }, @@ -34,16 +34,36 @@ const datasets = [ { id: 'data-unavailable', label: 'Climate change data unavailable', - styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/1', + style: { // This is used just for the key - so that it renders the pattern correctly. + fillPattern: 'dot', + fillPatternForegroundColor: { outdoor: '#000000', dark: '#ffffff' }, + stroke: { outdoor: '#000000', dark: '#FFFFFF' }, + }, showInKey: true, showInMenu: false, + }, + { + id: 'data-unavailable-outline', style: { - filter: ['==', '_symbol', 1], - fillPattern: 'dot', - fillPatternForegroundColor: { outdoor: '#000000', dark: '#ffffff' }, - fill: { outdoor: '#0000ff', dark: '#BF3D4A' }, - stroke: { outdoor: '#000000', dark: '#ffffff' }, + stroke: { outdoor: '#000000', dark: '#FFFFFF' }, }, + styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/0', + showInKey: false, + showInMenu: false, + }, + { + id: 'data-unavailable-light', + visibleWhen: { mapStyleId: ['outdoor', 'black-and-white'] }, + styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/1', + showInKey: false, + showInMenu: false, + }, + { + id: 'data-unavailable-dark', + visibleWhen: { mapStyleId: ['dark'] }, + styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/2', + showInKey: false, + showInMenu: false, } ] }, @@ -64,9 +84,8 @@ const datasets = [ showInKey: true, showInMenu: false, style: { - filter: ['==', 'flood_zone', 'FZ2'], - fill: { outdoor: '#ff0000', dark: '#7fcdbb' }, - // fill: { outdoor: '#1d70b8', dark: '#7fcdbb' }, + // filter: ['==', 'flood_zone', 'FZ2'], + fill: { outdoor: '#1d70b8', dark: '#7fcdbb' }, stroke: 'none' }, }, @@ -77,9 +96,8 @@ const datasets = [ showInKey: true, showInMenu: false, style: { - filter: ['==', 'flood_zone', 'FZ3'], - fill: { outdoor: '#00ff00', dark: '#e5f5e0' }, - // fill: { outdoor: '#003078', dark: '#e5f5e0' }, + // filter: ['==', 'flood_zone', 'FZ3'], + fill: { outdoor: '#003078', dark: '#e5f5e0' }, stroke: 'none' }, } diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index 63ddd40ab..fe2ba1591 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -1,6 +1,6 @@ import VectorTileLayer from '@arcgis/core/layers/VectorTileLayer.js' import { datasetRegistry } from '../../registry/datasetRegistry.js' -import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle.js' +import { EsriDataset } from './registry/esriDataset.js' export default class EsriLayerAdapter { constructor (mapProvider, symbolRegistry, patternRegistry) { @@ -10,16 +10,23 @@ export default class EsriLayerAdapter { this._mapLayers = {} } + createDataset (datasetDefinition) { + return new EsriDataset(datasetDefinition) + } + async init (mapStyle) { // TODO - move some of this into a super LayerAdapter class that this extends - console.log('adding layers') const topLevelDatasets = datasetRegistry.topLevelDatasets() - await Promise.all(topLevelDatasets.map(registryDataset => this._addLayers(registryDataset, mapStyle))) + // ensure the datasets are added in order + for await (const registryDataset of topLevelDatasets) { + await this._addLayers(registryDataset, mapStyle) + } await Promise.all(topLevelDatasets.map(registryDataset => this.applyDatasetVisibility(registryDataset.id))) + console.log(this._map.layers.items.map(layer => layer.id)) } async _addLayers (registryDataset, mapStyle) { - console.log('Creating Esri VectorTileLayer for dataset', registryDataset.id) + console.log('Adding VectorTileLayer for dataset', registryDataset.id) const vectorTileLayer = new VectorTileLayer({ id: registryDataset.id, url: registryDataset.tiles, @@ -27,28 +34,20 @@ export default class EsriLayerAdapter { visible: false }) await vectorTileLayer.load() - + console.log('VectorTileLayer loaded for dataset', registryDataset.id) registryDataset.sublayers.forEach(sublayer => { - const { styleLayerId, style, hasStroke, hasFill } = sublayer + const { styleLayerId } = sublayer if (!styleLayerId) { return } const layerPaintProperties = vectorTileLayer.getPaintProperties(styleLayerId) if (layerPaintProperties) { - console.log('VectorTileLayer styleLayer paint properties', styleLayerId, layerPaintProperties) - if (hasStroke) { - layerPaintProperties['line-color'] = getValueForStyle(style.stroke, mapStyle.id) - } - if (hasFill) { - layerPaintProperties['fill-color'] = getValueForStyle(style.fill, mapStyle.id) - } - console.log('VectorTileLayer updated paint properties', styleLayerId, layerPaintProperties) - vectorTileLayer.setPaintProperties(styleLayerId, layerPaintProperties) + vectorTileLayer.setPaintProperties(styleLayerId, sublayer.applyLayerPaintProperties(layerPaintProperties)) } }) this._mapLayers[registryDataset.id] = vectorTileLayer this._map.add(vectorTileLayer) - console.log('finished adding layers') + console.log('Added VectorTileLayer for dataset', registryDataset.id) return vectorTileLayer.when() } @@ -65,13 +64,12 @@ export default class EsriLayerAdapter { } async applyDatasetVisibility (datasetId) { - console.log('ESRI: applyDatasetVisibility', datasetId) const registryDataset = datasetRegistry.getDataset(datasetId) const { id, isSublayer, visible } = registryDataset if (isSublayer) { const { parent, styleLayerId } = registryDataset const vectorTileLayer = this._mapLayers[parent.id] - console.log('SUBLAYER setStyleLayerVisibility', vectorTileLayer.id, styleLayerId) + // console.log('SUBLAYER setStyleLayerVisibility', vectorTileLayer.id, styleLayerId) vectorTileLayer.setStyleLayerVisibility(styleLayerId, registryDataset.visibility) return } else if (visible) { @@ -81,7 +79,7 @@ export default class EsriLayerAdapter { if (!styleLayerId) { return } - console.log('setStyleLayerVisibility', datasetId, styleLayerId) + // console.log('setStyleLayerVisibility', datasetId, styleLayerId) vectorTileLayer.setStyleLayerVisibility(styleLayerId, sublayer.visibility) }) } @@ -97,7 +95,7 @@ export default class EsriLayerAdapter { } async applyGlobalOpacity (...args) { - console.log('ESRI: applyGlobalOpacity', args) + // console.log('ESRI: applyGlobalOpacity', args) } async addDataset (...args) { diff --git a/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.js b/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.js new file mode 100644 index 000000000..304921d4a --- /dev/null +++ b/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.js @@ -0,0 +1,16 @@ +import { Dataset } from '../../../registry/dataset.js' +import { datasetRegistry } from '../../../registry/datasetRegistry.js' +import { getValueForStyle } from '../../../../../../../src/utils/getValueForStyle.js' + +export class EsriDataset extends Dataset { + applyLayerPaintProperties (layerPaintProperties) { + const { mapStyle } = datasetRegistry + if (this.hasStroke) { + layerPaintProperties['line-color'] = getValueForStyle(this.style.stroke, mapStyle.id) + } + if (this.hasFill) { + layerPaintProperties['fill-color'] = getValueForStyle(this.style.fill, mapStyle.id) + } + return layerPaintProperties + } +} From cf865cf4a349768847cdf82b5349068ed46ef36f Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Wed, 17 Jun 2026 14:54:47 +0100 Subject: [PATCH 09/53] IM-360 map style change working --- demo/js/esri-datasets.js | 8 +-- .../src/adapters/esri/esriLayerAdapter.js | 69 ++++++++++--------- .../datasets/src/registry/datasetRegistry.js | 10 ++- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 508296a1a..0893e7b92 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -12,17 +12,17 @@ const datasets = [ id: 'flood-zones-cc', label: 'Flood Zones Climate Change', groupLabel: 'Datasets', - groupId: 'flood-zones', + esriGroupId: 'flood-zones', tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_CCP1_NON_PRODUCTION/VectorTileServer`, showInKey: true, showInMenu: true, - removeStyles: true, visible: true, sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea CCP1', sublayers: [ { id: 'climate-change', label: 'Climate change (2070 to 2125)', + // TODO change styleLayerId to esriStyleLayerId styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Flood Zones plus climate change/1', showInKey: true, showInMenu: false, @@ -71,7 +71,7 @@ const datasets = [ id: 'flood-zones', label: 'Flood Zones', groupLabel: 'Datasets', - groupId: 'flood-zones', + esriGroupId: 'flood-zones', tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_NON_PRODUCTION/VectorTileServer`, showInKey: true, showInMenu: true, @@ -81,7 +81,6 @@ const datasets = [ id: 'flood-zone-2', label: 'Flood Zone 2', styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 2/1', - showInKey: true, showInMenu: false, style: { // filter: ['==', 'flood_zone', 'FZ2'], @@ -93,7 +92,6 @@ const datasets = [ id: 'flood-zone-3', label: 'Flood Zone 3', styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 3/1', - showInKey: true, showInMenu: false, style: { // filter: ['==', 'flood_zone', 'FZ3'], diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index fe2ba1591..65faac992 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -21,12 +21,13 @@ export default class EsriLayerAdapter { for await (const registryDataset of topLevelDatasets) { await this._addLayers(registryDataset, mapStyle) } + // Handles showing and hiding sublayers based on the mapStyle + // and updating the paint properties of the layers based on the dataset/mapStyle style + await this.onMapStyleChange(datasetRegistry.mapStyle, null) await Promise.all(topLevelDatasets.map(registryDataset => this.applyDatasetVisibility(registryDataset.id))) - console.log(this._map.layers.items.map(layer => layer.id)) } async _addLayers (registryDataset, mapStyle) { - console.log('Adding VectorTileLayer for dataset', registryDataset.id) const vectorTileLayer = new VectorTileLayer({ id: registryDataset.id, url: registryDataset.tiles, @@ -34,20 +35,8 @@ export default class EsriLayerAdapter { visible: false }) await vectorTileLayer.load() - console.log('VectorTileLayer loaded for dataset', registryDataset.id) - registryDataset.sublayers.forEach(sublayer => { - const { styleLayerId } = sublayer - if (!styleLayerId) { - return - } - const layerPaintProperties = vectorTileLayer.getPaintProperties(styleLayerId) - if (layerPaintProperties) { - vectorTileLayer.setPaintProperties(styleLayerId, sublayer.applyLayerPaintProperties(layerPaintProperties)) - } - }) this._mapLayers[registryDataset.id] = vectorTileLayer this._map.add(vectorTileLayer) - console.log('Added VectorTileLayer for dataset', registryDataset.id) return vectorTileLayer.when() } @@ -63,27 +52,25 @@ export default class EsriLayerAdapter { console.log('ESRI: applyStyle', args) } + _applyStyleLayerVisibility (sublayer, vectorTileLayer) { + const { styleLayerId } = sublayer + if (!styleLayerId) { + return + } + vectorTileLayer.setStyleLayerVisibility(styleLayerId, sublayer.visibility) + } + async applyDatasetVisibility (datasetId) { const registryDataset = datasetRegistry.getDataset(datasetId) - const { id, isSublayer, visible } = registryDataset + const { id, isSublayer, visible, parentId } = registryDataset + const vectorTileLayer = this._mapLayers[isSublayer ? parentId : id] if (isSublayer) { - const { parent, styleLayerId } = registryDataset - const vectorTileLayer = this._mapLayers[parent.id] - // console.log('SUBLAYER setStyleLayerVisibility', vectorTileLayer.id, styleLayerId) - vectorTileLayer.setStyleLayerVisibility(styleLayerId, registryDataset.visibility) + this._applyStyleLayerVisibility(registryDataset, vectorTileLayer) return } else if (visible) { - const vectorTileLayer = this._mapLayers[datasetId] - registryDataset.sublayers.forEach(sublayer => { - const { styleLayerId } = sublayer - if (!styleLayerId) { - return - } - // console.log('setStyleLayerVisibility', datasetId, styleLayerId) - vectorTileLayer.setStyleLayerVisibility(styleLayerId, sublayer.visibility) - }) + registryDataset.sublayers.forEach(sublayer => this._applyStyleLayerVisibility(sublayer, vectorTileLayer)) } - this._mapLayers[id].visible = registryDataset.visible + this._mapLayers[id].visible = visible } async applyGlobalVisibility (...args) { @@ -106,8 +93,28 @@ export default class EsriLayerAdapter { console.log('ESRI: applyFeatureFilter', args) } - async onMapStyleChange (...args) { - console.log('ESRI: onMapStyleChange', args) + async onMapStyleChange (newMapStyle, dynamicSources) { + if (datasetRegistry.mapStyle.id !== newMapStyle.id) { + // Ensure the datasetRegistry is aware of the new mapStyle so that the visibility and style properties of the datasets can be correctly applied + // TODO - this is a bit of a hack, we should probably have a better way to handle this + // such as having DatasetsInit listen for a mapStyle change event and then call datasetRegistry.attach with the new mapStyle + // and finally trigger this method + datasetRegistry.attach(datasetRegistry.datasets, datasetRegistry._orderedDatasets, newMapStyle) + } + datasetRegistry.forEach(registryDataset => { + const { id, isSublayer, styleLayerId, parent } = registryDataset + const vectorTileLayer = this._mapLayers[isSublayer ? parent.id : id] + if (vectorTileLayer && styleLayerId) { + // Show hide the style layer based on the dataset's mapStyle visibility + vectorTileLayer.setStyleLayerVisibility(styleLayerId, registryDataset.visibility) + // Update the paint properties of the style layer based on the dataset's mapStyle style + const layerPaintProperties = vectorTileLayer.getPaintProperties(styleLayerId) + if (layerPaintProperties) { + vectorTileLayer.setPaintProperties(styleLayerId, registryDataset.applyLayerPaintProperties(layerPaintProperties)) + } + } + }) + // TODO - handle dynamic sources } async onMapSizeChange (...args) { diff --git a/plugins/beta/datasets/src/registry/datasetRegistry.js b/plugins/beta/datasets/src/registry/datasetRegistry.js index 5766efb54..f7939a585 100644 --- a/plugins/beta/datasets/src/registry/datasetRegistry.js +++ b/plugins/beta/datasets/src/registry/datasetRegistry.js @@ -5,20 +5,26 @@ const datasetRegistry = { attach (datasetsRef, orderedDatasetsRef, mapStyle) { this._datasets = datasetsRef this._orderedDatasets = orderedDatasetsRef - this._mapStyle = mapStyle + if (mapStyle) { + this._mapStyle = mapStyle + } this._invalidateChangedDatasets() }, + _definitionCache: new DatasetDefinitionCache(), + _invalidateCache () { // used in tests to clear the cache between runs this._definitionCache = new DatasetDefinitionCache() }, - _invalidateChangedDatasets () { // used in tests to clear the cache between runs + + _invalidateChangedDatasets () { if (this._datasets) { this._definitionCache.invalidateChangedDatasets(Object.values(this._datasets)) } else { this._invalidateCache() } }, + // createDataset defaults to a generic dataset factory function, but can be overridden by calling // attachCreateDataset, which allows the layer adapter to provide its own createDataset function, attachCreateDataset (createDataset) { this._createDataset = createDataset }, From 391b528ebde01b8d812a8417f7ab4e462a506173 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Wed, 17 Jun 2026 16:57:22 +0100 Subject: [PATCH 10/53] IM-360 replaced styleLayerId with esriStyleLayerId --- demo/js/esri-datasets.js | 19 +++++++++---------- .../src/adapters/esri/esriLayerAdapter.js | 16 ++++++++-------- .../datasets/src/reducers/datasetsToMenu.js | 18 +++++++++++++++--- plugins/beta/datasets/src/registry/dataset.js | 2 +- 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 0893e7b92..2c48e2066 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -22,8 +22,7 @@ const datasets = [ { id: 'climate-change', label: 'Climate change (2070 to 2125)', - // TODO change styleLayerId to esriStyleLayerId - styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Flood Zones plus climate change/1', + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Flood Zones plus climate change/1', showInKey: true, showInMenu: false, style: { @@ -47,21 +46,21 @@ const datasets = [ style: { stroke: { outdoor: '#000000', dark: '#FFFFFF' }, }, - styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/0', + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/0', showInKey: false, showInMenu: false, }, { id: 'data-unavailable-light', visibleWhen: { mapStyleId: ['outdoor', 'black-and-white'] }, - styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/1', + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/1', showInKey: false, showInMenu: false, }, { id: 'data-unavailable-dark', visibleWhen: { mapStyleId: ['dark'] }, - styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/2', + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/2', showInKey: false, showInMenu: false, } @@ -74,14 +73,14 @@ const datasets = [ esriGroupId: 'flood-zones', tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_NON_PRODUCTION/VectorTileServer`, showInKey: true, - showInMenu: true, + // showInMenu: true, sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea', sublayers: [ { id: 'flood-zone-2', label: 'Flood Zone 2', - styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 2/1', - showInMenu: false, + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 2/1', + showInMenu: true, style: { // filter: ['==', 'flood_zone', 'FZ2'], fill: { outdoor: '#1d70b8', dark: '#7fcdbb' }, @@ -91,8 +90,8 @@ const datasets = [ { id: 'flood-zone-3', label: 'Flood Zone 3', - styleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 3/1', - showInMenu: false, + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 3/1', + showInMenu: true, style: { // filter: ['==', 'flood_zone', 'FZ3'], fill: { outdoor: '#003078', dark: '#e5f5e0' }, diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index 65faac992..e1d64eb52 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -53,11 +53,11 @@ export default class EsriLayerAdapter { } _applyStyleLayerVisibility (sublayer, vectorTileLayer) { - const { styleLayerId } = sublayer - if (!styleLayerId) { + const { esriStyleLayerId } = sublayer + if (!esriStyleLayerId) { return } - vectorTileLayer.setStyleLayerVisibility(styleLayerId, sublayer.visibility) + vectorTileLayer.setStyleLayerVisibility(esriStyleLayerId, sublayer.visibility) } async applyDatasetVisibility (datasetId) { @@ -102,15 +102,15 @@ export default class EsriLayerAdapter { datasetRegistry.attach(datasetRegistry.datasets, datasetRegistry._orderedDatasets, newMapStyle) } datasetRegistry.forEach(registryDataset => { - const { id, isSublayer, styleLayerId, parent } = registryDataset + const { id, isSublayer, esriStyleLayerId, parent } = registryDataset const vectorTileLayer = this._mapLayers[isSublayer ? parent.id : id] - if (vectorTileLayer && styleLayerId) { + if (vectorTileLayer && esriStyleLayerId) { // Show hide the style layer based on the dataset's mapStyle visibility - vectorTileLayer.setStyleLayerVisibility(styleLayerId, registryDataset.visibility) + vectorTileLayer.setStyleLayerVisibility(esriStyleLayerId, registryDataset.visibility) // Update the paint properties of the style layer based on the dataset's mapStyle style - const layerPaintProperties = vectorTileLayer.getPaintProperties(styleLayerId) + const layerPaintProperties = vectorTileLayer.getPaintProperties(esriStyleLayerId) if (layerPaintProperties) { - vectorTileLayer.setPaintProperties(styleLayerId, registryDataset.applyLayerPaintProperties(layerPaintProperties)) + vectorTileLayer.setPaintProperties(esriStyleLayerId, registryDataset.applyLayerPaintProperties(layerPaintProperties)) } } }) diff --git a/plugins/beta/datasets/src/reducers/datasetsToMenu.js b/plugins/beta/datasets/src/reducers/datasetsToMenu.js index fc6521e2a..523079291 100644 --- a/plugins/beta/datasets/src/reducers/datasetsToMenu.js +++ b/plugins/beta/datasets/src/reducers/datasetsToMenu.js @@ -10,8 +10,10 @@ export const datasetsToMenu = (state) => { const visibleSublayers = dataset.sublayers.filter(sublayer => dataset.showInMenu ? sublayer.showInMenu !== false : sublayer.showInMenu ) - if (!visibleSublayers.length) { return } - menu.push({ + if (dataset.showInMenu === false && !visibleSublayers.length) { + return + } + const groupObject = { id: dataset.id, groupLabel: dataset.label, visibleWhen: true, @@ -20,7 +22,17 @@ export const datasetsToMenu = (state) => { id: `${dataset.id}-${sublayer.id}`, label: sublayer.label })) - }) + } + // Temporary change until we handle multiple datasets with sublayers, that have a groupLabel + // So that the esri datasets with sublayers are displayed in the menu, even though they have a groupLabel + if (groupObject.items.length === 0) { + delete groupObject.groupLabel + groupObject.items.push({ + id: dataset.id, + label: dataset.label + }) + } + menu.push(groupObject) } else if (dataset.groupLabel) { // Check for existing group object for this groupLabel, or create it if it doesn't exist, // then add the dataset to its items and push it to the menu if it's new diff --git a/plugins/beta/datasets/src/registry/dataset.js b/plugins/beta/datasets/src/registry/dataset.js index c3aede2b0..c20c0c81d 100644 --- a/plugins/beta/datasets/src/registry/dataset.js +++ b/plugins/beta/datasets/src/registry/dataset.js @@ -91,7 +91,7 @@ export class Dataset { return this._datasetDefinition.visible && getGlobalVisibility() } - get styleLayerId () { return this._datasetDefinition.styleLayerId } + get esriStyleLayerId () { return this._datasetDefinition.esriStyleLayerId } get visibility () { const visible = this.visible if (visible && this.visibleWhen?.mapStyleId) { From 04dcc5feee1c0235a2b902d27f1b79c9b0a939a0 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Wed, 17 Jun 2026 17:10:35 +0100 Subject: [PATCH 11/53] IM-360 implemented global visibility --- demo/js/esri-datasets.js | 11 +++++++++-- .../datasets/src/adapters/esri/esriLayerAdapter.js | 12 +++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 2c48e2066..b144d7b9e 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -111,8 +111,6 @@ const datasetsPlugin = createDatasetsPlugin({ datasets }) - - const interactiveMap = new InteractiveMap('map', { behaviour: 'mapOnly', mapProvider: esriProvider({ setupConfig: setupEsriConfig }), @@ -139,4 +137,13 @@ const interactiveMap = new InteractiveMap('map', { } }) ] +}) + +const testGlobalVisibility = () => { + setTimeout(() => datasetsPlugin.setDatasetVisibility(false), 3000) + setTimeout(() => datasetsPlugin.setDatasetVisibility(true), 6000) +} + +interactiveMap.on('datasets:ready', function () { + testGlobalVisibility() }) \ No newline at end of file diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index e1d64eb52..ee3bb32c7 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -60,8 +60,7 @@ export default class EsriLayerAdapter { vectorTileLayer.setStyleLayerVisibility(esriStyleLayerId, sublayer.visibility) } - async applyDatasetVisibility (datasetId) { - const registryDataset = datasetRegistry.getDataset(datasetId) + _applyRegistryDatasetVisibility (registryDataset) { const { id, isSublayer, visible, parentId } = registryDataset const vectorTileLayer = this._mapLayers[isSublayer ? parentId : id] if (isSublayer) { @@ -73,8 +72,15 @@ export default class EsriLayerAdapter { this._mapLayers[id].visible = visible } + async applyDatasetVisibility (datasetId) { + const registryDataset = datasetRegistry.getDataset(datasetId) + if (registryDataset) { + this._applyRegistryDatasetVisibility(registryDataset) + } + } + async applyGlobalVisibility (...args) { - console.log('ESRI: applyGlobalVisibility', args) + datasetRegistry.forEachDataset(registryDataset => this._applyRegistryDatasetVisibility(registryDataset)) } async applyDatasetOpacity (...args) { From 51405c1556ae87687c725c8486542763f1d65745 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Thu, 18 Jun 2026 16:38:12 +0100 Subject: [PATCH 12/53] IM-360 opacity working for esriDatasets --- demo/js/esri-datasets.js | 4 +- .../src/adapters/esri/esriLayerAdapter.js | 110 ++++++++++++++---- .../src/adapters/esri/registry/esriDataset.js | 11 ++ 3 files changed, 102 insertions(+), 23 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index b144d7b9e..a8200c8aa 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -54,6 +54,7 @@ const datasets = [ id: 'data-unavailable-light', visibleWhen: { mapStyleId: ['outdoor', 'black-and-white'] }, esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/1', + esriUseServerStyle: true, showInKey: false, showInMenu: false, }, @@ -61,6 +62,7 @@ const datasets = [ id: 'data-unavailable-dark', visibleWhen: { mapStyleId: ['dark'] }, esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/2', + esriUseServerStyle: true, showInKey: false, showInMenu: false, } @@ -82,7 +84,6 @@ const datasets = [ esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 2/1', showInMenu: true, style: { - // filter: ['==', 'flood_zone', 'FZ2'], fill: { outdoor: '#1d70b8', dark: '#7fcdbb' }, stroke: 'none' }, @@ -93,7 +94,6 @@ const datasets = [ esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 3/1', showInMenu: true, style: { - // filter: ['==', 'flood_zone', 'FZ3'], fill: { outdoor: '#003078', dark: '#e5f5e0' }, stroke: 'none' }, diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index ee3bb32c7..4a9c5942e 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -1,4 +1,5 @@ import VectorTileLayer from '@arcgis/core/layers/VectorTileLayer.js' +import GroupLayer from '@arcgis/core/layers/GroupLayer.js' import { datasetRegistry } from '../../registry/datasetRegistry.js' import { EsriDataset } from './registry/esriDataset.js' @@ -7,7 +8,19 @@ export default class EsriLayerAdapter { this._mapProvider = mapProvider this._map = mapProvider.map // TODO: Implement symbolRegistry and patternRegistry usage in the adapter - this._mapLayers = {} + + // _vectorTileLayers is a map of datasetId to VectorTileLayer instances + // it includes stand alone vectorTileLayers and vectorTileLayers that are part of a groupLayer + // but does not include group layers themselves, which are tracked in _groupLayers + this._vectorTileLayers = {} + + // _vectorTileOpacityLayers is a map of datasetId to VectorTileLayer/GroupLayer where opacity is applied + // it includes stand alone vectorTileLayers and groupLayers that contain vectorTileLayers + // but does not include vectorTileLayers that are part of a groupLayer + this._vectorTileOpacityLayers = {} + + // _groupLayers is a map of esriGroupId to GroupLayer + this._groupLayers = {} } createDataset (datasetDefinition) { @@ -15,41 +28,78 @@ export default class EsriLayerAdapter { } async init (mapStyle) { - // TODO - move some of this into a super LayerAdapter class that this extends const topLevelDatasets = datasetRegistry.topLevelDatasets() // ensure the datasets are added in order for await (const registryDataset of topLevelDatasets) { await this._addLayers(registryDataset, mapStyle) } - // Handles showing and hiding sublayers based on the mapStyle + + // onMapStyleChange: handles showing and hiding sublayers based on the mapStyle // and updating the paint properties of the layers based on the dataset/mapStyle style await this.onMapStyleChange(datasetRegistry.mapStyle, null) + + // Apply opacity to all layers + await this.applyGlobalOpacity() + + // Finally show all layers that are visible based on the dataset/mapStyle visibility await Promise.all(topLevelDatasets.map(registryDataset => this.applyDatasetVisibility(registryDataset.id))) + + // Log the layers for debugging + // this.logLayers('_vectorTileLayers') + // console.log('this._vectorTileOpacityLayers', this._vectorTileOpacityLayers) + // console.log('this._groupLayers', this._groupLayers) + } + + logLayers (name) { + const obj = this[name] + console.log(`${name}:`) + Object.entries(obj).forEach(([datasetId, layer]) => { + console.log(` ${datasetId}`) + layer.styleRepository.layers.forEach(styleLayer => { + console.log(` ${styleLayer.id}`) + }) + }) + } + + _addGroupLayer (esriGroupId) { + // Either adds a new group layer to the map, or returns an existing one if it already exists + if (!this._groupLayers[esriGroupId]) { + const groupLayer = new GroupLayer({ + id: esriGroupId, + opacity: 1, + visible: true + }) + this._groupLayers[esriGroupId] = groupLayer + this._map.add(groupLayer) + } + return this._groupLayers[esriGroupId] } async _addLayers (registryDataset, mapStyle) { + const { esriGroupId } = registryDataset + const vectorTileParent = esriGroupId ? this._addGroupLayer(esriGroupId) : this._map const vectorTileLayer = new VectorTileLayer({ id: registryDataset.id, url: registryDataset.tiles, opacity: 1, visible: false }) - await vectorTileLayer.load() - this._mapLayers[registryDataset.id] = vectorTileLayer - this._map.add(vectorTileLayer) + this._vectorTileLayers[registryDataset.id] = vectorTileLayer + this._vectorTileOpacityLayers[registryDataset.id] = esriGroupId ? vectorTileParent : vectorTileLayer + vectorTileParent.add(vectorTileLayer) return vectorTileLayer.when() } async removeDataset (...args) { - console.log('ESRI: removeDataset', args) + console.log('TODO: removeDataset', args) } async setData (...args) { - console.log('ESRI: setData', args) + console.log('TODO: setData', args) } async applyStyle (...args) { - console.log('ESRI: applyStyle', args) + console.log('TODO: applyStyle', args) } _applyStyleLayerVisibility (sublayer, vectorTileLayer) { @@ -61,15 +111,20 @@ export default class EsriLayerAdapter { } _applyRegistryDatasetVisibility (registryDataset) { + // if this is a sublayer, we need to apply the visibility to the vectorTileLayers style sheet + // if this is a top level dataset, we need to apply the visibility to the vectorTileLayer/ groupLayer itself const { id, isSublayer, visible, parentId } = registryDataset - const vectorTileLayer = this._mapLayers[isSublayer ? parentId : id] + const vectorTileLayer = this._vectorTileLayers[isSublayer ? parentId : id] + if (isSublayer) { this._applyStyleLayerVisibility(registryDataset, vectorTileLayer) + // Don't apply the visibility change to the parent, since the parent may have other sublayers that are visible return } else if (visible) { + // No need to apply style layer visibility for datasets that are hidden registryDataset.sublayers.forEach(sublayer => this._applyStyleLayerVisibility(sublayer, vectorTileLayer)) } - this._mapLayers[id].visible = visible + vectorTileLayer.visible = visible } async applyDatasetVisibility (datasetId) { @@ -79,24 +134,33 @@ export default class EsriLayerAdapter { } } - async applyGlobalVisibility (...args) { + async applyGlobalVisibility () { datasetRegistry.forEachDataset(registryDataset => this._applyRegistryDatasetVisibility(registryDataset)) } - async applyDatasetOpacity (...args) { - console.log('ESRI: applyDatasetOpacity', args) + async applyDatasetOpacity (datasetId) { + const vectorTileLayer = this._vectorTileOpacityLayers[datasetId] + const registryDataset = datasetRegistry.getDataset(datasetId) + if (vectorTileLayer && registryDataset) { + vectorTileLayer.opacity = registryDataset.opacity + } } async applyGlobalOpacity (...args) { - // console.log('ESRI: applyGlobalOpacity', args) + Object.entries(this._vectorTileOpacityLayers).forEach(([datasetId, vectorTileLayer]) => { + const registryDataset = datasetRegistry.getDataset(datasetId) + if (registryDataset) { + vectorTileLayer.opacity = registryDataset.opacity + } + }) } async addDataset (...args) { - console.log('ESRI: addDataset', args) + console.log('TODO: addDataset', args) } async applyFeatureFilter (...args) { - console.log('ESRI: applyFeatureFilter', args) + console.log('TODO: applyFeatureFilter', args) } async onMapStyleChange (newMapStyle, dynamicSources) { @@ -109,13 +173,18 @@ export default class EsriLayerAdapter { } datasetRegistry.forEach(registryDataset => { const { id, isSublayer, esriStyleLayerId, parent } = registryDataset - const vectorTileLayer = this._mapLayers[isSublayer ? parent.id : id] + const vectorTileLayer = this._vectorTileLayers[isSublayer ? parent.id : id] if (vectorTileLayer && esriStyleLayerId) { // Show hide the style layer based on the dataset's mapStyle visibility vectorTileLayer.setStyleLayerVisibility(esriStyleLayerId, registryDataset.visibility) + if (registryDataset.useServerStyle) { + // If the dataset is using the server style, we don't need to apply any paint properties + return + } // Update the paint properties of the style layer based on the dataset's mapStyle style const layerPaintProperties = vectorTileLayer.getPaintProperties(esriStyleLayerId) if (layerPaintProperties) { + registryDataset.applyLayerPaintProperties(layerPaintProperties) vectorTileLayer.setPaintProperties(esriStyleLayerId, registryDataset.applyLayerPaintProperties(layerPaintProperties)) } } @@ -123,7 +192,6 @@ export default class EsriLayerAdapter { // TODO - handle dynamic sources } - async onMapSizeChange (...args) { - console.log('ESRI: onMapSizeChange', args) - } + // onMapSizeChange is not applicable to the esriLayerAdapter + async onMapSizeChange (_mapStyle) {} } diff --git a/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.js b/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.js index 304921d4a..e34667166 100644 --- a/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.js +++ b/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.js @@ -13,4 +13,15 @@ export class EsriDataset extends Dataset { } return layerPaintProperties } + + get esriGroupId () { + if (this._datasetDefinition.esriGroupId === undefined) { + return this.parent?.esriGroupId + } + return this._datasetDefinition.esriGroupId + } + + get useServerStyle () { + return Boolean(this._datasetDefinition.esriUseServerStyle) + } } From 508b1e8a64b69cc7f04b6a3af6049f59cf655dd9 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Thu, 18 Jun 2026 17:01:49 +0100 Subject: [PATCH 13/53] IM-360 added comment to visibleWhen, removed unused ...args --- plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js | 2 +- plugins/beta/datasets/src/registry/dataset.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index 4a9c5942e..05e36b01f 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -146,7 +146,7 @@ export default class EsriLayerAdapter { } } - async applyGlobalOpacity (...args) { + async applyGlobalOpacity () { Object.entries(this._vectorTileOpacityLayers).forEach(([datasetId, vectorTileLayer]) => { const registryDataset = datasetRegistry.getDataset(datasetId) if (registryDataset) { diff --git a/plugins/beta/datasets/src/registry/dataset.js b/plugins/beta/datasets/src/registry/dataset.js index c20c0c81d..dce6f78fe 100644 --- a/plugins/beta/datasets/src/registry/dataset.js +++ b/plugins/beta/datasets/src/registry/dataset.js @@ -29,6 +29,9 @@ export class Dataset { return this.parent?.showInKey ?? false } + // Note visibleWhen is used in combination with visible. + // both must be true for the dataset to be visible. + // visibleWhen is used to show/hide datasets based on mapStyle. get visibleWhen () { if (this._datasetDefinition.visibleWhen === undefined) { return this.parent?.visibleWhen From 904532505d2432f936a29a4f43ccdc56081fe5c2 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 19 Jun 2026 08:50:07 +0100 Subject: [PATCH 14/53] IM-360 corrected unit test after menu change --- plugins/beta/datasets/src/reducers/datasetsToMenu.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/beta/datasets/src/reducers/datasetsToMenu.test.js b/plugins/beta/datasets/src/reducers/datasetsToMenu.test.js index 3da636b11..3c3a68a58 100644 --- a/plugins/beta/datasets/src/reducers/datasetsToMenu.test.js +++ b/plugins/beta/datasets/src/reducers/datasetsToMenu.test.js @@ -76,9 +76,9 @@ describe('datasetsToMenu', () => { expect(result[0].items).toEqual([{ id: 'test-a', label: 'A' }]) }) - it('does not add a menu entry when dataset has showInMenu: true but all sublayers opt out', () => { + it('Should have a single menu entry for the parent dataset when showInMenu: true but all sublayers opt out', () => { const result = datasetsToMenu({ datasets: [sublayerDataset(true, false)] }) - expect(result).toHaveLength(0) + expect(result).toEqual([{ id: 'test', items: [{ id: 'test', label: 'Test' }], type: 'checkbox', visibleWhen: true }]) }) }) }) From 114b5410ceca23689c134490329fb77d6abae799 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 19 Jun 2026 09:19:12 +0100 Subject: [PATCH 15/53] IM-373 added a temp assertion of mapStyle --- .../src/adapters/maplibre/maplibreLayerAdapter.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js index a3641f2bd..cfb9c9434 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js @@ -38,12 +38,19 @@ export default class MaplibreLayerAdapter { // ─── Lifecycle ────────────────────────────────────────────────────────────── + assertMapStyle (methodName, mapStyle) { + if (mapStyle !== datasetRegistry.mapStyle) { + console.error(`MaplibreLayerAdapter.${methodName} has mapStyle ${mapStyle.id}, but datasetRegistry has ${datasetRegistry.mapStyle.id}.`) + } + } + /** * Initialise all datasets: register patterns, add layers, then wait for idle. * @param {Object} mapStyle * @returns {Promise} Resolves once the map has processed all layers. */ async init (mapStyle) { + this.assertMapStyle('init', mapStyle) const { patternConfigs, symbolConfigs } = datasetRegistry.getPatternAndSymbolConfigs() await this.addPatternsAndSymbolsToMap(patternConfigs, symbolConfigs, mapStyle) @@ -60,6 +67,7 @@ export default class MaplibreLayerAdapter { } async addPatternsAndSymbolsToMap (patterns, symbols, mapStyle) { + this.assertMapStyle('addPatternsAndSymbolsToMap', mapStyle) const mapStyleId = mapStyle.id return Promise.all([ this._mapProvider.addPatternsToMap(patterns, mapStyleId, this._patternRegistry), @@ -91,6 +99,7 @@ export default class MaplibreLayerAdapter { * @returns {Promise} */ async onMapStyleChange (newMapStyle, dynamicSources) { + this.assertMapStyle('onMapStyleChange', newMapStyle) // MapLibre wipes all sources/layers on style change — must wait for idle first await new Promise(resolve => this._map.once('idle', resolve)) @@ -115,6 +124,7 @@ export default class MaplibreLayerAdapter { * @returns {Promise} */ async onMapSizeChange (mapStyle) { + this.assertMapStyle('onMapSizeChange', mapStyle) const { patternConfigs, symbolConfigs } = datasetRegistry.getPatternAndSymbolConfigs() await this.addPatternsAndSymbolsToMap(patternConfigs, symbolConfigs, mapStyle) @@ -142,6 +152,7 @@ export default class MaplibreLayerAdapter { * @param {Object} mapStyle */ async addDataset (datasetId, mapStyle) { + this.assertMapStyle('addDataset', mapStyle) const registryDataset = datasetRegistry.getDataset(datasetId) await this.addPatternsAndSymbolsToMap(registryDataset.patternConfigs, registryDataset.symbolConfigs, mapStyle) this._addLayers(registryDataset, mapStyle) @@ -196,6 +207,7 @@ export default class MaplibreLayerAdapter { * @returns {Promise} */ async applyStyle (datasetId, mapStyle) { + this.assertMapStyle('applyStyle', mapStyle) const registryDataset = datasetRegistry.getDataset(datasetId) registryDataset.layerIds.forEach(layerId => this.removeLayer(layerId)) await this.addPatternsAndSymbolsToMap(registryDataset.patternConfigs, registryDataset.symbolConfigs, mapStyle) @@ -254,6 +266,7 @@ export default class MaplibreLayerAdapter { } _addLayers (registryDataset, mapStyle) { + this.assertMapStyle('_addLayers', mapStyle) const sourceId = addDatasetLayers(this._map, registryDataset, mapStyle, this._symbolRegistry, this._patternRegistry, this._pixelRatio) this._datasetSourceMap.set(registryDataset.id, sourceId) this._maintainSymbolOrdering(registryDataset) From fb002ea5236cd5b50c8c7c69553c8562a47e8a86 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 19 Jun 2026 14:03:55 +0100 Subject: [PATCH 16/53] IM-373 WIP towards moving the mapStyle dependency out of the layerAdapter --- .../beta/datasets/src/adapters/layerAdapter.js | 9 +++++++++ .../adapters/maplibre/maplibreLayerAdapter.js | 6 +++++- .../datasets/src/initialise/DatasetsInit.jsx | 12 ++++++++++-- .../src/initialise/initialiseDatasets.js | 17 +++++++++-------- .../datasets/src/registry/datasetRegistry.js | 10 +++++++--- 5 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 plugins/beta/datasets/src/adapters/layerAdapter.js diff --git a/plugins/beta/datasets/src/adapters/layerAdapter.js b/plugins/beta/datasets/src/adapters/layerAdapter.js new file mode 100644 index 000000000..2188f3a12 --- /dev/null +++ b/plugins/beta/datasets/src/adapters/layerAdapter.js @@ -0,0 +1,9 @@ +export class LayerAdapter { + attachDynamicSources (dynamicSources) { + this._dynamicSources = dynamicSources + } + + get dynamicSources () { + return this._dynamicSources + } +} diff --git a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js index cfb9c9434..2c4f82c82 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js @@ -1,3 +1,4 @@ +import { LayerAdapter } from '../layerAdapter.js' import { addDatasetLayers } from './layerBuilders.js' import { MapLibreDataset } from './registry/mapLibreDataset.js' import { datasetRegistry } from '../../registry/datasetRegistry.js' @@ -14,13 +15,14 @@ import { datasetRegistry } from '../../registry/datasetRegistry.js' * Symbol image rasterisation is delegated to the map provider via * `mapProvider.addSymbolsToMap()`, keeping this adapter free of provider internals. */ -export default class MaplibreLayerAdapter { +export default class MaplibreLayerAdapter extends LayerAdapter { /** * @param {Object} mapProvider - Map provider instance (e.g. MapLibreProvider) * @param {Object} symbolRegistry * @param {Object} patternRegistry */ constructor (mapProvider, symbolRegistry, patternRegistry) { + super() this._mapProvider = mapProvider this._map = mapProvider.map this._symbolRegistry = symbolRegistry @@ -41,6 +43,8 @@ export default class MaplibreLayerAdapter { assertMapStyle (methodName, mapStyle) { if (mapStyle !== datasetRegistry.mapStyle) { console.error(`MaplibreLayerAdapter.${methodName} has mapStyle ${mapStyle.id}, but datasetRegistry has ${datasetRegistry.mapStyle.id}.`) + } else { + console.log(`MaplibreLayerAdapter.${methodName} has mapStyle ${mapStyle.id}, which matches datasetRegistry.`) } } diff --git a/plugins/beta/datasets/src/initialise/DatasetsInit.jsx b/plugins/beta/datasets/src/initialise/DatasetsInit.jsx index 0f45026a4..7f4fb53b6 100755 --- a/plugins/beta/datasets/src/initialise/DatasetsInit.jsx +++ b/plugins/beta/datasets/src/initialise/DatasetsInit.jsx @@ -49,8 +49,16 @@ export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, m initDatasets() }, [isBaseMapReady, appState.mode]) - useEffect(() => datasetRegistry.attach(pluginState.mappedDatasets, pluginState.orderedDatasets, mapState.mapStyle), - [pluginState.mappedDatasets, pluginState.orderedDatasets, mapState.mapStyle]) + useEffect(() => datasetRegistry.attach(pluginState.mappedDatasets, pluginState.orderedDatasets), + [pluginState.mappedDatasets, pluginState.orderedDatasets]) + + useEffect(() => { + datasetRegistry.attachMapStyle(mapState.mapStyle) + if (layerAdapter?.onMapStyleChange) { + layerAdapter.onMapStyleChange(mapState.mapStyle) + } + }, + [mapState.mapStyle]) useEffect(() => attachGlobalState(pluginState.globals), [pluginState.globals]) diff --git a/plugins/beta/datasets/src/initialise/initialiseDatasets.js b/plugins/beta/datasets/src/initialise/initialiseDatasets.js index 7772866a5..186483807 100644 --- a/plugins/beta/datasets/src/initialise/initialiseDatasets.js +++ b/plugins/beta/datasets/src/initialise/initialiseDatasets.js @@ -46,24 +46,25 @@ export const initialiseDatasets = ({ eventBus.emit('datasets:ready') }) - let currentMapStyle = mapStyle + // let currentMapStyle = mapStyle // Handle basemap style changes — delegate entirely to the adapter - const onSetMapStyle = (newMapStyle) => { - currentMapStyle = newMapStyle - adapter.onMapStyleChange(newMapStyle, dynamicSources) - } + // const onSetMapStyle = (newMapStyle) => { + // currentMapStyle = newMapStyle + // adapter.onMapStyleChange(newMapStyle, dynamicSources) + // } const onMapSizeChange = () => { - adapter.onMapSizeChange(currentMapStyle) + // adapter.onMapSizeChange(currentMapStyle) + adapter.onMapSizeChange() } - eventBus.on(events.MAP_SET_STYLE, onSetMapStyle) + // eventBus.on(events.MAP_SET_STYLE, onSetMapStyle) eventBus.on(events.MAP_SIZE_CHANGE, onMapSizeChange) return { remove () { - eventBus.off(events.MAP_SET_STYLE, onSetMapStyle) + // eventBus.off(events.MAP_SET_STYLE, onSetMapStyle) eventBus.off(events.MAP_SIZE_CHANGE, onMapSizeChange) // Clean up dynamic sources diff --git a/plugins/beta/datasets/src/registry/datasetRegistry.js b/plugins/beta/datasets/src/registry/datasetRegistry.js index f7939a585..aebdc2d4f 100644 --- a/plugins/beta/datasets/src/registry/datasetRegistry.js +++ b/plugins/beta/datasets/src/registry/datasetRegistry.js @@ -5,9 +5,7 @@ const datasetRegistry = { attach (datasetsRef, orderedDatasetsRef, mapStyle) { this._datasets = datasetsRef this._orderedDatasets = orderedDatasetsRef - if (mapStyle) { - this._mapStyle = mapStyle - } + this.attachMapStyle(mapStyle) this._invalidateChangedDatasets() }, @@ -30,6 +28,12 @@ const datasetRegistry = { attachCreateDataset (createDataset) { this._createDataset = createDataset }, _createDataset: (datasetDefinition) => createDataset(datasetDefinition), + attachMapStyle (mapStyle) { + if (mapStyle) { + this._mapStyle = mapStyle + } + }, + get mapStyle () { return this._mapStyle }, From 0447ae2bd3618420e5aef7e9179f988fb7d129d9 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Thu, 25 Jun 2026 17:03:34 +0100 Subject: [PATCH 17/53] IM-373 removed maplibreLayerAdapter's mapStyle dependency --- demo/js/esri-datasets.js | 2 +- .../src/adapters/esri/esriLayerAdapter.js | 4 +- .../adapters/maplibre/maplibreLayerAdapter.js | 58 +++++++------------ .../src/initialise/initialiseDatasets.js | 12 +--- 4 files changed, 25 insertions(+), 51 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index a8200c8aa..670728c71 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -145,5 +145,5 @@ const testGlobalVisibility = () => { } interactiveMap.on('datasets:ready', function () { - testGlobalVisibility() + // testGlobalVisibility() }) \ No newline at end of file diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index 05e36b01f..ebd917ea6 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -1,10 +1,12 @@ import VectorTileLayer from '@arcgis/core/layers/VectorTileLayer.js' import GroupLayer from '@arcgis/core/layers/GroupLayer.js' +import { LayerAdapter } from '../layerAdapter.js' import { datasetRegistry } from '../../registry/datasetRegistry.js' import { EsriDataset } from './registry/esriDataset.js' -export default class EsriLayerAdapter { +export default class EsriLayerAdapter extends LayerAdapter { constructor (mapProvider, symbolRegistry, patternRegistry) { + super() this._mapProvider = mapProvider this._map = mapProvider.map // TODO: Implement symbolRegistry and patternRegistry usage in the adapter diff --git a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js index 2c4f82c82..dc214ad45 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js @@ -40,26 +40,16 @@ export default class MaplibreLayerAdapter extends LayerAdapter { // ─── Lifecycle ────────────────────────────────────────────────────────────── - assertMapStyle (methodName, mapStyle) { - if (mapStyle !== datasetRegistry.mapStyle) { - console.error(`MaplibreLayerAdapter.${methodName} has mapStyle ${mapStyle.id}, but datasetRegistry has ${datasetRegistry.mapStyle.id}.`) - } else { - console.log(`MaplibreLayerAdapter.${methodName} has mapStyle ${mapStyle.id}, which matches datasetRegistry.`) - } - } - /** * Initialise all datasets: register patterns, add layers, then wait for idle. - * @param {Object} mapStyle * @returns {Promise} Resolves once the map has processed all layers. */ - async init (mapStyle) { - this.assertMapStyle('init', mapStyle) + async init () { const { patternConfigs, symbolConfigs } = datasetRegistry.getPatternAndSymbolConfigs() - await this.addPatternsAndSymbolsToMap(patternConfigs, symbolConfigs, mapStyle) + await this.addPatternsAndSymbolsToMap(patternConfigs, symbolConfigs) this._symbolLayerIds.clear() - datasetRegistry.forEachDataset(registryDataset => this._addLayers(registryDataset, mapStyle)) + datasetRegistry.forEachDataset(registryDataset => this._addLayers(registryDataset)) await new Promise(resolve => this._map.once('idle', resolve)) } @@ -70,8 +60,8 @@ export default class MaplibreLayerAdapter extends LayerAdapter { this._symbolLayerIds.delete(layerId) } - async addPatternsAndSymbolsToMap (patterns, symbols, mapStyle) { - this.assertMapStyle('addPatternsAndSymbolsToMap', mapStyle) + async addPatternsAndSymbolsToMap (patterns, symbols) { + const mapStyle = datasetRegistry.mapStyle const mapStyleId = mapStyle.id return Promise.all([ this._mapProvider.addPatternsToMap(patterns, mapStyleId, this._patternRegistry), @@ -98,39 +88,35 @@ export default class MaplibreLayerAdapter extends LayerAdapter { /** * Re-register patterns and re-add all layers after a basemap style change, * then reapply cached dynamic source data and hidden-feature filters. - * @param {Object} newMapStyle - * @param {Map} dynamicSources - datasetId → dynamic source instance * @returns {Promise} */ - async onMapStyleChange (newMapStyle, dynamicSources) { - this.assertMapStyle('onMapStyleChange', newMapStyle) + async onMapStyleChange () { // MapLibre wipes all sources/layers on style change — must wait for idle first await new Promise(resolve => this._map.once('idle', resolve)) const { patternConfigs, symbolConfigs } = datasetRegistry.getPatternAndSymbolConfigs() - await this.addPatternsAndSymbolsToMap(patternConfigs, symbolConfigs, newMapStyle) + await this.addPatternsAndSymbolsToMap(patternConfigs, symbolConfigs) this._symbolLayerIds.clear() datasetRegistry.forEachDataset(registryDataset => { - this._addLayers(registryDataset, newMapStyle) + this._addLayers(registryDataset) this._applyFeatureFilter(registryDataset) }) // TODO: check dynamicSources still work // Re-push cached data for dynamic sources - dynamicSources.forEach(source => source.reapply()) + this.dynamicSources.forEach(source => source.reapply()) } /** * Re-register symbols at the new pixel ratio and update icon-image on all symbol layers. * Called when the map size changes so symbols are rasterised at the correct resolution. - * @param {Object} mapStyle * @returns {Promise} */ - async onMapSizeChange (mapStyle) { - this.assertMapStyle('onMapSizeChange', mapStyle) + async onMapSizeChange () { + const { mapStyle } = datasetRegistry const { patternConfigs, symbolConfigs } = datasetRegistry.getPatternAndSymbolConfigs() - await this.addPatternsAndSymbolsToMap(patternConfigs, symbolConfigs, mapStyle) + await this.addPatternsAndSymbolsToMap(patternConfigs, symbolConfigs) datasetRegistry.forEach(registryDataset => { const { fillLayerId, symbolLayerId } = registryDataset @@ -153,13 +139,11 @@ export default class MaplibreLayerAdapter extends LayerAdapter { /** * Add a single dataset's source and layers to the map. * @param {string} datasetId - * @param {Object} mapStyle */ - async addDataset (datasetId, mapStyle) { - this.assertMapStyle('addDataset', mapStyle) + async addDataset (datasetId) { const registryDataset = datasetRegistry.getDataset(datasetId) - await this.addPatternsAndSymbolsToMap(registryDataset.patternConfigs, registryDataset.symbolConfigs, mapStyle) - this._addLayers(registryDataset, mapStyle) + await this.addPatternsAndSymbolsToMap(registryDataset.patternConfigs, registryDataset.symbolConfigs) + this._addLayers(registryDataset) } /** @@ -207,15 +191,13 @@ export default class MaplibreLayerAdapter extends LayerAdapter { /** * Update a dataset's style and re-render all its layers. * @param {string} datasetId - Updated dataset (style changes already merged in) - * @param {Object} mapStyle * @returns {Promise} */ - async applyStyle (datasetId, mapStyle) { - this.assertMapStyle('applyStyle', mapStyle) + async applyStyle (datasetId) { const registryDataset = datasetRegistry.getDataset(datasetId) registryDataset.layerIds.forEach(layerId => this.removeLayer(layerId)) - await this.addPatternsAndSymbolsToMap(registryDataset.patternConfigs, registryDataset.symbolConfigs, mapStyle) - this._addLayers(registryDataset, mapStyle) + await this.addPatternsAndSymbolsToMap(registryDataset.patternConfigs, registryDataset.symbolConfigs) + this._addLayers(registryDataset) } /** @@ -269,8 +251,8 @@ export default class MaplibreLayerAdapter extends LayerAdapter { return this._mapProvider.map.getPixelRatio() } - _addLayers (registryDataset, mapStyle) { - this.assertMapStyle('_addLayers', mapStyle) + _addLayers (registryDataset) { + const { mapStyle } = datasetRegistry const sourceId = addDatasetLayers(this._map, registryDataset, mapStyle, this._symbolRegistry, this._patternRegistry, this._pixelRatio) this._datasetSourceMap.set(registryDataset.id, sourceId) this._maintainSymbolOrdering(registryDataset) diff --git a/plugins/beta/datasets/src/initialise/initialiseDatasets.js b/plugins/beta/datasets/src/initialise/initialiseDatasets.js index 186483807..ecd97f1f7 100644 --- a/plugins/beta/datasets/src/initialise/initialiseDatasets.js +++ b/plugins/beta/datasets/src/initialise/initialiseDatasets.js @@ -41,30 +41,20 @@ export const initialiseDatasets = ({ }) dynamicSources.set(registryDataset.id, dynamicSource) }) + adapter.attachDynamicSources(dynamicSources) // TODO - apply dynamic source defaults here, and include in mappedDatasets dispatch({ type: 'SET_DATASETS', payload: { datasets: processedDatasets, mappedDatasets, orderedDatasets } }) eventBus.emit('datasets:ready') }) - // let currentMapStyle = mapStyle - - // Handle basemap style changes — delegate entirely to the adapter - // const onSetMapStyle = (newMapStyle) => { - // currentMapStyle = newMapStyle - // adapter.onMapStyleChange(newMapStyle, dynamicSources) - // } - const onMapSizeChange = () => { - // adapter.onMapSizeChange(currentMapStyle) adapter.onMapSizeChange() } - // eventBus.on(events.MAP_SET_STYLE, onSetMapStyle) eventBus.on(events.MAP_SIZE_CHANGE, onMapSizeChange) return { remove () { - // eventBus.off(events.MAP_SET_STYLE, onSetMapStyle) eventBus.off(events.MAP_SIZE_CHANGE, onMapSizeChange) // Clean up dynamic sources From 1270c58e05b593d3886bfbdd967326ca684ee9a0 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 26 Jun 2026 09:07:15 +0100 Subject: [PATCH 18/53] IM-373 initialiseDatasets.test.js updated to reflect new mapStyle method --- .../src/initialise/initialiseDatasets.test.js | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js b/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js index 46743d48d..783028052 100644 --- a/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js +++ b/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js @@ -16,6 +16,7 @@ jest.mock('../registry/datasetRegistry.js', () => ({ const makeAdapter = (overrides = {}) => ({ init: jest.fn().mockResolvedValue(undefined), + attachDynamicSources: jest.fn(), onMapStyleChange: jest.fn(), onMapSizeChange: jest.fn(), setData: jest.fn(), @@ -42,7 +43,7 @@ const makeArgs = (overrides = {}) => { pluginStateRef: {}, mapStyle: { layers: [] }, mapProvider: { map: {} }, - events: { MAP_SET_STYLE: 'map:setStyle', MAP_SIZE_CHANGE: 'map:sizeChange' }, + events: { MAP_SIZE_CHANGE: 'map:sizeChange' }, dispatch: jest.fn(), eventBus, ...overrides @@ -94,10 +95,9 @@ describe('initialiseDatasets', () => { expect(args.dispatch).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'SET_GLOBAL_STATE' })) }) - it('registers MAP_SET_STYLE and MAP_SIZE_CHANGE event listeners', () => { + it('registers MAP_SET_STYLE event listeners', () => { const args = makeArgs() initialiseDatasets(args) - expect(args.eventBus.on).toHaveBeenCalledWith('map:setStyle', expect.any(Function)) expect(args.eventBus.on).toHaveBeenCalledWith('map:sizeChange', expect.any(Function)) }) @@ -153,28 +153,11 @@ describe('initialiseDatasets', () => { // ─── event handlers ─────────────────────────────────────────────────────────── describe('event handlers', () => { - it('delegates MAP_SET_STYLE to adapter.onMapStyleChange', () => { - const args = makeArgs() - initialiseDatasets(args) - const newStyle = { layers: [{ id: 'new' }] } - args.eventBus._handlers['map:setStyle'](newStyle) - expect(args.adapter.onMapStyleChange).toHaveBeenCalledWith(newStyle, expect.any(Map)) - }) - it('delegates MAP_SIZE_CHANGE to adapter.onMapSizeChange with current mapStyle', () => { const args = makeArgs() initialiseDatasets(args) args.eventBus._handlers['map:sizeChange']() - expect(args.adapter.onMapSizeChange).toHaveBeenCalledWith(args.mapStyle) - }) - - it('uses the updated mapStyle after MAP_SET_STYLE', () => { - const args = makeArgs() - initialiseDatasets(args) - const newStyle = { layers: [{ id: 'updated' }] } - args.eventBus._handlers['map:setStyle'](newStyle) - args.eventBus._handlers['map:sizeChange']() - expect(args.adapter.onMapSizeChange).toHaveBeenCalledWith(newStyle) + expect(args.adapter.onMapSizeChange).toHaveBeenCalled() }) }) @@ -185,7 +168,6 @@ describe('returned API', () => { const args = makeArgs() const instance = initialiseDatasets(args) instance.remove() - expect(args.eventBus.off).toHaveBeenCalledWith('map:setStyle', expect.any(Function)) expect(args.eventBus.off).toHaveBeenCalledWith('map:sizeChange', expect.any(Function)) expect(args.adapter.destroy).toHaveBeenCalled() }) From 3445851bc9a9ff1cb8d0643a56ae4805f00facbb Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 26 Jun 2026 09:29:51 +0100 Subject: [PATCH 19/53] IM-373 fixed maplibreLayerAdapter.test.js --- .../src/adapters/maplibre/maplibreLayerAdapter.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js index 247d1b54b..ab0bb242b 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +++ b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js @@ -42,9 +42,10 @@ const makeMapProvider = (map) => ({ const MAP_STYLE = { id: 'outdoor', layers: [] } let map, mapProvider, adapter +const dynamicSources = new Map() beforeEach(() => { - datasetRegistry.attachCreateDataset(def => new MapLibreDataset(def)) + datasetRegistry.attachMapStyle(MAP_STYLE) symbolRegistry.clear() symbolRegistry.initialise() patternRegistry.clear() @@ -53,6 +54,8 @@ beforeEach(() => { map = makeMap() mapProvider = makeMapProvider(map) adapter = new MaplibreLayerAdapter(mapProvider, symbolRegistry, patternRegistry) + adapter.attachDynamicSources(dynamicSources) + datasetRegistry.attachCreateDataset(adapter.createDataset) }) // ─── init ───────────────────────────────────────────────────────────────────── From 3a947b43f4075485f5b9993d061c36ffdfcc7400 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 26 Jun 2026 09:49:24 +0100 Subject: [PATCH 20/53] IM-373 maplibreLayerAdapter fully covered --- .../maplibre/maplibreLayerAdapter.test.js | 98 +++++++++++++------ 1 file changed, 69 insertions(+), 29 deletions(-) diff --git a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js index ab0bb242b..7470c28fa 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +++ b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js @@ -62,26 +62,26 @@ beforeEach(() => { describe('init', () => { it('calls addPatternsAndSymbolsToMap before adding layers', async () => { - await adapter.init(MAP_STYLE) + await adapter.init() expect(mapProvider.addPatternsToMap).toHaveBeenCalled() expect(mapProvider.addSymbolsToMap).toHaveBeenCalled() }) it('adds fill + stroke layers for existing-fields', async () => { - await adapter.init(MAP_STYLE) + await adapter.init() expect(map.getLayer('existing-fields')).toMatchObject({ type: 'fill' }) expect(map.getLayer('existing-fields-stroke')).toMatchObject({ type: 'line' }) }) it('adds symbol layers for all historic-monuments sublayers', async () => { - await adapter.init(MAP_STYLE) + await adapter.init() expect(map.getLayer('historic-monuments-prehistoric')).toMatchObject({ type: 'symbol' }) expect(map.getLayer('historic-monuments-roman')).toMatchObject({ type: 'symbol' }) expect(map.getLayer('historic-monuments-medieval')).toMatchObject({ type: 'symbol' }) }) it('adds fill + stroke layers for land-covers sublayers', async () => { - await adapter.init(MAP_STYLE) + await adapter.init() expect(map.getLayer('land-covers-130-131')).toMatchObject({ type: 'fill' }) expect(map.getLayer('land-covers-130-131-stroke')).toMatchObject({ type: 'line' }) expect(map.getLayer('land-covers-332')).toMatchObject({ type: 'fill' }) @@ -90,49 +90,49 @@ describe('init', () => { }) it('adds a stroke-only layer for hedge-control', async () => { - await adapter.init(MAP_STYLE) + await adapter.init() expect(map.getLayer('hedge-control')).toMatchObject({ type: 'line' }) }) it('adds a vector source for existing-fields', async () => { - await adapter.init(MAP_STYLE) + await adapter.init() const ds = datasetRegistry.getDataset('existing-fields') expect(map.addSource).toHaveBeenCalledWith(ds.sourceId, expect.objectContaining({ type: 'vector' })) }) it('adds a geojson source for historic-monuments', async () => { - await adapter.init(MAP_STYLE) + await adapter.init() const ds = datasetRegistry.getDataset('historic-monuments') expect(map.addSource).toHaveBeenCalledWith(ds.sourceId, expect.objectContaining({ type: 'geojson' })) }) it('adds a dynamic geojson source for land-covers', async () => { - await adapter.init(MAP_STYLE) + await adapter.init() const ds = datasetRegistry.getDataset('land-covers') expect(map.addSource).toHaveBeenCalledWith(ds.sourceId, expect.objectContaining({ type: 'geojson' })) }) it('adds each source only once even when multiple sublayers share it', async () => { - await adapter.init(MAP_STYLE) + await adapter.init() const ds = datasetRegistry.getDataset('historic-monuments') const calls = map.addSource.mock.calls.filter(([id]) => id === ds.sourceId) expect(calls).toHaveLength(1) }) it('waits for map idle before resolving', async () => { - await adapter.init(MAP_STYLE) + await adapter.init() expect(map.once).toHaveBeenCalledWith('idle', expect.any(Function)) }) it('tracks symbol layers in _symbolLayerIds', async () => { - await adapter.init(MAP_STYLE) + await adapter.init() expect(adapter._symbolLayerIds.has('historic-monuments-prehistoric')).toBe(true) expect(adapter._symbolLayerIds.has('historic-monuments-roman')).toBe(true) expect(adapter._symbolLayerIds.has('historic-monuments-medieval')).toBe(true) }) it('does not track fill/line layers in _symbolLayerIds', async () => { - await adapter.init(MAP_STYLE) + await adapter.init() expect(adapter._symbolLayerIds.has('existing-fields')).toBe(false) expect(adapter._symbolLayerIds.has('land-covers-130-131')).toBe(false) expect(adapter._symbolLayerIds.has('hedge-control')).toBe(false) @@ -143,7 +143,7 @@ describe('init', () => { describe('removeLayer', () => { it('removes an existing layer from the map', async () => { - await adapter.init(MAP_STYLE) + await adapter.init() adapter.removeLayer('existing-fields') expect(map.removeLayer).toHaveBeenCalledWith('existing-fields') expect(map.getLayer('existing-fields')).toBeNull() @@ -155,7 +155,7 @@ describe('removeLayer', () => { }) it('removes the layer id from _symbolLayerIds', async () => { - await adapter.init(MAP_STYLE) + await adapter.init() expect(adapter._symbolLayerIds.has('historic-monuments-prehistoric')).toBe(true) adapter.removeLayer('historic-monuments-prehistoric') expect(adapter._symbolLayerIds.has('historic-monuments-prehistoric')).toBe(false) @@ -165,7 +165,7 @@ describe('removeLayer', () => { // ─── destroy ────────────────────────────────────────────────────────────────── describe('destroy', () => { - beforeEach(async () => { await adapter.init(MAP_STYLE) }) + beforeEach(async () => { await adapter.init() }) it('removes all layers from the map', () => { adapter.destroy() @@ -181,6 +181,11 @@ describe('destroy', () => { adapter.destroy() expect(adapter._datasetSourceMap.size).toBe(0) }) + + it('does not throw when getStyle returns null (covers _getLayersUsingSource early return)', () => { + map.getStyle.mockReturnValue(null) + expect(() => adapter.destroy()).not.toThrow() + }) }) // ─── addDataset ─────────────────────────────────────────────────────────────── @@ -204,12 +209,18 @@ describe('addDataset', () => { expect(mapProvider.addPatternsToMap).toHaveBeenCalled() expect(mapProvider.addSymbolsToMap).toHaveBeenCalled() }) + + it('does not call moveLayer when getStyle returns no layers (covers _getFirstSymbolLayerId null branch)', async () => { + map.getStyle.mockReturnValue({}) + await adapter.addDataset('existing-fields') + expect(map.moveLayer).not.toHaveBeenCalled() + }) }) // ─── removeDataset ──────────────────────────────────────────────────────────── describe('removeDataset', () => { - beforeEach(async () => { await adapter.init(MAP_STYLE) }) + beforeEach(async () => { await adapter.init() }) it('removes all layers for existing-fields', () => { adapter.removeDataset('existing-fields') @@ -257,7 +268,7 @@ describe('removeDataset', () => { // ─── setData ────────────────────────────────────────────────────────────────── describe('setData', () => { - beforeEach(async () => { await adapter.init(MAP_STYLE) }) + beforeEach(async () => { await adapter.init() }) it('calls source.setData for a dynamic dataset (land-covers)', () => { const ds = datasetRegistry.getDataset('land-covers') @@ -280,7 +291,7 @@ describe('setData', () => { // ─── applyDatasetVisibility ─────────────────────────────────────────────────── describe('applyDatasetVisibility', () => { - beforeEach(async () => { await adapter.init(MAP_STYLE) }) + beforeEach(async () => { await adapter.init() }) it('sets visibility to "visible" for existing-fields layers', () => { adapter.applyDatasetVisibility('existing-fields') @@ -305,12 +316,19 @@ describe('applyDatasetVisibility', () => { adapter.applyDatasetVisibility('unknown') expect(map.setLayoutProperty.mock.calls.length).toBe(before) }) + + it('skips setLayoutProperty for a layer that has been removed from the map', () => { + map._layers.delete('existing-fields') + map.setLayoutProperty.mockClear() + adapter.applyDatasetVisibility('existing-fields') + expect(map.setLayoutProperty).not.toHaveBeenCalledWith('existing-fields', 'visibility', expect.any(String)) + }) }) // ─── applyGlobalVisibility ──────────────────────────────────────────────────── describe('applyGlobalVisibility', () => { - beforeEach(async () => { await adapter.init(MAP_STYLE) }) + beforeEach(async () => { await adapter.init() }) it('sets visibility on layers for all top-level datasets', () => { map.setLayoutProperty.mockClear() @@ -327,7 +345,7 @@ describe('applyGlobalVisibility', () => { // ─── applyFeatureFilter ─────────────────────────────────────────────────────── describe('applyFeatureFilter', () => { - beforeEach(async () => { await adapter.init(MAP_STYLE) }) + beforeEach(async () => { await adapter.init() }) it('sets filter on all land-covers sublayers (parent has hiddenFeatures: [42])', () => { adapter.applyFeatureFilter('land-covers') @@ -361,12 +379,19 @@ describe('applyFeatureFilter', () => { adapter.applyFeatureFilter('unknown') expect(map.setFilter).not.toHaveBeenCalled() }) + + it('skips setFilter for a sublayer whose layer has been removed from the map', () => { + map._layers.delete('land-covers-130-131') + map.setFilter.mockClear() + adapter.applyFeatureFilter('land-covers') + expect(map.setFilter).not.toHaveBeenCalledWith('land-covers-130-131', expect.anything()) + }) }) // ─── applyDatasetOpacity ────────────────────────────────────────────────────── describe('applyDatasetOpacity', () => { - beforeEach(async () => { await adapter.init(MAP_STYLE) }) + beforeEach(async () => { await adapter.init() }) it('sets fill-opacity on the fill layer for existing-fields', () => { adapter.applyDatasetOpacity('existing-fields') @@ -396,12 +421,19 @@ describe('applyDatasetOpacity', () => { adapter.applyDatasetOpacity('unknown') expect(map.setPaintProperty.mock.calls.length).toBe(before) }) + + it('does not call setPaintProperty for a layer that has been removed from the map (covers _setPaintOpacity early return)', () => { + map._layers.delete('existing-fields') + map.setPaintProperty.mockClear() + adapter.applyDatasetOpacity('existing-fields') + expect(map.setPaintProperty).not.toHaveBeenCalledWith('existing-fields', expect.any(String), expect.any(Number)) + }) }) // ─── applyGlobalOpacity ─────────────────────────────────────────────────────── describe('applyGlobalOpacity', () => { - beforeEach(async () => { await adapter.init(MAP_STYLE) }) + beforeEach(async () => { await adapter.init() }) it('sets paint opacity on layers for all datasets', () => { map.setPaintProperty.mockClear() @@ -418,7 +450,7 @@ describe('applyGlobalOpacity', () => { // ─── applyStyle ─────────────────────────────────────────────────────────────── describe('applyStyle', () => { - beforeEach(async () => { await adapter.init(MAP_STYLE) }) + beforeEach(async () => { await adapter.init() }) it('removes old fill + stroke layers then re-adds them for existing-fields', async () => { await adapter.applyStyle('existing-fields', MAP_STYLE) @@ -444,7 +476,7 @@ describe('applyStyle', () => { // ─── onMapStyleChange ───────────────────────────────────────────────────────── describe('onMapStyleChange', () => { - beforeEach(async () => { await adapter.init(MAP_STYLE) }) + beforeEach(async () => { await adapter.init() }) it('waits for map idle before proceeding', async () => { let idleCb @@ -483,11 +515,11 @@ describe('onMapStyleChange', () => { // ─── onMapSizeChange ────────────────────────────────────────────────────────── describe('onMapSizeChange', () => { - beforeEach(async () => { await adapter.init(MAP_STYLE) }) + beforeEach(async () => { await adapter.init() }) it('updates icon-image layout property for historic-monuments symbol layers', async () => { map.setLayoutProperty.mockClear() - await adapter.onMapSizeChange(MAP_STYLE) + await adapter.onMapSizeChange() const symbolUpdates = map.setLayoutProperty.mock.calls.filter(([, prop]) => prop === 'icon-image') const ids = symbolUpdates.map(([id]) => id) expect(ids).toContain('historic-monuments-prehistoric') @@ -497,7 +529,7 @@ describe('onMapSizeChange', () => { it('updates fill-pattern paint property for land-covers sublayers', async () => { map.setPaintProperty.mockClear() - await adapter.onMapSizeChange(MAP_STYLE) + await adapter.onMapSizeChange() const patternUpdates = map.setPaintProperty.mock.calls.filter(([, prop]) => prop === 'fill-pattern') const ids = patternUpdates.map(([id]) => id) expect(ids).toContain('land-covers-130-131') @@ -506,14 +538,22 @@ describe('onMapSizeChange', () => { it('does not update fill-pattern for existing-fields (no fillPattern style)', async () => { map.setPaintProperty.mockClear() - await adapter.onMapSizeChange(MAP_STYLE) + await adapter.onMapSizeChange() const patternUpdates = map.setPaintProperty.mock.calls.filter(([, prop]) => prop === 'fill-pattern') expect(patternUpdates.map(([id]) => id)).not.toContain('existing-fields') }) it('calls addPatternsAndSymbolsToMap', async () => { mapProvider.addPatternsToMap.mockClear() - await adapter.onMapSizeChange(MAP_STYLE) + await adapter.onMapSizeChange() expect(mapProvider.addPatternsToMap).toHaveBeenCalled() }) + + it('does not call setLayoutProperty for icon-image when getSymbolImageId returns null', async () => { + jest.spyOn(symbolRegistry, 'getSymbolImageId').mockReturnValue(null) + map.setLayoutProperty.mockClear() + await adapter.onMapSizeChange() + const symbolUpdates = map.setLayoutProperty.mock.calls.filter(([, prop]) => prop === 'icon-image') + expect(symbolUpdates).toHaveLength(0) + }) }) From cd75575a1044149cc2ccaf999bd0f80d06b7d62e Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 26 Jun 2026 09:56:10 +0100 Subject: [PATCH 21/53] IM-373 mapLibreDataset fully covered --- .../maplibre/registry/mapLibreDataset.test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js b/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js index d2a9c1cb3..5d6adb4a4 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js +++ b/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js @@ -309,6 +309,14 @@ describe('MapLibreDataset', () => { expect(datasetRegistry.getDataset('ds-string-with-prop').source).toEqual(expect.objectContaining({ type: 'geojson' })) expect(logger.warn).not.toHaveBeenCalled() }) + + it('adds promoteId keyed by sourceLayer when a vector source has both idProperty and sourceLayer', () => { + datasetRegistry.mockExtend({ 'ds-tiles-id-prop': { id: 'ds-tiles-id-prop', tiles: ['https://example.com/{z}/{x}/{y}'], idProperty: 'myId', sourceLayer: 'my-layer' } }) + expect(datasetRegistry.getDataset('ds-tiles-id-prop').source).toEqual(expect.objectContaining({ + type: 'vector', + promoteId: { 'my-layer': 'myId' } + })) + }) }) describe('getSymbolSource', () => { @@ -438,6 +446,10 @@ describe('MapLibreDataset', () => { expect(datasetRegistry.getDataset('land-covers')._hiddenFeaturesIdExpression).toEqual(['to-string', ['get', 'id']]) }) + it('uses get(idProperty) for a non-dynamic dataset with idProperty set', () => { + expect(datasetRegistry.getDataset('ds-no-transform')._hiddenFeaturesIdExpression).toEqual(['to-string', ['get', 'id']]) + }) + it('uses the feature id when idProperty is not set', () => { expect(datasetRegistry.getDataset('ds-bare')._hiddenFeaturesIdExpression).toEqual(['to-string', ['id']]) }) From 477cd526b56ce8d13e025bcc5baecb47ae066488 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 26 Jun 2026 10:19:17 +0100 Subject: [PATCH 22/53] IM-373 covered layerBuilders --- .../adapters/maplibre/layerBuilders.test.js | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 plugins/beta/datasets/src/adapters/maplibre/layerBuilders.test.js diff --git a/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.test.js b/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.test.js new file mode 100644 index 000000000..6d3fdd8e9 --- /dev/null +++ b/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.test.js @@ -0,0 +1,83 @@ +import { addFillLayer, addStrokeLayer, addSymbolLayer } from './layerBuilders.js' + +// ─── helpers ────────────────────────────────────────────────────────────────── + +const makeMap = () => { + const layers = new Map() + return { + getLayer: jest.fn(id => layers.get(id) ?? null), + getSource: jest.fn(() => null), + addLayer: jest.fn(spec => layers.set(spec.id, spec)), + addSource: jest.fn() + } +} + +const makeDataset = (overrides = {}) => ({ + id: 'test-ds', + hasFill: false, + fillLayerId: null, + hasStroke: false, + strokeLayerId: null, + hasSymbol: false, + symbolLayerId: null, + style: {}, + opacity: 1, + getFillSource: jest.fn(paint => ({ id: 'test-ds', type: 'fill', paint })), + getStrokeSource: jest.fn(paint => ({ id: 'test-ds-stroke', type: 'line', paint })), + getSymbolSource: jest.fn((imageId, anchor) => ({ id: 'test-ds', type: 'symbol', layout: { 'icon-image': imageId } })), + ...overrides +}) + +// ─── addFillLayer ───────────────────────────────────────────────────────────── + +describe('addFillLayer', () => { + it('uses pixelRatio = 1 when the argument is omitted', () => { + const map = makeMap() + const patternRegistry = { getPatternImageId: jest.fn(() => null) } + const ds = makeDataset({ hasFill: true, fillLayerId: 'test-ds', style: { fill: '#ff0000' } }) + addFillLayer(map, ds, 'outdoor', patternRegistry) // no pixelRatio argument + expect(patternRegistry.getPatternImageId).toHaveBeenCalledWith(ds.style, 'outdoor', 1) + }) +}) + +// ─── addStrokeLayer ─────────────────────────────────────────────────────────── + +describe('addStrokeLayer', () => { + it('defaults line-width to 1 when strokeWidth is not set on the style', () => { + const map = makeMap() + const ds = makeDataset({ hasStroke: true, strokeLayerId: 'test-ds-stroke', style: { stroke: '#000000' } }) + addStrokeLayer(map, ds, 'outdoor') + expect(ds.getStrokeSource).toHaveBeenCalledWith(expect.objectContaining({ 'line-width': 1 })) + }) + + it('includes line-dasharray in the paint when strokeDashArray is set', () => { + const map = makeMap() + const ds = makeDataset({ + hasStroke: true, + strokeLayerId: 'test-ds-stroke', + style: { stroke: '#000000', strokeWidth: 2, strokeDashArray: [4, 2] } + }) + addStrokeLayer(map, ds, 'outdoor') + expect(ds.getStrokeSource).toHaveBeenCalledWith(expect.objectContaining({ 'line-dasharray': [4, 2] })) + }) +}) + +// ─── addSymbolLayer ─────────────────────────────────────────────────────────── + +describe('addSymbolLayer', () => { + it('returns early without adding a layer when symbolDef is null', () => { + const map = makeMap() + const symbolRegistry = { getSymbolDef: jest.fn(() => null), getSymbolImageId: jest.fn() } + const ds = makeDataset({ hasSymbol: true, symbolLayerId: 'test-ds', style: {} }) + addSymbolLayer(map, ds, { id: 'outdoor' }, symbolRegistry, 1) + expect(map.addLayer).not.toHaveBeenCalled() + }) + + it('returns early without adding a layer when imageId is null', () => { + const map = makeMap() + const symbolRegistry = { getSymbolDef: jest.fn(() => ({})), getSymbolImageId: jest.fn(() => null) } + const ds = makeDataset({ hasSymbol: true, symbolLayerId: 'test-ds', style: {} }) + addSymbolLayer(map, ds, { id: 'outdoor' }, symbolRegistry, 1) + expect(map.addLayer).not.toHaveBeenCalled() + }) +}) From 3ddf0ba9db1285d9e722b86d24446416bad5f9be Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 26 Jun 2026 13:19:56 +0100 Subject: [PATCH 23/53] IM-373 covered esriDataset --- .../src/adapters/esri/esriLayerAdapter.js | 16 --- .../esri/registry/esriDataset.test.js | 118 ++++++++++++++++++ 2 files changed, 118 insertions(+), 16 deletions(-) create mode 100644 plugins/beta/datasets/src/adapters/esri/registry/esriDataset.test.js diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index ebd917ea6..0869642b8 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -45,22 +45,6 @@ export default class EsriLayerAdapter extends LayerAdapter { // Finally show all layers that are visible based on the dataset/mapStyle visibility await Promise.all(topLevelDatasets.map(registryDataset => this.applyDatasetVisibility(registryDataset.id))) - - // Log the layers for debugging - // this.logLayers('_vectorTileLayers') - // console.log('this._vectorTileOpacityLayers', this._vectorTileOpacityLayers) - // console.log('this._groupLayers', this._groupLayers) - } - - logLayers (name) { - const obj = this[name] - console.log(`${name}:`) - Object.entries(obj).forEach(([datasetId, layer]) => { - console.log(` ${datasetId}`) - layer.styleRepository.layers.forEach(styleLayer => { - console.log(` ${styleLayer.id}`) - }) - }) } _addGroupLayer (esriGroupId) { diff --git a/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.test.js b/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.test.js new file mode 100644 index 000000000..c5f890caf --- /dev/null +++ b/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.test.js @@ -0,0 +1,118 @@ +import { EsriDataset } from './esriDataset.js' +import { datasetRegistry } from '../../../registry/datasetRegistry.js' + +jest.mock('../../../registry/datasetRegistry.js') + +const MAP_STYLE = { id: 'outdoor' } + +describe('EsriDataset', () => { + beforeEach(() => { + datasetRegistry.attachMapStyle(MAP_STYLE) + datasetRegistry.attachCreateDataset(def => new EsriDataset(def)) + datasetRegistry.mockExtend({ + // applyLayerPaintProperties + 'esri-fill-stroke': { id: 'esri-fill-stroke', style: { fill: '#ff0000', stroke: '#0000ff' } }, + 'esri-stroke-only': { id: 'esri-stroke-only', style: { stroke: '#0000ff' } }, + 'esri-fill-only': { id: 'esri-fill-only', style: { fill: '#ff0000' } }, + 'esri-bare': { id: 'esri-bare' }, + // esriGroupId — parent owns the id, child inherits it + 'esri-group': { id: 'esri-group', esriGroupId: 'group-123', sublayerIds: ['esri-child'] }, + 'esri-child': { id: 'esri-child', parentId: 'esri-group' }, + // esriGroupId — explicitly set to null (distinct from undefined) + 'esri-group-null': { id: 'esri-group-null', esriGroupId: null }, + // useServerStyle + 'esri-server-style': { id: 'esri-server-style', esriUseServerStyle: true }, + 'esri-no-server': { id: 'esri-no-server' } + }) + }) + + // ─── applyLayerPaintProperties ────────────────────────────────────────────── + + describe('applyLayerPaintProperties', () => { + it('adds line-color when the dataset has a stroke style', () => { + const ds = datasetRegistry.getDataset('esri-stroke-only') + const paint = {} + ds.applyLayerPaintProperties(paint) + expect(paint['line-color']).toBeDefined() + }) + + it('resolves line-color against the current map style id', () => { + const ds = datasetRegistry.getDataset('esri-stroke-only') + const paint = {} + ds.applyLayerPaintProperties(paint) + expect(paint['line-color']).toBe('#0000ff') + }) + + it('adds fill-color when the dataset has a fill style', () => { + const ds = datasetRegistry.getDataset('esri-fill-only') + const paint = {} + ds.applyLayerPaintProperties(paint) + expect(paint['fill-color']).toBeDefined() + }) + + it('resolves fill-color against the current map style id', () => { + const ds = datasetRegistry.getDataset('esri-fill-only') + const paint = {} + ds.applyLayerPaintProperties(paint) + expect(paint['fill-color']).toBe('#ff0000') + }) + + it('adds both line-color and fill-color when the dataset has stroke and fill', () => { + const ds = datasetRegistry.getDataset('esri-fill-stroke') + const paint = {} + ds.applyLayerPaintProperties(paint) + expect(paint['line-color']).toBe('#0000ff') + expect(paint['fill-color']).toBe('#ff0000') + }) + + it('does not add line-color or fill-color when the dataset has neither', () => { + const ds = datasetRegistry.getDataset('esri-bare') + const paint = {} + ds.applyLayerPaintProperties(paint) + expect(paint['line-color']).toBeUndefined() + expect(paint['fill-color']).toBeUndefined() + }) + + it('returns the paint object', () => { + const ds = datasetRegistry.getDataset('esri-fill-stroke') + const paint = { existing: true } + expect(ds.applyLayerPaintProperties(paint)).toBe(paint) + }) + }) + + // ─── esriGroupId ──────────────────────────────────────────────────────────── + + describe('esriGroupId', () => { + it('returns the esriGroupId from the dataset definition when set', () => { + const ds = datasetRegistry.getDataset('esri-group') + expect(ds.esriGroupId).toBe('group-123') + }) + + it('returns null when esriGroupId is explicitly null', () => { + const ds = datasetRegistry.getDataset('esri-group-null') + expect(ds.esriGroupId).toBeNull() + }) + + it('inherits esriGroupId from the parent when own esriGroupId is undefined', () => { + const child = datasetRegistry.getDataset('esri-child') + expect(child.esriGroupId).toBe('group-123') + }) + + it('returns undefined when esriGroupId is not set and there is no parent', () => { + const ds = datasetRegistry.getDataset('esri-bare') + expect(ds.esriGroupId).toBeUndefined() + }) + }) + + // ─── useServerStyle ───────────────────────────────────────────────────────── + + describe('useServerStyle', () => { + it('returns true when esriUseServerStyle is true', () => { + expect(datasetRegistry.getDataset('esri-server-style').useServerStyle).toBe(true) + }) + + it('returns false when esriUseServerStyle is not set', () => { + expect(datasetRegistry.getDataset('esri-no-server').useServerStyle).toBe(false) + }) + }) +}) From c1ff7180096fd1990525380e7b34e8975b9d73cf Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 26 Jun 2026 13:36:13 +0100 Subject: [PATCH 24/53] IM-373 removed some mocks in esriDataset.test.js --- .../esri/registry/esriDataset.test.js | 55 ++----------------- 1 file changed, 5 insertions(+), 50 deletions(-) diff --git a/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.test.js b/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.test.js index c5f890caf..206c868fc 100644 --- a/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.test.js +++ b/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.test.js @@ -11,58 +11,24 @@ describe('EsriDataset', () => { datasetRegistry.attachCreateDataset(def => new EsriDataset(def)) datasetRegistry.mockExtend({ // applyLayerPaintProperties - 'esri-fill-stroke': { id: 'esri-fill-stroke', style: { fill: '#ff0000', stroke: '#0000ff' } }, - 'esri-stroke-only': { id: 'esri-stroke-only', style: { stroke: '#0000ff' } }, - 'esri-fill-only': { id: 'esri-fill-only', style: { fill: '#ff0000' } }, 'esri-bare': { id: 'esri-bare' }, // esriGroupId — parent owns the id, child inherits it 'esri-group': { id: 'esri-group', esriGroupId: 'group-123', sublayerIds: ['esri-child'] }, 'esri-child': { id: 'esri-child', parentId: 'esri-group' }, - // esriGroupId — explicitly set to null (distinct from undefined) - 'esri-group-null': { id: 'esri-group-null', esriGroupId: null }, // useServerStyle - 'esri-server-style': { id: 'esri-server-style', esriUseServerStyle: true }, - 'esri-no-server': { id: 'esri-no-server' } + 'esri-server-style': { id: 'esri-server-style', esriUseServerStyle: true } }) }) // ─── applyLayerPaintProperties ────────────────────────────────────────────── describe('applyLayerPaintProperties', () => { - it('adds line-color when the dataset has a stroke style', () => { - const ds = datasetRegistry.getDataset('esri-stroke-only') - const paint = {} - ds.applyLayerPaintProperties(paint) - expect(paint['line-color']).toBeDefined() - }) - - it('resolves line-color against the current map style id', () => { - const ds = datasetRegistry.getDataset('esri-stroke-only') - const paint = {} - ds.applyLayerPaintProperties(paint) - expect(paint['line-color']).toBe('#0000ff') - }) - - it('adds fill-color when the dataset has a fill style', () => { - const ds = datasetRegistry.getDataset('esri-fill-only') - const paint = {} - ds.applyLayerPaintProperties(paint) - expect(paint['fill-color']).toBeDefined() - }) - - it('resolves fill-color against the current map style id', () => { - const ds = datasetRegistry.getDataset('esri-fill-only') - const paint = {} - ds.applyLayerPaintProperties(paint) - expect(paint['fill-color']).toBe('#ff0000') - }) - it('adds both line-color and fill-color when the dataset has stroke and fill', () => { - const ds = datasetRegistry.getDataset('esri-fill-stroke') + const ds = datasetRegistry.getDataset('land-covers-other') const paint = {} ds.applyLayerPaintProperties(paint) - expect(paint['line-color']).toBe('#0000ff') - expect(paint['fill-color']).toBe('#ff0000') + expect(paint['line-color']).toBe('#1565C0') + expect(paint['fill-color']).toBe('rgba(0,0,255,0.1)') }) it('does not add line-color or fill-color when the dataset has neither', () => { @@ -72,12 +38,6 @@ describe('EsriDataset', () => { expect(paint['line-color']).toBeUndefined() expect(paint['fill-color']).toBeUndefined() }) - - it('returns the paint object', () => { - const ds = datasetRegistry.getDataset('esri-fill-stroke') - const paint = { existing: true } - expect(ds.applyLayerPaintProperties(paint)).toBe(paint) - }) }) // ─── esriGroupId ──────────────────────────────────────────────────────────── @@ -88,11 +48,6 @@ describe('EsriDataset', () => { expect(ds.esriGroupId).toBe('group-123') }) - it('returns null when esriGroupId is explicitly null', () => { - const ds = datasetRegistry.getDataset('esri-group-null') - expect(ds.esriGroupId).toBeNull() - }) - it('inherits esriGroupId from the parent when own esriGroupId is undefined', () => { const child = datasetRegistry.getDataset('esri-child') expect(child.esriGroupId).toBe('group-123') @@ -112,7 +67,7 @@ describe('EsriDataset', () => { }) it('returns false when esriUseServerStyle is not set', () => { - expect(datasetRegistry.getDataset('esri-no-server').useServerStyle).toBe(false) + expect(datasetRegistry.getDataset('land-covers-other').useServerStyle).toBe(false) }) }) }) From 97b5b0350bc160f6320b1157f4074548f9aeccd6 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Wed, 8 Jul 2026 11:44:00 +0100 Subject: [PATCH 25/53] IM-373 esriLayerAdapter method signatures match mapLibres --- .../src/adapters/esri/esriLayerAdapter.js | 21 +- .../adapters/esri/esriLayerAdapter.test.js | 266 ++++++++++++++++++ .../datasets/src/initialise/DatasetsInit.jsx | 2 +- .../src/reducers/__data__/esriDatasets.js | 144 ++++++++++ .../src/registry/__mocks__/datasetRegistry.js | 9 + plugins/beta/datasets/src/registry/dataset.js | 2 +- 6 files changed, 428 insertions(+), 16 deletions(-) create mode 100644 plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js create mode 100644 plugins/beta/datasets/src/reducers/__data__/esriDatasets.js diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index 0869642b8..f9303757e 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -29,16 +29,16 @@ export default class EsriLayerAdapter extends LayerAdapter { return new EsriDataset(datasetDefinition) } - async init (mapStyle) { + async init () { const topLevelDatasets = datasetRegistry.topLevelDatasets() // ensure the datasets are added in order for await (const registryDataset of topLevelDatasets) { - await this._addLayers(registryDataset, mapStyle) + await this._addLayers(registryDataset) } - // onMapStyleChange: handles showing and hiding sublayers based on the mapStyle + // onMapStyleChange: handles showing and hiding sublayers based on the current mapStyle // and updating the paint properties of the layers based on the dataset/mapStyle style - await this.onMapStyleChange(datasetRegistry.mapStyle, null) + await this.onMapStyleChange() // Apply opacity to all layers await this.applyGlobalOpacity() @@ -61,7 +61,7 @@ export default class EsriLayerAdapter extends LayerAdapter { return this._groupLayers[esriGroupId] } - async _addLayers (registryDataset, mapStyle) { + async _addLayers (registryDataset) { const { esriGroupId } = registryDataset const vectorTileParent = esriGroupId ? this._addGroupLayer(esriGroupId) : this._map const vectorTileLayer = new VectorTileLayer({ @@ -149,14 +149,7 @@ export default class EsriLayerAdapter extends LayerAdapter { console.log('TODO: applyFeatureFilter', args) } - async onMapStyleChange (newMapStyle, dynamicSources) { - if (datasetRegistry.mapStyle.id !== newMapStyle.id) { - // Ensure the datasetRegistry is aware of the new mapStyle so that the visibility and style properties of the datasets can be correctly applied - // TODO - this is a bit of a hack, we should probably have a better way to handle this - // such as having DatasetsInit listen for a mapStyle change event and then call datasetRegistry.attach with the new mapStyle - // and finally trigger this method - datasetRegistry.attach(datasetRegistry.datasets, datasetRegistry._orderedDatasets, newMapStyle) - } + async onMapStyleChange () { datasetRegistry.forEach(registryDataset => { const { id, isSublayer, esriStyleLayerId, parent } = registryDataset const vectorTileLayer = this._vectorTileLayers[isSublayer ? parent.id : id] @@ -179,5 +172,5 @@ export default class EsriLayerAdapter extends LayerAdapter { } // onMapSizeChange is not applicable to the esriLayerAdapter - async onMapSizeChange (_mapStyle) {} + async onMapSizeChange () {} } diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js new file mode 100644 index 000000000..8b40e4902 --- /dev/null +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js @@ -0,0 +1,266 @@ +import { EsriDataset } from './registry/esriDataset.js' +import EsriLayerAdapter from './esriLayerAdapter.js' +import { datasetRegistry } from '../../registry/datasetRegistry.js' + +jest.mock('../../registry/datasetRegistry.js') +jest.mock('@arcgis/core/layers/VectorTileLayer.js', () => + jest.fn().mockImplementation((opts = {}) => ({ + ...opts, + add: jest.fn(), + when: jest.fn().mockResolvedValue(undefined), + setStyleLayerVisibility: jest.fn(), + getPaintProperties: jest.fn().mockReturnValue({}), + setPaintProperties: jest.fn() + })) +) +jest.mock('@arcgis/core/layers/GroupLayer.js', () => + jest.fn().mockImplementation((opts = {}) => ({ + ...opts, + add: jest.fn() + })) +) + +// Uncovered: 80-88,124,145-149 + +const MAP_STYLE = { id: 'outdoor' } +const makeMap = () => { + const _added = {} + return { + add: jest.fn(({ id }) => { + // Ensure that the same layer is never added twice + expect(_added[id]).toBeUndefined() + _added[id] = true + }) + } +} +const makeMapProvider = (map) => ({ map }) + +describe('esriLayerAdapter', () => { + let map, mapProvider, adapter + + beforeEach(() => { + datasetRegistry.useEsriDatasets() + datasetRegistry.attachMapStyle(MAP_STYLE) + datasetRegistry.attachCreateDataset(def => new EsriDataset(def)) + map = makeMap() + mapProvider = makeMapProvider(map) + adapter = new EsriLayerAdapter(mapProvider, null, null) + }) + + // ─── createDataset ─────────────────────────────────────────────────────────── + + describe('createDataset', () => { + it('returns an EsriDataset instance', () => { + expect(adapter.createDataset({ id: 'test' })).toBeInstanceOf(EsriDataset) + }) + }) + + // ─── _addLayers ────────────────────────────────────────────────────────────── + + // describe('_addLayers', () => { + // it('stores the VectorTileLayer in _vectorTileLayers', async () => { + // await adapter._addLayers(datasetRegistry.getDataset('esri-standalone'), MAP_STYLE) + // expect(adapter._vectorTileLayers['esri-standalone']).toBeDefined() + // }) + + // it('adds the VectorTileLayer directly to the map when there is no esriGroupId', async () => { + // const ds = datasetRegistry.getDataset('esri-standalone') + // await adapter._addLayers(ds, MAP_STYLE) + // expect(map.add).toHaveBeenCalledWith(adapter._vectorTileLayers['esri-standalone']) + // }) + + // it('stores the VectorTileLayer as the opacity layer when standalone', async () => { + // const ds = datasetRegistry.getDataset('esri-standalone') + // await adapter._addLayers(ds, MAP_STYLE) + // expect(adapter._vectorTileOpacityLayers['esri-standalone']).toBe(adapter._vectorTileLayers['esri-standalone']) + // }) + + // it('adds the VectorTileLayer inside a GroupLayer when esriGroupId is set', async () => { + // const ds = datasetRegistry.getDataset('esri-grouped') + // await adapter._addLayers(ds, MAP_STYLE) + // const gl = adapter._groupLayers['my-group'] + // expect(gl.add).toHaveBeenCalledWith(adapter._vectorTileLayers['esri-grouped']) + // }) + + // it('stores the GroupLayer as the opacity layer when esriGroupId is set', async () => { + // const ds = datasetRegistry.getDataset('esri-grouped') + // await adapter._addLayers(ds, MAP_STYLE) + // expect(adapter._vectorTileOpacityLayers['esri-grouped']).toBe(adapter._groupLayers['my-group']) + // }) + // }) + + // ─── _applyRegistryDatasetVisibility ───────────────────────────────────────── + + describe.skip('_applyRegistryDatasetVisibility', () => { + it('calls setStyleLayerVisibility on the parent VectorTileLayer for a sublayer', async () => { + await adapter._addLayers(datasetRegistry.getDataset('esri-parent'), MAP_STYLE) + const subA = datasetRegistry.getDataset('esri-parent-sub-a') + adapter._applyRegistryDatasetVisibility(subA) + expect(adapter._vectorTileLayers['esri-parent'].setStyleLayerVisibility) + .toHaveBeenCalledWith('style-sub-a', subA.visibility) + }) + + it('sets vectorTileLayer.visible to true for a visible top-level dataset', async () => { + const ds = datasetRegistry.getDataset('esri-standalone') + await adapter._addLayers(ds, MAP_STYLE) + adapter._applyRegistryDatasetVisibility(ds) + expect(adapter._vectorTileLayers['esri-standalone'].visible).toBe(true) + }) + + it('sets vectorTileLayer.visible to false for a hidden top-level dataset', async () => { + const ds = datasetRegistry.getDataset('esri-grouped') + await adapter._addLayers(ds, MAP_STYLE) + adapter._applyRegistryDatasetVisibility(ds) + expect(adapter._vectorTileLayers['esri-grouped'].visible).toBe(false) + }) + + it('applies sublayer style visibility when the top-level dataset is visible', async () => { + await adapter._addLayers(datasetRegistry.getDataset('esri-parent'), MAP_STYLE) + const parent = datasetRegistry.getDataset('esri-parent') + adapter._applyRegistryDatasetVisibility(parent) + const vtl = adapter._vectorTileLayers['esri-parent'] + expect(vtl.setStyleLayerVisibility).toHaveBeenCalledWith('style-sub-a', 'visible') + expect(vtl.setStyleLayerVisibility).toHaveBeenCalledWith('style-sub-b', 'none') + }) + + it('skips sublayer style visibility when the top-level dataset is not visible', async () => { + const ds = datasetRegistry.getDataset('esri-grouped') + await adapter._addLayers(ds, MAP_STYLE) + adapter._applyRegistryDatasetVisibility(ds) + expect(adapter._vectorTileLayers['esri-grouped'].setStyleLayerVisibility).not.toHaveBeenCalled() + }) + }) + + // ─── applyDatasetVisibility ────────────────────────────────────────────────── + + describe('applyDatasetVisibility', () => { + beforeEach(async () => { + const standAlone = datasetRegistry.getDataset('esri-standalone') + await adapter._addLayers(standAlone, MAP_STYLE) + }) + + it('applies visibility for a known dataset', async () => { + await adapter.applyDatasetVisibility('esri-standalone') + expect(adapter._vectorTileLayers['esri-standalone'].visible).toBe(true) + }) + + it('does nothing for an unknown dataset', async () => { + await expect(adapter.applyDatasetVisibility('unknown')).resolves.not.toThrow() + }) + }) + + // ─── applyGlobalVisibility ─────────────────────────────────────────────────── + + describe('applyGlobalVisibility', () => { + it.skip('applies visibility to all known datasets', async () => { + await adapter._addLayers(datasetRegistry.getDataset('esri-standalone'), MAP_STYLE) + await adapter.applyGlobalVisibility() + expect(adapter._vectorTileLayers['esri-standalone'].visible).toBe(true) + }) + }) + + // ─── applyDatasetOpacity ───────────────────────────────────────────────────── + + describe('applyDatasetOpacity', () => { + it('sets opacity on the opacity layer for a known dataset', async () => { + await adapter._addLayers(datasetRegistry.getDataset('esri-standalone'), MAP_STYLE) + await adapter.applyDatasetOpacity('esri-standalone') + expect(adapter._vectorTileOpacityLayers['esri-standalone'].opacity) + .toBe(datasetRegistry.getDataset('esri-standalone').opacity) + }) + + it('does nothing when the dataset has no opacity layer', async () => { + await expect(adapter.applyDatasetOpacity('esri-standalone')).resolves.not.toThrow() + }) + + it('does nothing for an unknown dataset', async () => { + await adapter._addLayers(datasetRegistry.getDataset('esri-standalone'), MAP_STYLE) + await expect(adapter.applyDatasetOpacity('unknown')).resolves.not.toThrow() + }) + }) + + // ─── applyGlobalOpacity ────────────────────────────────────────────────────── + + describe('applyGlobalOpacity', () => { + it('sets opacity on all opacity layers', async () => { + await adapter._addLayers(datasetRegistry.getDataset('esri-standalone'), MAP_STYLE) + await adapter.applyGlobalOpacity() + expect(adapter._vectorTileOpacityLayers['esri-standalone'].opacity) + .toBe(datasetRegistry.getDataset('esri-standalone').opacity) + }) + + it('skips entries whose dataset is not in the registry', async () => { + adapter._vectorTileOpacityLayers['ghost-id'] = { opacity: 99 } + await expect(adapter.applyGlobalOpacity()).resolves.not.toThrow() + expect(adapter._vectorTileOpacityLayers['ghost-id'].opacity).toBe(99) + }) + }) + + // ─── onMapStyleChange ──────────────────────────────────────────────────────── + + describe('onMapStyleChange', () => { + beforeEach(async () => { + await adapter._addLayers(datasetRegistry.getDataset('esri-standalone'), MAP_STYLE) + await adapter._addLayers(datasetRegistry.getDataset('esri-server'), MAP_STYLE) + }) + + it('calls setStyleLayerVisibility for datasets with esriStyleLayerId', async () => { + await adapter.onMapStyleChange(MAP_STYLE, null) + expect(adapter._vectorTileLayers['esri-standalone'].setStyleLayerVisibility) + .toHaveBeenCalledWith('standalone-style', expect.any(String)) + }) + + it('calls setPaintProperties for datasets not using server style', async () => { + await adapter.onMapStyleChange(MAP_STYLE, null) + expect(adapter._vectorTileLayers['esri-standalone'].setPaintProperties) + .toHaveBeenCalledWith('standalone-style', expect.any(Object)) + }) + + it('does not call setPaintProperties when useServerStyle is true', async () => { + await adapter.onMapStyleChange(MAP_STYLE, null) + expect(adapter._vectorTileLayers['esri-server'].setPaintProperties).not.toHaveBeenCalled() + }) + + // it('does not call setPaintProperties when getPaintProperties returns null', async () => { + // const vtl = adapter._vectorTileLayers['esri-standalone'] + // vtl.getPaintProperties.mockReturnValueOnce(null) + // await adapter.onMapStyleChange(MAP_STYLE, null) + // expect(vtl.setPaintProperties).not.toHaveBeenCalled() + // }) + }) + + // ─── init ──────────────────────────────────────────────────────────────────── + + describe('init', () => { + beforeEach(async () => { + await adapter.init() + }) + + it('adds VectorTileLayers for all top-level datasets', async () => { + expect(adapter._vectorTileLayers['flood-zones-cc']).toBeDefined() + expect(adapter._vectorTileLayers['flood-zones']).toBeDefined() + }) + + it('applies dataset visibility after adding layers', async () => { + expect(adapter._groupLayers['flood-zones-group'].visible).toBe(true) + expect(adapter._vectorTileLayers['flood-zones-cc'].visible).toBe(true) + expect(adapter._vectorTileLayers['flood-zones'].visible).toBe(false) + }) + + it('adds GroupLayers for datasets with esriGroupId', async () => { + expect(map.add).toHaveBeenCalledWith(expect.objectContaining({ id: 'flood-zones-group' })) + }) + + it('calls map.add for each top-level dataset', async () => { + expect(map.add.mock.calls.length).toEqual(3) + }) + }) + + // ─── onMapSizeChange ───────────────────────────────────────────────────────── + + describe('onMapSizeChange', () => { + it('resolves without doing anything', async () => { + await expect(adapter.onMapSizeChange()).resolves.toBeUndefined() + }) + }) +}) diff --git a/plugins/beta/datasets/src/initialise/DatasetsInit.jsx b/plugins/beta/datasets/src/initialise/DatasetsInit.jsx index 7f4fb53b6..36964be12 100755 --- a/plugins/beta/datasets/src/initialise/DatasetsInit.jsx +++ b/plugins/beta/datasets/src/initialise/DatasetsInit.jsx @@ -55,7 +55,7 @@ export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, m useEffect(() => { datasetRegistry.attachMapStyle(mapState.mapStyle) if (layerAdapter?.onMapStyleChange) { - layerAdapter.onMapStyleChange(mapState.mapStyle) + layerAdapter.onMapStyleChange() } }, [mapState.mapStyle]) diff --git a/plugins/beta/datasets/src/reducers/__data__/esriDatasets.js b/plugins/beta/datasets/src/reducers/__data__/esriDatasets.js new file mode 100644 index 000000000..56407d4d3 --- /dev/null +++ b/plugins/beta/datasets/src/reducers/__data__/esriDatasets.js @@ -0,0 +1,144 @@ +export const datasets = [ + { + id: 'flood-zones-cc', + label: 'Flood Zones Climate Change', + groupLabel: 'Datasets', + esriGroupId: 'flood-zones-group', + tiles: 'https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_CCP1_NON_PRODUCTION/VectorTileServer', + showInKey: true, + showInMenu: true, + visible: true, + sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea CCP1', + sublayers: [ + { + id: 'climate-change', + label: 'Climate change (2070 to 2125)', + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Flood Zones plus climate change/1', + showInKey: true, + showInMenu: false, + style: { + fill: { outdoor: '#F4A582', dark: '#BF3D4A' }, + stroke: 'none' + } + }, + { + id: 'data-unavailable', + label: 'Climate change data unavailable', + style: { // This is used just for the key - so that it renders the pattern correctly. + fillPattern: 'dot', + fillPatternForegroundColor: { outdoor: '#000000', dark: '#ffffff' }, + stroke: { outdoor: '#000000', dark: '#FFFFFF' } + }, + showInKey: true, + showInMenu: false + }, + { + id: 'data-unavailable-outline', + style: { + stroke: { outdoor: '#000000', dark: '#FFFFFF' } + }, + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/0', + showInKey: false, + showInMenu: false + }, + { + id: 'data-unavailable-light', + visibleWhen: { mapStyleId: ['outdoor', 'black-and-white'] }, + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/1', + esriUseServerStyle: true, + showInKey: false, + showInMenu: false + }, + { + id: 'data-unavailable-dark', + visibleWhen: { mapStyleId: ['dark'] }, + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/2', + esriUseServerStyle: true, + showInKey: false, + showInMenu: false + } + ] + }, + { + id: 'flood-zones', + label: 'Flood Zones', + groupLabel: 'Datasets', + esriGroupId: 'flood-zones-group', + tiles: 'https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_NON_PRODUCTION/VectorTileServer', + showInKey: true, + visible: false, + // showInMenu: true, + sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea', + sublayers: [ + { + id: 'flood-zone-2', + label: 'Flood Zone 2', + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 2/1', + showInMenu: true, + style: { + fill: { outdoor: '#1d70b8', dark: '#7fcdbb' }, + stroke: 'none' + } + }, + { + id: 'flood-zone-3', + label: 'Flood Zone 3', + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 3/1', + showInMenu: true, + style: { + fill: { outdoor: '#003078', dark: '#e5f5e0' }, + stroke: 'none' + } + } + ] + }, + { + id: 'esri-standalone', + label: 'Standalone Layer', + tiles: 'https://example.com/vtl/standalone', + visible: true, + esriStyleLayerId: 'standalone-style', + style: { fill: '#ff0000', stroke: '#000000' } + }, + // Grouped: belongs to a GroupLayer, not visible (covers the visible=false branch) + { + id: 'esri-grouped', + label: 'Grouped Layer', + tiles: 'https://example.com/vtl/grouped', + visible: false, + esriGroupId: 'my-group' + }, + // Server style: setPaintProperties should be skipped + { + id: 'esri-server', + label: 'Server Style Layer', + tiles: 'https://example.com/vtl/server', + visible: true, + esriStyleLayerId: 'server-style-layer', + esriUseServerStyle: true, + style: {} + }, + // Parent with sublayers that have esriStyleLayerId + { + id: 'esri-parent', + label: 'Parent With Sublayers', + tiles: 'https://example.com/vtl/parent', + visible: true, + sublayers: [ + { + id: 'sub-a', + label: 'Sub A', + esriStyleLayerId: 'style-sub-a', + visible: true, + style: { fill: '#00ff00' } + }, + { + id: 'sub-b', + label: 'Sub B', + esriStyleLayerId: 'style-sub-b', + visible: false, + style: {} + } + ] + } +] diff --git a/plugins/beta/datasets/src/registry/__mocks__/datasetRegistry.js b/plugins/beta/datasets/src/registry/__mocks__/datasetRegistry.js index fbb2717bc..02eb2e5b8 100644 --- a/plugins/beta/datasets/src/registry/__mocks__/datasetRegistry.js +++ b/plugins/beta/datasets/src/registry/__mocks__/datasetRegistry.js @@ -1,5 +1,6 @@ import { mappedDatasetsReducer } from '../../reducers/mappedDatasetsReducer.js' import { datasets as datasetDefinitions } from '../../reducers/__data__/demoDatasets.js' +import { datasets as esriDatasetDefinitions } from '../../reducers/__data__/esriDatasets.js' import { attachGlobalState } from '../globalDataset.js' const { datasetRegistry } = jest.requireActual('../datasetRegistry.js') const { mappedDatasets, orderedDatasets } = mappedDatasetsReducer({ datasets: datasetDefinitions }) @@ -26,4 +27,12 @@ datasetRegistry.mockExtend = (extraDatasets) => datasetRegistry.attach( [...orderedDatasets, ...Object.keys(extraDatasets)] ) +datasetRegistry.useEsriDatasets = (extraDatasets = {}) => { + const { mappedDatasets, orderedDatasets } = mappedDatasetsReducer({ datasets: esriDatasetDefinitions }) + datasetRegistry.attach( + { ...mappedDatasets, ...extraDatasets }, + [...orderedDatasets, ...Object.keys(extraDatasets)] + ) +} + export { datasetRegistry } diff --git a/plugins/beta/datasets/src/registry/dataset.js b/plugins/beta/datasets/src/registry/dataset.js index dce6f78fe..a8bd74235 100644 --- a/plugins/beta/datasets/src/registry/dataset.js +++ b/plugins/beta/datasets/src/registry/dataset.js @@ -137,7 +137,7 @@ export class Dataset { if (sublayerIds) { return sublayerIds.map(id => datasetRegistry.getDataset(id)) } - return undefined + return [] } get parent () { From b90837148f21b5ea7c2cf02aad86d717e6dab878 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Wed, 8 Jul 2026 12:29:58 +0100 Subject: [PATCH 26/53] IM-373 fixed a couple of tests --- .../beta/datasets/src/adapters/esri/esriLayerAdapter.test.js | 2 +- plugins/beta/datasets/src/registry/dataset.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js index 8b40e4902..884260021 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js @@ -252,7 +252,7 @@ describe('esriLayerAdapter', () => { }) it('calls map.add for each top-level dataset', async () => { - expect(map.add.mock.calls.length).toEqual(3) + expect(map.add.mock.calls.length).toEqual(5) // 3 top-level datasets + 2 group layers }) }) diff --git a/plugins/beta/datasets/src/registry/dataset.test.js b/plugins/beta/datasets/src/registry/dataset.test.js index d88fb69dd..c7bf00c14 100644 --- a/plugins/beta/datasets/src/registry/dataset.test.js +++ b/plugins/beta/datasets/src/registry/dataset.test.js @@ -41,9 +41,9 @@ describe('Dataset class', () => { }) describe('sublayers', () => { - it('returns undefined for a dataset with no sublayerIds', () => { + it('returns an empty array for a dataset with no sublayerIds', () => { const dataset = datasetRegistry.getDataset('hedge-control') - expect(dataset.sublayers).toBeUndefined() + expect(dataset.sublayers).toEqual([]) }) it('returns a Dataset instance for each sublayer', () => { From 5f750928c6b24009fd6b247d50ec4a153cc8565f Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Wed, 8 Jul 2026 16:55:19 +0100 Subject: [PATCH 27/53] IM-373 removed MapLibreDataset from maplibreLayerAdapter.test.js --- .../datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js index 7470c28fa..66ad72fd5 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +++ b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js @@ -1,5 +1,4 @@ import MaplibreLayerAdapter from './maplibreLayerAdapter.js' -import { MapLibreDataset } from './registry/mapLibreDataset.js' import { datasetRegistry } from '../../registry/datasetRegistry.js' import { symbolRegistry } from '../../../../../../src/services/symbolRegistry.js' import { patternRegistry } from '../../../../../../src/services/patternRegistry.js' From 11dc03ad684539bdddb321410f001c12ef6ceb00 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Wed, 8 Jul 2026 17:09:28 +0100 Subject: [PATCH 28/53] IM-373 removed commented tests --- .../adapters/esri/esriLayerAdapter.test.js | 86 ------------------- 1 file changed, 86 deletions(-) diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js index 884260021..bb43d16c3 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js @@ -55,82 +55,6 @@ describe('esriLayerAdapter', () => { }) }) - // ─── _addLayers ────────────────────────────────────────────────────────────── - - // describe('_addLayers', () => { - // it('stores the VectorTileLayer in _vectorTileLayers', async () => { - // await adapter._addLayers(datasetRegistry.getDataset('esri-standalone'), MAP_STYLE) - // expect(adapter._vectorTileLayers['esri-standalone']).toBeDefined() - // }) - - // it('adds the VectorTileLayer directly to the map when there is no esriGroupId', async () => { - // const ds = datasetRegistry.getDataset('esri-standalone') - // await adapter._addLayers(ds, MAP_STYLE) - // expect(map.add).toHaveBeenCalledWith(adapter._vectorTileLayers['esri-standalone']) - // }) - - // it('stores the VectorTileLayer as the opacity layer when standalone', async () => { - // const ds = datasetRegistry.getDataset('esri-standalone') - // await adapter._addLayers(ds, MAP_STYLE) - // expect(adapter._vectorTileOpacityLayers['esri-standalone']).toBe(adapter._vectorTileLayers['esri-standalone']) - // }) - - // it('adds the VectorTileLayer inside a GroupLayer when esriGroupId is set', async () => { - // const ds = datasetRegistry.getDataset('esri-grouped') - // await adapter._addLayers(ds, MAP_STYLE) - // const gl = adapter._groupLayers['my-group'] - // expect(gl.add).toHaveBeenCalledWith(adapter._vectorTileLayers['esri-grouped']) - // }) - - // it('stores the GroupLayer as the opacity layer when esriGroupId is set', async () => { - // const ds = datasetRegistry.getDataset('esri-grouped') - // await adapter._addLayers(ds, MAP_STYLE) - // expect(adapter._vectorTileOpacityLayers['esri-grouped']).toBe(adapter._groupLayers['my-group']) - // }) - // }) - - // ─── _applyRegistryDatasetVisibility ───────────────────────────────────────── - - describe.skip('_applyRegistryDatasetVisibility', () => { - it('calls setStyleLayerVisibility on the parent VectorTileLayer for a sublayer', async () => { - await adapter._addLayers(datasetRegistry.getDataset('esri-parent'), MAP_STYLE) - const subA = datasetRegistry.getDataset('esri-parent-sub-a') - adapter._applyRegistryDatasetVisibility(subA) - expect(adapter._vectorTileLayers['esri-parent'].setStyleLayerVisibility) - .toHaveBeenCalledWith('style-sub-a', subA.visibility) - }) - - it('sets vectorTileLayer.visible to true for a visible top-level dataset', async () => { - const ds = datasetRegistry.getDataset('esri-standalone') - await adapter._addLayers(ds, MAP_STYLE) - adapter._applyRegistryDatasetVisibility(ds) - expect(adapter._vectorTileLayers['esri-standalone'].visible).toBe(true) - }) - - it('sets vectorTileLayer.visible to false for a hidden top-level dataset', async () => { - const ds = datasetRegistry.getDataset('esri-grouped') - await adapter._addLayers(ds, MAP_STYLE) - adapter._applyRegistryDatasetVisibility(ds) - expect(adapter._vectorTileLayers['esri-grouped'].visible).toBe(false) - }) - - it('applies sublayer style visibility when the top-level dataset is visible', async () => { - await adapter._addLayers(datasetRegistry.getDataset('esri-parent'), MAP_STYLE) - const parent = datasetRegistry.getDataset('esri-parent') - adapter._applyRegistryDatasetVisibility(parent) - const vtl = adapter._vectorTileLayers['esri-parent'] - expect(vtl.setStyleLayerVisibility).toHaveBeenCalledWith('style-sub-a', 'visible') - expect(vtl.setStyleLayerVisibility).toHaveBeenCalledWith('style-sub-b', 'none') - }) - - it('skips sublayer style visibility when the top-level dataset is not visible', async () => { - const ds = datasetRegistry.getDataset('esri-grouped') - await adapter._addLayers(ds, MAP_STYLE) - adapter._applyRegistryDatasetVisibility(ds) - expect(adapter._vectorTileLayers['esri-grouped'].setStyleLayerVisibility).not.toHaveBeenCalled() - }) - }) - // ─── applyDatasetVisibility ────────────────────────────────────────────────── describe('applyDatasetVisibility', () => { @@ -149,16 +73,6 @@ describe('esriLayerAdapter', () => { }) }) - // ─── applyGlobalVisibility ─────────────────────────────────────────────────── - - describe('applyGlobalVisibility', () => { - it.skip('applies visibility to all known datasets', async () => { - await adapter._addLayers(datasetRegistry.getDataset('esri-standalone'), MAP_STYLE) - await adapter.applyGlobalVisibility() - expect(adapter._vectorTileLayers['esri-standalone'].visible).toBe(true) - }) - }) - // ─── applyDatasetOpacity ───────────────────────────────────────────────────── describe('applyDatasetOpacity', () => { From acd0f4c76a5b783ed4d5a28ec602726abcd3d0a2 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Thu, 9 Jul 2026 09:28:59 +0100 Subject: [PATCH 29/53] IM-394 added a remove dataset api --- demo/js/esri-datasets.js | 2 ++ .../src/adapters/esri/esriLayerAdapter.js | 26 +++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 670728c71..1c7a3fd53 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -146,4 +146,6 @@ const testGlobalVisibility = () => { interactiveMap.on('datasets:ready', function () { // testGlobalVisibility() + setTimeout(() => datasetsPlugin.removeDataset('flood-zones-cc'), 1000) + setTimeout(() => datasetsPlugin.removeDataset('flood-zones'), 3000) }) \ No newline at end of file diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index f9303757e..58e4ee2c5 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -76,8 +76,30 @@ export default class EsriLayerAdapter extends LayerAdapter { return vectorTileLayer.when() } - async removeDataset (...args) { - console.log('TODO: removeDataset', args) + async removeDataset (datasetId) { + const registryDataset = datasetRegistry.getDataset(datasetId) + if (!registryDataset) { + return + } + const { esriGroupId } = registryDataset + const vectorTileLayer = this._vectorTileLayers[datasetId] + // If the dataset is part of a group layer, we need to remove it from the group layer + const groupLayer = esriGroupId ? this._groupLayers[esriGroupId] : null + const vectorTileParent = groupLayer || this._map + + if (vectorTileLayer) { + // Remove the vectorTileLayer from the map or group layer + vectorTileParent.remove(vectorTileLayer) + // And remove the vectorTileLayer from the adapter's internal state + delete this._vectorTileLayers[datasetId] + delete this._vectorTileOpacityLayers[datasetId] + } + + // If the group layer has no more sublayers, we need to also remove the group layer from the map + if (groupLayer && groupLayer.layers.length === 0) { + this._map.remove(groupLayer) + delete this._groupLayers[esriGroupId] + } } async setData (...args) { From c65f3b8dc848f8e0943e00413d4906293bd60531 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Thu, 9 Jul 2026 09:52:17 +0100 Subject: [PATCH 30/53] IM-394 added tests to cover removeDataset --- .../adapters/esri/esriLayerAdapter.test.js | 69 +++++++++++++++++-- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js index bb43d16c3..ac186f250 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js @@ -14,13 +14,21 @@ jest.mock('@arcgis/core/layers/VectorTileLayer.js', () => })) ) jest.mock('@arcgis/core/layers/GroupLayer.js', () => - jest.fn().mockImplementation((opts = {}) => ({ - ...opts, - add: jest.fn() - })) + jest.fn().mockImplementation((opts = {}) => { + const layers = [] + return { + ...opts, + layers, + add: jest.fn(layer => layers.push(layer)), + remove: jest.fn(layer => { + const idx = layers.indexOf(layer) + if (idx !== -1) layers.splice(idx, 1) + }) + } + }) ) -// Uncovered: 80-88,124,145-149 +// Uncovered: 124,145-149 const MAP_STYLE = { id: 'outdoor' } const makeMap = () => { @@ -30,7 +38,8 @@ const makeMap = () => { // Ensure that the same layer is never added twice expect(_added[id]).toBeUndefined() _added[id] = true - }) + }), + remove: jest.fn() } } const makeMapProvider = (map) => ({ map }) @@ -55,6 +64,54 @@ describe('esriLayerAdapter', () => { }) }) + // ─── removeDataset ──────────────────────────────────────────────────────────── + + describe('removeDataset', () => { + it('does nothing when dataset is not in the registry', async () => { + await expect(adapter.removeDataset('unknown')).resolves.toBeUndefined() + expect(map.remove).not.toHaveBeenCalled() + }) + + it('does nothing when the dataset has not been added to the adapter', async () => { + await adapter.removeDataset('esri-standalone') + expect(map.remove).not.toHaveBeenCalled() + }) + + it('removes a standalone vectorTileLayer from the map and clears internal state', async () => { + await adapter._addLayers(datasetRegistry.getDataset('esri-standalone')) + const vtl = adapter._vectorTileLayers['esri-standalone'] + await adapter.removeDataset('esri-standalone') + expect(map.remove).toHaveBeenCalledWith(vtl) + expect(adapter._vectorTileLayers['esri-standalone']).toBeUndefined() + expect(adapter._vectorTileOpacityLayers['esri-standalone']).toBeUndefined() + }) + + it('removes the vectorTileLayer from its group layer but keeps the group when other layers remain', async () => { + await adapter._addLayers(datasetRegistry.getDataset('flood-zones-cc')) + await adapter._addLayers(datasetRegistry.getDataset('flood-zones')) + const vtl = adapter._vectorTileLayers['flood-zones-cc'] + const groupLayer = adapter._groupLayers['flood-zones-group'] + await adapter.removeDataset('flood-zones-cc') + expect(groupLayer.remove).toHaveBeenCalledWith(vtl) + expect(map.remove).not.toHaveBeenCalledWith(groupLayer) + expect(adapter._groupLayers['flood-zones-group']).toBeDefined() + expect(adapter._vectorTileLayers['flood-zones-cc']).toBeUndefined() + expect(adapter._vectorTileOpacityLayers['flood-zones-cc']).toBeUndefined() + }) + + it('removes the group layer from the map when its last vectorTileLayer is removed', async () => { + await adapter._addLayers(datasetRegistry.getDataset('esri-grouped')) + const vtl = adapter._vectorTileLayers['esri-grouped'] + const groupLayer = adapter._groupLayers['my-group'] + await adapter.removeDataset('esri-grouped') + expect(groupLayer.remove).toHaveBeenCalledWith(vtl) + expect(map.remove).toHaveBeenCalledWith(groupLayer) + expect(adapter._groupLayers['my-group']).toBeUndefined() + expect(adapter._vectorTileLayers['esri-grouped']).toBeUndefined() + expect(adapter._vectorTileOpacityLayers['esri-grouped']).toBeUndefined() + }) + }) + // ─── applyDatasetVisibility ────────────────────────────────────────────────── describe('applyDatasetVisibility', () => { From 19d480a08f496c46fa8fb493b6e2b92443f110c5 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Thu, 9 Jul 2026 10:37:14 +0100 Subject: [PATCH 31/53] IM-394 removed un-needed params in esriLayerAdapter.test.js --- .../adapters/esri/esriLayerAdapter.test.js | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js index ac186f250..0c711b18a 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js @@ -117,7 +117,9 @@ describe('esriLayerAdapter', () => { describe('applyDatasetVisibility', () => { beforeEach(async () => { const standAlone = datasetRegistry.getDataset('esri-standalone') - await adapter._addLayers(standAlone, MAP_STYLE) + await adapter._addLayers(standAlone) + const fz3 = datasetRegistry.getDataset('flood-zones-flood-zone-3') + await adapter._addLayers(fz3) }) it('applies visibility for a known dataset', async () => { @@ -134,7 +136,7 @@ describe('esriLayerAdapter', () => { describe('applyDatasetOpacity', () => { it('sets opacity on the opacity layer for a known dataset', async () => { - await adapter._addLayers(datasetRegistry.getDataset('esri-standalone'), MAP_STYLE) + await adapter._addLayers(datasetRegistry.getDataset('esri-standalone')) await adapter.applyDatasetOpacity('esri-standalone') expect(adapter._vectorTileOpacityLayers['esri-standalone'].opacity) .toBe(datasetRegistry.getDataset('esri-standalone').opacity) @@ -145,7 +147,7 @@ describe('esriLayerAdapter', () => { }) it('does nothing for an unknown dataset', async () => { - await adapter._addLayers(datasetRegistry.getDataset('esri-standalone'), MAP_STYLE) + await adapter._addLayers(datasetRegistry.getDataset('esri-standalone')) await expect(adapter.applyDatasetOpacity('unknown')).resolves.not.toThrow() }) }) @@ -154,7 +156,7 @@ describe('esriLayerAdapter', () => { describe('applyGlobalOpacity', () => { it('sets opacity on all opacity layers', async () => { - await adapter._addLayers(datasetRegistry.getDataset('esri-standalone'), MAP_STYLE) + await adapter._addLayers(datasetRegistry.getDataset('esri-standalone')) await adapter.applyGlobalOpacity() expect(adapter._vectorTileOpacityLayers['esri-standalone'].opacity) .toBe(datasetRegistry.getDataset('esri-standalone').opacity) @@ -171,33 +173,26 @@ describe('esriLayerAdapter', () => { describe('onMapStyleChange', () => { beforeEach(async () => { - await adapter._addLayers(datasetRegistry.getDataset('esri-standalone'), MAP_STYLE) - await adapter._addLayers(datasetRegistry.getDataset('esri-server'), MAP_STYLE) + await adapter._addLayers(datasetRegistry.getDataset('esri-standalone')) + await adapter._addLayers(datasetRegistry.getDataset('esri-server')) }) it('calls setStyleLayerVisibility for datasets with esriStyleLayerId', async () => { - await adapter.onMapStyleChange(MAP_STYLE, null) + await adapter.onMapStyleChange() expect(adapter._vectorTileLayers['esri-standalone'].setStyleLayerVisibility) .toHaveBeenCalledWith('standalone-style', expect.any(String)) }) it('calls setPaintProperties for datasets not using server style', async () => { - await adapter.onMapStyleChange(MAP_STYLE, null) + await adapter.onMapStyleChange() expect(adapter._vectorTileLayers['esri-standalone'].setPaintProperties) .toHaveBeenCalledWith('standalone-style', expect.any(Object)) }) it('does not call setPaintProperties when useServerStyle is true', async () => { - await adapter.onMapStyleChange(MAP_STYLE, null) + await adapter.onMapStyleChange() expect(adapter._vectorTileLayers['esri-server'].setPaintProperties).not.toHaveBeenCalled() }) - - // it('does not call setPaintProperties when getPaintProperties returns null', async () => { - // const vtl = adapter._vectorTileLayers['esri-standalone'] - // vtl.getPaintProperties.mockReturnValueOnce(null) - // await adapter.onMapStyleChange(MAP_STYLE, null) - // expect(vtl.setPaintProperties).not.toHaveBeenCalled() - // }) }) // ─── init ──────────────────────────────────────────────────────────────────── From cbfd63e5b0b79c247e68f4c0870150ee3bee27c9 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Thu, 9 Jul 2026 10:46:58 +0100 Subject: [PATCH 32/53] IM-394 covered missing line in esriLayerAdapter --- .../src/adapters/esri/esriLayerAdapter.test.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js index 0c711b18a..46bc287b4 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js @@ -118,8 +118,6 @@ describe('esriLayerAdapter', () => { beforeEach(async () => { const standAlone = datasetRegistry.getDataset('esri-standalone') await adapter._addLayers(standAlone) - const fz3 = datasetRegistry.getDataset('flood-zones-flood-zone-3') - await adapter._addLayers(fz3) }) it('applies visibility for a known dataset', async () => { @@ -130,6 +128,13 @@ describe('esriLayerAdapter', () => { it('does nothing for an unknown dataset', async () => { await expect(adapter.applyDatasetVisibility('unknown')).resolves.not.toThrow() }) + + it('calls setStyleLayerVisibility for datasets with esriStyleLayerId', async () => { + const applyStyleLayerVisibilitySpy = jest.spyOn(adapter, '_applyStyleLayerVisibility') + await adapter._addLayers(datasetRegistry.getDataset('flood-zones')) + await adapter.applyDatasetVisibility('flood-zones-flood-zone-3') + expect(applyStyleLayerVisibilitySpy.mock.calls).toHaveLength(1) + }) }) // ─── applyDatasetOpacity ───────────────────────────────────────────────────── @@ -218,7 +223,7 @@ describe('esriLayerAdapter', () => { }) it('calls map.add for each top-level dataset', async () => { - expect(map.add.mock.calls.length).toEqual(5) // 3 top-level datasets + 2 group layers + expect(map.add.mock.calls).toHaveLength(5) // 3 top-level datasets + 2 group layers }) }) From 2029c6c51407407f92c9d2f13095474c1a1cfad7 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Thu, 9 Jul 2026 12:48:46 +0100 Subject: [PATCH 33/53] IM-394 added an addDataset method --- demo/js/esri-datasets.js | 12 ++- .../src/adapters/esri/esriLayerAdapter.js | 81 +++++++++++-------- .../adapters/esri/esriLayerAdapter.test.js | 12 +++ 3 files changed, 66 insertions(+), 39 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 1c7a3fd53..c3c902027 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -7,8 +7,7 @@ import createDatasetsPlugin from '/plugins/beta/datasets/src/index.js' import { vtsMapStyles27700 } from './mapStyles.js' import { transformGeocodeRequest, transformVtsRequest3857, setupEsriConfig } from './auth.js' -const datasets = [ - { +const datasetFloodZonesCC = { id: 'flood-zones-cc', label: 'Flood Zones Climate Change', groupLabel: 'Datasets', @@ -67,8 +66,9 @@ const datasets = [ showInMenu: false, } ] - }, - { + } + + const datasetFloodZones = { id: 'flood-zones', label: 'Flood Zones', groupLabel: 'Datasets', @@ -100,6 +100,9 @@ const datasets = [ } ] } + +const datasets = [ + datasetFloodZonesCC, datasetFloodZones ] const datasetsPlugin = createDatasetsPlugin({ @@ -148,4 +151,5 @@ interactiveMap.on('datasets:ready', function () { // testGlobalVisibility() setTimeout(() => datasetsPlugin.removeDataset('flood-zones-cc'), 1000) setTimeout(() => datasetsPlugin.removeDataset('flood-zones'), 3000) + setTimeout(() => datasetsPlugin.addDataset(datasetFloodZones), 5000) }) \ No newline at end of file diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index 58e4ee2c5..0f9e6738c 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -76,6 +76,20 @@ export default class EsriLayerAdapter extends LayerAdapter { return vectorTileLayer.when() } + async addDataset (datasetId) { + const registryDataset = datasetRegistry.getDataset(datasetId) + if (!registryDataset) { + console.warn(`addDataset called, but Dataset with id ${datasetId} not found in registry`) + return + } + await this._addLayers(registryDataset) + const { parentId } = registryDataset + const vectorTileLayer = this._vectorTileLayers[parentId || datasetId] + this.applyDatasetOpacity(datasetId) + this._applyStyleLayerPaintProperties(registryDataset, vectorTileLayer) + this.applyDatasetVisibility(datasetId) + } + async removeDataset (datasetId) { const registryDataset = datasetRegistry.getDataset(datasetId) if (!registryDataset) { @@ -102,22 +116,6 @@ export default class EsriLayerAdapter extends LayerAdapter { } } - async setData (...args) { - console.log('TODO: setData', args) - } - - async applyStyle (...args) { - console.log('TODO: applyStyle', args) - } - - _applyStyleLayerVisibility (sublayer, vectorTileLayer) { - const { esriStyleLayerId } = sublayer - if (!esriStyleLayerId) { - return - } - vectorTileLayer.setStyleLayerVisibility(esriStyleLayerId, sublayer.visibility) - } - _applyRegistryDatasetVisibility (registryDataset) { // if this is a sublayer, we need to apply the visibility to the vectorTileLayers style sheet // if this is a top level dataset, we need to apply the visibility to the vectorTileLayer/ groupLayer itself @@ -163,36 +161,49 @@ export default class EsriLayerAdapter extends LayerAdapter { }) } - async addDataset (...args) { - console.log('TODO: addDataset', args) + _applyStyleLayerVisibility (registryDataset, vectorTileLayer) { + const { esriStyleLayerId } = registryDataset + if (!esriStyleLayerId || !vectorTileLayer) { + return + } + vectorTileLayer.setStyleLayerVisibility(esriStyleLayerId, registryDataset.visibility) } - async applyFeatureFilter (...args) { - console.log('TODO: applyFeatureFilter', args) + _applyStyleLayerPaintProperties (registryDataset, vectorTileLayer) { + const { esriStyleLayerId, useServerStyle } = registryDataset + if (useServerStyle || !esriStyleLayerId || !vectorTileLayer) { + return + } + const layerPaintProperties = vectorTileLayer.getPaintProperties(esriStyleLayerId) + if (layerPaintProperties) { + registryDataset.applyLayerPaintProperties(layerPaintProperties) + vectorTileLayer.setPaintProperties(esriStyleLayerId, registryDataset.applyLayerPaintProperties(layerPaintProperties)) + } } async onMapStyleChange () { datasetRegistry.forEach(registryDataset => { - const { id, isSublayer, esriStyleLayerId, parent } = registryDataset + const { id, isSublayer, parent } = registryDataset const vectorTileLayer = this._vectorTileLayers[isSublayer ? parent.id : id] - if (vectorTileLayer && esriStyleLayerId) { - // Show hide the style layer based on the dataset's mapStyle visibility - vectorTileLayer.setStyleLayerVisibility(esriStyleLayerId, registryDataset.visibility) - if (registryDataset.useServerStyle) { - // If the dataset is using the server style, we don't need to apply any paint properties - return - } - // Update the paint properties of the style layer based on the dataset's mapStyle style - const layerPaintProperties = vectorTileLayer.getPaintProperties(esriStyleLayerId) - if (layerPaintProperties) { - registryDataset.applyLayerPaintProperties(layerPaintProperties) - vectorTileLayer.setPaintProperties(esriStyleLayerId, registryDataset.applyLayerPaintProperties(layerPaintProperties)) - } - } + this._applyStyleLayerVisibility(registryDataset, vectorTileLayer) + this._applyStyleLayerPaintProperties(registryDataset, vectorTileLayer) }) // TODO - handle dynamic sources } // onMapSizeChange is not applicable to the esriLayerAdapter async onMapSizeChange () {} + + // Remaining methods are still todo + async applyFeatureFilter (...args) { + console.log('TODO: applyFeatureFilter', args) + } + + async setData (...args) { + console.log('TODO: setData', args) + } + + async applyStyle (...args) { + console.log('TODO: applyStyle', args) + } } diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js index 46bc287b4..d27c25e55 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js @@ -234,4 +234,16 @@ describe('esriLayerAdapter', () => { await expect(adapter.onMapSizeChange()).resolves.toBeUndefined() }) }) + + describe('applyGlobalVisibility', () => { + beforeEach(async () => { + await adapter.init() + }) + + it('applies visibility for all datasets', async () => { + const applyStyleLayerVisibilitySpy = jest.spyOn(adapter, '_applyStyleLayerVisibility') + await adapter.applyGlobalVisibility() + expect(applyStyleLayerVisibilitySpy.mock.calls).toHaveLength(7) + }) + }) }) From f87db00d7a1cfb1aadf2c05a262fce594f151f9c Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Thu, 9 Jul 2026 13:04:46 +0100 Subject: [PATCH 34/53] IM-394 added tests for addDataset --- .../src/adapters/esri/esriLayerAdapter.js | 3 +- .../adapters/esri/esriLayerAdapter.test.js | 43 +++++++++++++++++++ src/services/__mocks/logger.js | 4 ++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/services/__mocks/logger.js diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js index 0f9e6738c..781b214d5 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -3,6 +3,7 @@ import GroupLayer from '@arcgis/core/layers/GroupLayer.js' import { LayerAdapter } from '../layerAdapter.js' import { datasetRegistry } from '../../registry/datasetRegistry.js' import { EsriDataset } from './registry/esriDataset.js' +import { logger } from '../../../../../../src/services/logger.js' export default class EsriLayerAdapter extends LayerAdapter { constructor (mapProvider, symbolRegistry, patternRegistry) { @@ -79,7 +80,7 @@ export default class EsriLayerAdapter extends LayerAdapter { async addDataset (datasetId) { const registryDataset = datasetRegistry.getDataset(datasetId) if (!registryDataset) { - console.warn(`addDataset called, but Dataset with id ${datasetId} not found in registry`) + logger.warn(`addDataset called, but Dataset with id ${datasetId} not found in registry`) return } await this._addLayers(registryDataset) diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js index d27c25e55..10bb557d9 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js @@ -3,6 +3,8 @@ import EsriLayerAdapter from './esriLayerAdapter.js' import { datasetRegistry } from '../../registry/datasetRegistry.js' jest.mock('../../registry/datasetRegistry.js') +jest.mock('../../../../../../src/services/logger.js') + jest.mock('@arcgis/core/layers/VectorTileLayer.js', () => jest.fn().mockImplementation((opts = {}) => ({ ...opts, @@ -64,6 +66,47 @@ describe('esriLayerAdapter', () => { }) }) + // ─── addDataset ────────────────────────────────────────────────────────────── + + describe('addDataset', () => { + it('copes and returns when the dataset is not in the registry', async () => { + await adapter.addDataset('unknown') + expect(adapter._vectorTileLayers.unknown).toBeUndefined() + }) + + it('adds a standalone dataset to the map and populates internal state', async () => { + await adapter.addDataset('esri-standalone') + expect(adapter._vectorTileLayers['esri-standalone']).toBeDefined() + expect(adapter._vectorTileOpacityLayers['esri-standalone']).toBeDefined() + }) + + it('applies opacity and visibility after adding layers', async () => { + await adapter.addDataset('esri-standalone') + const vtl = adapter._vectorTileLayers['esri-standalone'] + expect(vtl.visible).toBe(true) + expect(adapter._vectorTileOpacityLayers['esri-standalone'].opacity) + .toBe(datasetRegistry.getDataset('esri-standalone').opacity) + }) + + it('applies paint properties for datasets with esriStyleLayerId', async () => { + await adapter.addDataset('esri-standalone') + const vtl = adapter._vectorTileLayers['esri-standalone'] + expect(vtl.setPaintProperties).toHaveBeenCalledWith('standalone-style', expect.any(Object)) + }) + + it('does not apply paint properties for server-style datasets', async () => { + await adapter.addDataset('esri-server') + const vtl = adapter._vectorTileLayers['esri-server'] + expect(vtl.setPaintProperties).not.toHaveBeenCalled() + }) + + it('creates a group layer when adding a grouped dataset', async () => { + await adapter.addDataset('esri-grouped') + expect(adapter._vectorTileLayers['esri-grouped']).toBeDefined() + expect(adapter._groupLayers['my-group']).toBeDefined() + }) + }) + // ─── removeDataset ──────────────────────────────────────────────────────────── describe('removeDataset', () => { diff --git a/src/services/__mocks/logger.js b/src/services/__mocks/logger.js new file mode 100644 index 000000000..ddf96c0c4 --- /dev/null +++ b/src/services/__mocks/logger.js @@ -0,0 +1,4 @@ +export const logger = { + warn: jest.fn(), + error: jest.fn() +} From 7c9cc0d63cf7191ef37ffd24ecc60c44776e901a Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Thu, 9 Jul 2026 13:09:07 +0100 Subject: [PATCH 35/53] IM-394 added __mocks__ for logger --- jest.config.mjs | 1 + .../src/adapters/maplibre/registry/mapLibreDataset.test.js | 4 +--- plugins/beta/datasets/src/api/getOpacity.test.js | 4 +--- plugins/beta/datasets/src/api/getStyle.test.js | 4 +--- plugins/beta/datasets/src/api/setData.test.js | 4 +--- plugins/beta/datasets/src/api/setFeatureVisibility.test.js | 4 +--- plugins/beta/datasets/src/api/setGlobals.test.js | 4 +--- plugins/beta/datasets/src/reducers/pluginState.test.js | 4 +--- src/App/controls/keyboardActions.test.js | 4 +--- src/App/renderer/mapButtons.test.js | 2 +- src/InteractiveMap/deviceChecker.test.js | 2 +- src/services/{__mocks => __mocks__}/logger.js | 0 12 files changed, 11 insertions(+), 26 deletions(-) rename src/services/{__mocks => __mocks__}/logger.js (100%) diff --git a/jest.config.mjs b/jest.config.mjs index a33d594ec..491ac260a 100755 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -21,6 +21,7 @@ export default { ], testPathIgnorePatterns: ['/src/test-utils.js'], coveragePathIgnorePatterns: [ + '/__mocks__/', '/src/index.umd.js', '/stylelint.config.js', '/coverage', diff --git a/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js b/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js index 5d6adb4a4..dd8c28f33 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js +++ b/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js @@ -4,9 +4,7 @@ import { logger } from '../../../../../../../src/services/logger.js' // Use the mock datasetRegistry with the demo datasets attached before each test // so we can test Dataset methods that depend on parent/sublayer relationships and styles jest.mock('../../../registry/datasetRegistry.js') -jest.mock('../../../../../../../src/services/logger.js', () => ({ - logger: { warn: jest.fn() } -})) +jest.mock('../../../../../../../src/services/logger.js') describe('MapLibreDataset', () => { beforeEach(() => { diff --git a/plugins/beta/datasets/src/api/getOpacity.test.js b/plugins/beta/datasets/src/api/getOpacity.test.js index a1e943e0b..772bce0f3 100644 --- a/plugins/beta/datasets/src/api/getOpacity.test.js +++ b/plugins/beta/datasets/src/api/getOpacity.test.js @@ -6,9 +6,7 @@ jest.mock('../registry/datasetRegistry.js', () => ({ datasetRegistry: { getDataset: jest.fn() } })) -jest.mock('../../../../../src/services/logger.js', () => ({ - logger: { warn: jest.fn() } -})) +jest.mock('../../../../../src/services/logger.js') describe('getOpacity', () => { beforeEach(() => { diff --git a/plugins/beta/datasets/src/api/getStyle.test.js b/plugins/beta/datasets/src/api/getStyle.test.js index fc1b62327..21702a531 100644 --- a/plugins/beta/datasets/src/api/getStyle.test.js +++ b/plugins/beta/datasets/src/api/getStyle.test.js @@ -6,9 +6,7 @@ jest.mock('../registry/datasetRegistry.js', () => ({ datasetRegistry: { getDataset: jest.fn() } })) -jest.mock('../../../../../src/services/logger.js', () => ({ - logger: { warn: jest.fn() } -})) +jest.mock('../../../../../src/services/logger.js') describe('getStyle', () => { beforeEach(() => { diff --git a/plugins/beta/datasets/src/api/setData.test.js b/plugins/beta/datasets/src/api/setData.test.js index 80b181428..7805bc0a7 100644 --- a/plugins/beta/datasets/src/api/setData.test.js +++ b/plugins/beta/datasets/src/api/setData.test.js @@ -13,9 +13,7 @@ jest.mock('../registry/datasetRegistry.js', () => ({ datasetRegistry: { getDataset: jest.fn() } })) -jest.mock('../../../../../src/services/logger.js', () => ({ - logger: { warn: jest.fn() } -})) +jest.mock('../../../../../src/services/logger.js') describe('setData', () => { const geojson = { type: 'FeatureCollection', features: [] } diff --git a/plugins/beta/datasets/src/api/setFeatureVisibility.test.js b/plugins/beta/datasets/src/api/setFeatureVisibility.test.js index 0dd5b5caf..a30b9fb88 100644 --- a/plugins/beta/datasets/src/api/setFeatureVisibility.test.js +++ b/plugins/beta/datasets/src/api/setFeatureVisibility.test.js @@ -6,9 +6,7 @@ jest.mock('../registry/datasetRegistry.js', () => ({ datasetRegistry: { getDataset: jest.fn() } })) -jest.mock('../../../../../src/services/logger.js', () => ({ - logger: { warn: jest.fn() } -})) +jest.mock('../../../../../src/services/logger.js') describe('setFeatureVisibility', () => { const dispatch = jest.fn() diff --git a/plugins/beta/datasets/src/api/setGlobals.test.js b/plugins/beta/datasets/src/api/setGlobals.test.js index bb0e16512..6a107d229 100644 --- a/plugins/beta/datasets/src/api/setGlobals.test.js +++ b/plugins/beta/datasets/src/api/setGlobals.test.js @@ -1,9 +1,7 @@ import { setGlobals } from './setGlobals.js' import { logger } from '../../../../../src/services/logger.js' -jest.mock('../../../../../src/services/logger.js', () => ({ - logger: { warn: jest.fn() } -})) +jest.mock('../../../../../src/services/logger.js') describe('setGlobals', () => { const dispatch = jest.fn() diff --git a/plugins/beta/datasets/src/reducers/pluginState.test.js b/plugins/beta/datasets/src/reducers/pluginState.test.js index 68b4089bf..9a608ef8d 100644 --- a/plugins/beta/datasets/src/reducers/pluginState.test.js +++ b/plugins/beta/datasets/src/reducers/pluginState.test.js @@ -1,9 +1,7 @@ import { initialState, actions } from './pluginState.js' import { logger } from '../../../../../src/services/logger.js' -jest.mock('../../../../../src/services/logger.js', () => ({ - logger: { error: jest.fn() } -})) +jest.mock('../../../../../src/services/logger.js') // Helper: build a minimal plugin state with some mapped datasets const makeState = (overrides = {}) => ({ diff --git a/src/App/controls/keyboardActions.test.js b/src/App/controls/keyboardActions.test.js index b2f84bbbe..620e6fed4 100644 --- a/src/App/controls/keyboardActions.test.js +++ b/src/App/controls/keyboardActions.test.js @@ -7,9 +7,7 @@ jest.mock('../../services/reverseGeocode.js', () => ({ hasReverseGeocode: jest.fn() })) -jest.mock('../../services/logger.js', () => ({ - logger: { warn: jest.fn() } -})) +jest.mock('../../services/logger.js') const PAN_DELTA = 10 const NUDGE_PAN_DELTA = 5 diff --git a/src/App/renderer/mapButtons.test.js b/src/App/renderer/mapButtons.test.js index 8171a6933..52d8ca12d 100755 --- a/src/App/renderer/mapButtons.test.js +++ b/src/App/renderer/mapButtons.test.js @@ -3,7 +3,7 @@ import { mapButtons, getMatchingButtons, applySlotExclusivity, renderButton, res import { logger } from '../../services/logger.js' import { getPanelConfig } from '../registry/panelRegistry.js' -jest.mock('../../services/logger.js', () => ({ logger: { warn: jest.fn() } })) +jest.mock('../../services/logger.js') jest.mock('../registry/buttonRegistry.js') jest.mock('../registry/panelRegistry.js') jest.mock('../components/MapButton/MapButton.jsx', () => ({ diff --git a/src/InteractiveMap/deviceChecker.test.js b/src/InteractiveMap/deviceChecker.test.js index 6102998dc..facbfbf26 100755 --- a/src/InteractiveMap/deviceChecker.test.js +++ b/src/InteractiveMap/deviceChecker.test.js @@ -5,7 +5,7 @@ import { logger } from '../services/logger.js' jest.mock('./renderError.js') jest.mock('./domStateManager.js') -jest.mock('../services/logger.js', () => ({ logger: { warn: jest.fn() } })) +jest.mock('../services/logger.js') describe('checkDeviceSupport', () => { let rootEl, config diff --git a/src/services/__mocks/logger.js b/src/services/__mocks__/logger.js similarity index 100% rename from src/services/__mocks/logger.js rename to src/services/__mocks__/logger.js From 45854a5e5f9e3dddec4df11e024596bc4b4d4680 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Tue, 14 Jul 2026 09:07:18 +0100 Subject: [PATCH 36/53] IM-360-reviewed added tests for new providers code --- providers/mapProvider.test.js | 32 +++++++++++++++++++ providers/maplibre/src/maplibreProvider.js | 2 +- .../maplibre/src/maplibreProvider.test.js | 25 ++++++++++++++- 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 providers/mapProvider.test.js diff --git a/providers/mapProvider.test.js b/providers/mapProvider.test.js new file mode 100644 index 000000000..b16b44430 --- /dev/null +++ b/providers/mapProvider.test.js @@ -0,0 +1,32 @@ +import { MapProvider } from './mapProvider.js' + +describe('MapProvider', () => { + describe('isBaseMapReady', () => { + it('throws an error indicating the subclass must implement isBaseMapReady()', () => { + const provider = new MapProvider() + expect(() => provider.isBaseMapReady()).toThrow('must implement isBaseMapReady()') + }) + + it('includes the instance name in the error message when name is set', () => { + const provider = new MapProvider() + provider.name = 'TestProvider' + expect(() => provider.isBaseMapReady()).toThrow('TestProvider must implement isBaseMapReady()') + }) + + it('throws an Error instance', () => { + const provider = new MapProvider() + expect(() => provider.isBaseMapReady()).toThrow(Error) + }) + }) + + it('can be subclassed with an isBaseMapReady implementation', () => { + class ConcreteProvider extends MapProvider { + isBaseMapReady () { + return true + } + } + + const provider = new ConcreteProvider() + expect(provider.isBaseMapReady()).toBe(true) + }) +}) diff --git a/providers/maplibre/src/maplibreProvider.js b/providers/maplibre/src/maplibreProvider.js index f69eb04c2..f96343a49 100755 --- a/providers/maplibre/src/maplibreProvider.js +++ b/providers/maplibre/src/maplibreProvider.js @@ -120,7 +120,7 @@ export default class MapLibreProvider extends MapProvider { } isBaseMapReady () { - return this.map?.getStyle() + return Boolean(this.map?.getStyle()) } /** Destroy the map and clean up resources. */ diff --git a/providers/maplibre/src/maplibreProvider.test.js b/providers/maplibre/src/maplibreProvider.test.js index 9ad505c64..927cd3261 100644 --- a/providers/maplibre/src/maplibreProvider.test.js +++ b/providers/maplibre/src/maplibreProvider.test.js @@ -64,7 +64,8 @@ describe('MapLibreProvider', () => { getPixelRatio: jest.fn(() => 1), getCanvas: jest.fn(() => ({ style: {} })), getLayer: jest.fn(() => true), - queryRenderedFeatures: jest.fn(() => []) + queryRenderedFeatures: jest.fn(() => []), + getStyle: jest.fn(() => ({ layers: [] })) } eventBus = { emit: jest.fn() } maplibreModule = { Map: jest.fn(() => map), LngLatBounds: jest.fn() } @@ -135,6 +136,28 @@ describe('MapLibreProvider', () => { expect(createMapLabelNavigator).toHaveBeenCalledWith(map, undefined, expect.anything(), eventBus) }) + describe('isBaseMapReady', () => { + test('returns the result of map.getStyle() after map is initialised', async () => { + const p = makeProvider() + await doInitMap(p) + const style = { layers: [{ id: 'background' }] } + map.getStyle.mockReturnValue(style) + expect(p.isBaseMapReady()).toEqual(true) + }) + + test('returns undefined when map has not been initialised', () => { + const p = makeProvider() + expect(p.isBaseMapReady()).toEqual(false) + }) + + test('returns undefined when map.getStyle() returns undefined', async () => { + const p = makeProvider() + await doInitMap(p) + map.getStyle.mockReturnValue(undefined) + expect(p.isBaseMapReady()).toEqual(false) + }) + }) + test('destroyMap: calls remove on mapEvents/appEvents if set; skips if absent', async () => { const p = makeProvider() await doInitMap(p) From a1139dfa5a07fdc472bbd0804ec611b5f9d3acf2 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Tue, 14 Jul 2026 10:46:04 +0100 Subject: [PATCH 37/53] IM-372 Added Layers Menu for user configured radios (#398) * IM-372 added a menu reducer for user configured menus * IM-372 removed fill: 'transparent' from defaults * IM-372 updated pluginState test to refclect changed menu * IM-372 ensure adapter visibility change not triggered, when state doesn't change * IM-372 added 1st SW datasets and an isVisibleWhen helper method * IM-372 added a menuStateReducer * IM-372 LayersMenu renders radios * IM-372 added visibleWhen filter to layersMenu * IM-372 added tests for isVisibleWhen * IM-372 covered LayersMenu.jsx * IM-372 covered all LayersMenu/*.jsx --- demo/js/esri-datasets.js | 495 ++++++++++++++---- .../components/LayersMenu/Layers.module.scss | 5 +- .../src/components/LayersMenu/LayersMenu.jsx | 41 +- .../components/LayersMenu/LayersMenu.test.jsx | 175 +++++++ .../LayersMenu/LayersMenuCheckbox.test.jsx | 117 +++++ .../LayersMenu/LayersMenuGroupWrapper.jsx | 3 - .../LayersMenuGroupWrapper.test.jsx | 68 +++ .../components/LayersMenu/LayersMenuRadio.jsx | 28 + .../LayersMenu/LayersMenuRadio.test.jsx | 105 ++++ .../LayersMenu/LayersRadioGroupWrapper.jsx | 40 ++ .../LayersRadioGroupWrapper.test.jsx | 136 +++++ .../datasets/src/initialise/DatasetsInit.jsx | 3 + .../beta/datasets/src/initialise/defaults.js | 1 - .../datasets/src/initialise/defaults.test.js | 1 - .../src/initialise/initialiseDatasets.js | 9 +- .../src/initialise/initialiseDatasets.test.js | 2 +- .../datasets/src/reducers/menuStateReducer.js | 9 + .../beta/datasets/src/reducers/pluginState.js | 39 +- .../datasets/src/reducers/pluginState.test.js | 5 +- plugins/beta/datasets/src/registry/dataset.js | 7 +- .../datasets/src/registry/isVisibleWhen.js | 55 ++ .../src/registry/isVisibleWhen.test.js | 67 +++ 22 files changed, 1281 insertions(+), 130 deletions(-) create mode 100644 plugins/beta/datasets/src/components/LayersMenu/LayersMenu.test.jsx create mode 100644 plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.test.jsx create mode 100644 plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.test.jsx create mode 100644 plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.jsx create mode 100644 plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx create mode 100644 plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx create mode 100644 plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx create mode 100644 plugins/beta/datasets/src/reducers/menuStateReducer.js create mode 100644 plugins/beta/datasets/src/registry/isVisibleWhen.js create mode 100644 plugins/beta/datasets/src/registry/isVisibleWhen.test.js diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index c3c902027..1d6e2d83e 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -7,102 +7,400 @@ import createDatasetsPlugin from '/plugins/beta/datasets/src/index.js' import { vtsMapStyles27700 } from './mapStyles.js' import { transformGeocodeRequest, transformVtsRequest3857, setupEsriConfig } from './auth.js' +const nonFloodZoneLight = '#2b8cbe' +const nonFloodZoneDark = '#7fcdbb' + +const COLOURS = { + // floodExtents: { default: nonFloodZoneLight, dark: nonFloodZoneDark }, + + depthOver2300: { default: '#7f2704', dark: '#238b45' }, + depth2300: { default: '#a63603', dark: '#41ab5d' }, + depth1200: { default: '#d94801', dark: '#74c476' }, + depth900: { default: '#f16913', dark: '#a1d99b' }, + depth600: { default: '#fd8d3c', dark: '#c7e9c0' }, + depth300: { default: '#fdae6b', dark: '#e5f5e0' }, + depth150: { default: '#fdd0a2', dark: '#f7fcf5' }, + + floodZone3: { default: '#003078', dark: '#e5f5e0' }, + floodZone2: { default: '#1d70b8', dark: '#41ab5d' }, + floodZoneClimateChange: { default: '#F4A582', dark: '#BF3D4A' }, + // floodZoneClimateChangeNoData: { default: darkTeal, dark: white }, + + floodDefences: { default: '#f47738', dark: '#f47738' }, + // waterStorageAreas: { default: darkTeal, dark: white }, + // mainRivers: { default: darkTeal, dark: white } +} + +// light tones > 2300 to < 150 +const nonFloodZoneDepthBandsLight = [COLOURS.depthOver2300.default, COLOURS.depth2300.default, COLOURS.depth1200.default, COLOURS.depth900.default, COLOURS.depth600.default, COLOURS.depth300.default, COLOURS.depth150.default] +// GREENS dark tones > 2300 to < 150 +const nonFloodZoneDepthBandsDark = [COLOURS.depthOver2300.dark, COLOURS.depth2300.dark, COLOURS.depth1200.dark, COLOURS.depth900.dark, COLOURS.depth600.dark, COLOURS.depth300.dark, COLOURS.depth150.dark] + + + const datasetFloodZonesCC = { - id: 'flood-zones-cc', - label: 'Flood Zones Climate Change', - groupLabel: 'Datasets', - esriGroupId: 'flood-zones', - tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_CCP1_NON_PRODUCTION/VectorTileServer`, - showInKey: true, - showInMenu: true, - visible: true, - sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea CCP1', - sublayers: [ - { - id: 'climate-change', - label: 'Climate change (2070 to 2125)', - esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Flood Zones plus climate change/1', - showInKey: true, - showInMenu: false, - style: { - fill: { outdoor: '#F4A582', dark: '#BF3D4A' }, - stroke: 'none' - }, - }, - { - id: 'data-unavailable', - label: 'Climate change data unavailable', - style: { // This is used just for the key - so that it renders the pattern correctly. - fillPattern: 'dot', - fillPatternForegroundColor: { outdoor: '#000000', dark: '#ffffff' }, - stroke: { outdoor: '#000000', dark: '#FFFFFF' }, - }, - showInKey: true, - showInMenu: false, - }, - { - id: 'data-unavailable-outline', - style: { - stroke: { outdoor: '#000000', dark: '#FFFFFF' }, - }, - esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/0', - showInKey: false, - showInMenu: false, - }, - { - id: 'data-unavailable-light', - visibleWhen: { mapStyleId: ['outdoor', 'black-and-white'] }, - esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/1', - esriUseServerStyle: true, - showInKey: false, - showInMenu: false, - }, - { - id: 'data-unavailable-dark', - visibleWhen: { mapStyleId: ['dark'] }, - esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/2', - esriUseServerStyle: true, - showInKey: false, - showInMenu: false, + id: 'floodzonescc', + label: 'Flood Zones Climate Change', + groupLabel: 'Datasets', + esriGroupId: 'floodzones-group', + tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_CCP1_NON_PRODUCTION/VectorTileServer`, + showInKey: true, + showInMenu: true, + visible: true, + sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea CCP1', + sublayers: [ + { + id: 'climate-change', + label: 'Climate change (2070 to 2125)', + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Flood Zones plus climate change/1', + showInKey: true, + showInMenu: false, + visibleWhen: { + menu: { + dataset: ['floodzones'], timeframe: ['climatechange'] + } + }, + style: { + fill: { outdoor: '#F4A582', dark: '#BF3D4A' }, + stroke: 'none' + }, + }, + { + id: 'data-unavailable', + label: 'Climate change data unavailable', + showInKey: true, + showInMenu: false, + visibleWhen: { + menu: { + dataset: ['floodzones'], timeframe: ['climatechange'] + } + }, + style: { // This is used just for the key - so that it renders the pattern correctly. + fillPattern: 'dot', + fillPatternForegroundColor: { outdoor: '#000000', dark: '#ffffff' }, + stroke: { outdoor: '#000000', dark: '#FFFFFF' }, } - ] - } + }, + { + id: 'data-unavailable-outline', + showInKey: false, + showInMenu: false, + visibleWhen: { + menu: { dataset: ['floodzones'], timeframe: ['climatechange'] } + }, + style: { + stroke: { outdoor: '#000000', dark: '#FFFFFF' }, + }, + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/0' + }, + { + id: 'data-unavailable-light', + visibleWhen: { + mapStyleId: ['outdoor', 'black-and-white'], + menu: { dataset: ['floodzones'], timeframe: ['climatechange'] } + }, + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/1', + esriUseServerStyle: true, + showInKey: false, + showInMenu: false, + }, + { + id: 'data-unavailable-dark', + visibleWhen: { + menu: { dataset: ['floodzones'], timeframe: ['climatechange'] }, + mapStyleId: ['dark'] + }, + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/2', + esriUseServerStyle: true, + showInKey: false, + showInMenu: false, + } + ] +} - const datasetFloodZones = { - id: 'flood-zones', - label: 'Flood Zones', - groupLabel: 'Datasets', - esriGroupId: 'flood-zones', - tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_NON_PRODUCTION/VectorTileServer`, - showInKey: true, - // showInMenu: true, - sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea', - sublayers: [ - { - id: 'flood-zone-2', - label: 'Flood Zone 2', - esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 2/1', - showInMenu: true, - style: { - fill: { outdoor: '#1d70b8', dark: '#7fcdbb' }, - stroke: 'none' - }, - }, - { - id: 'flood-zone-3', - label: 'Flood Zone 3', - esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 3/1', - showInMenu: true, - style: { - fill: { outdoor: '#003078', dark: '#e5f5e0' }, - stroke: 'none' - }, +const datasetFloodZones = { + id: 'floodzones', + label: 'Flood Zones', + groupLabel: 'Datasets', + esriGroupId: 'floodzones-group', + tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_NON_PRODUCTION/VectorTileServer`, + showInKey: true, + // showInMenu: true, + sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea', + visibleWhen: { + menu: { dataset: ['floodzones'] } + }, + sublayers: [ + { + id: 'flood-zone-2', + label: 'Flood Zone 2', + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 2/1', + style: { + fill: { outdoor: '#1d70b8', dark: '#7fcdbb' }, + stroke: 'none' + }, + }, + { + id: 'flood-zone-3', + label: 'Flood Zone 3', + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 3/1', + style: { + fill: { outdoor: '#003078', dark: '#e5f5e0' }, + stroke: 'none' + }, + } + ] +} + +const surfaceWaterDataset = { + id: 'surfacewater', + label: 'Surface Water', + groupLabel: 'Datasets', + tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Surface_Water_Spatial_Planning_1_in_1000_Depths_NON_PRODUCTION/VectorTileServer`, + showInKey: true, + sourceLayer: 'Surface Water Spatial Planning 1 in 1000 Depths', + visibleWhen: { + menu: { + dataset: ['surfacewater'], + timeframe: ['presentday'], + aep: ['low'], + } + }, + sublayers: [ + { + id: 'depthOver2300', + label: 'Depth over 2300mm', + esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/>2300mm/1', + visibleWhen: { + menu: { + dataset: ['surfacewater'], + timeframe: ['presentday'], + aep: ['low'], + depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200', 'depth2300', 'depthOver2300'] + } + }, + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[0], dark: nonFloodZoneDepthBandsDark[0] }, } - ] - } + }, + { + id: 'depth2300', + esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/1200-2300mm/1', + visibleWhen: { + menu: { + dataset: ['surfacewater'], + timeframe: ['presentday'], + aep: ['low'], + depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200', 'depth2300'] + } + }, + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[1], dark: nonFloodZoneDepthBandsDark[1] }, + } + }, + { + id: 'depth1200', + esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/900-1200mm/1', + visibleWhen: { + menu: { + dataset: ['surfacewater'], + timeframe: ['presentday'], + aep: ['low'], + depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200'] + } + }, + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[2], dark: nonFloodZoneDepthBandsDark[2] }, + } + }, + { + id: 'depth900', + esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/600-900mm/1', + visibleWhen: { + menu: { + dataset: ['surfacewater'], + timeframe: ['presentday'], + aep: ['low'], + depth: ['depth150', 'depth300', 'depth600', 'depth900'] + } + }, + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[3], dark: nonFloodZoneDepthBandsDark[3] }, + } + }, + { + id: 'depth600', + esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/300-600mm/1', + visibleWhen: { + menu: { + dataset: ['surfacewater'], + timeframe: ['presentday'], + aep: ['low'], + depth: ['depth150', 'depth300', 'depth600'] + } + }, + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[4], dark: nonFloodZoneDepthBandsDark[4] }, + } + }, + { + id: 'depth300', + esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/150-300mm/1', + visibleWhen: { + menu: { + dataset: ['surfacewater'], + timeframe: ['presentday'], + aep: ['low'], + depth: ['depth150', 'depth300'] + } + }, + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[5], dark: nonFloodZoneDepthBandsDark[5] }, + } + }, + { + id: 'depth150', + esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/<150mm/1', + visibleWhen: { + menu: { + dataset: ['surfacewater'], + timeframe: ['presentday'], + aep: ['low'], + depth: ['depth150'] + } + }, + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[6], dark: nonFloodZoneDepthBandsDark[6] }, + } + }, + ] +} + +const surfaceWaterDepthAllDataset = { + id: 'surfacewaterDepthAll', + label: 'Surface Water Depth All', + groupLabel: 'Datasets', + tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Surface_Water_Spatial_Planning_1_in_1000_Depths_NON_PRODUCTION/VectorTileServer`, + showInKey: true, + sourceLayer: 'Surface Water Spatial Planning 1 in 1000 Depths', + style: { + fill: { outdoor: nonFloodZoneLight, dark: nonFloodZoneDark }, + }, + visibleWhen: { + menu: { + dataset: ['surfacewater'], + timeframe: ['presentday'], + aep: ['low'], + depth: ['depthAll'] + }, + }, + sublayers: [ + { + id: 'depthOver2300', + esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/>2300mm/1', + }, + { + id: 'depth2300', + esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/1200-2300mm/1', + }, + { + id: 'depth1200', + esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/900-1200mm/1', + }, + { + id: 'depth900', + esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/600-900mm/1', + }, + { + id: 'depth600', + esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/300-600mm/1', + }, + { + id: 'depth300', + esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/150-300mm/1', + }, + { + id: 'depth150', + esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/<150mm/1', + } + ] +} const datasets = [ - datasetFloodZonesCC, datasetFloodZones + datasetFloodZonesCC, datasetFloodZones, surfaceWaterDataset, surfaceWaterDepthAllDataset +] + +const menu = [ + { + id: 'dataset', + label: 'Datasets', + urlKey: 'dataset', + visibleWhen: true, + type: 'radio', // 'checkbox' or 'radio' + value: 'floodzones', // this is the default value for the menu, it should be one of the items' id + items: [ + { id: 'floodzones', label: 'Flood zones' }, + { id: 'surfacewater', label: 'Surface water' }, + { id: 'none', label: 'None', }, + ], + }, + { + id: 'timeframe', + label: 'Timeframe', + urlKey: 'dataset', + urlIndex: 1, // eg: surfacewater-presentday-high-depth or floodzones-climatechange + type: 'radio', + visibleWhen: { menu: { dataset: ['floodzones', 'surfacewater'] } }, + value: 'presentday', + items: [ + { id: 'presentday', label: 'Present day' }, + { id: 'climatechange', label: '2070 to 2125', visibleWhen: { menu: { dataset: ['floodzones'] } } }, + { id: 'climatechange', label: '2061 to 2125', visibleWhen: { menu: { dataset: ['surfacewater'] } } }, + ] + }, { + id: 'aep', + label: 'Annual likelihood of flooding', + urlKey: 'dataset', + urlIndex: 2, + type: 'radio', + visibleWhen: { menu: { dataset: ['surfacewater'] } }, + value: 'medium', + items: [ + { id: 'high', label: '1 in 30' }, + { id: 'medium', label: '1 in 100' }, + { id: 'low', label: '1 in 1000' }, + ] + }, { + id: 'depth', + label: 'Depth', + urlKey: 'dataset', + urlIndex: 3, + type: 'radio', + visibleWhen: { menu: { dataset: ['surfacewater'] } }, + subMenu: true, + value: 'depthAll', + items: [ + { id: 'depthAll', label: 'All depths', }, + { id: 'depth150', label: 'Full extent of flooding', }, + { id: 'depth300', label: 'Extent over 150mm', }, + { id: 'depth600', label: 'Extent over 300mm', }, + { id: 'depth900', label: 'Extent over 600mm', }, + { id: 'depth1200', label: 'Extent over 900mm', }, + { id: 'depth2300', label: 'Extent over 1200mm', }, + { id: 'depthOver2300', label: 'Extent over 2300mm', }, + ] + }, { + id: 'features', + label: 'Map features', + urlKey: 'features', + type: 'checkbox', + visibleWhen: true, + items: [ + { id: 'water-storage', label: 'Water storage', checked: false }, + { id: 'flood-defence', label: 'Flood defence', checked: false }, + { id: 'main-rivers', label: 'Main rivers', checked: false }, + ] + } ] const datasetsPlugin = createDatasetsPlugin({ @@ -111,7 +409,8 @@ const datasetsPlugin = createDatasetsPlugin({ opacity: 0.75, visible: true }, - datasets + datasets, + menu }) const interactiveMap = new InteractiveMap('map', { @@ -132,8 +431,8 @@ const interactiveMap = new InteractiveMap('map', { desktop: { slot: 'right-top', showLabel: true } }], panels: [ - { - id: 'mapStyles', + { + id: 'mapStyles', desktop: { slot: 'map-styles-button', width: '400px', modal: true } } ] @@ -147,9 +446,13 @@ const testGlobalVisibility = () => { setTimeout(() => datasetsPlugin.setDatasetVisibility(true), 6000) } +const testAddRemoveDataset = () => { + setTimeout(() => datasetsPlugin.removeDataset('floodzonescc'), 1000) + setTimeout(() => datasetsPlugin.removeDataset('floodzones'), 3000) + setTimeout(() => datasetsPlugin.addDataset(datasetFloodZones), 5000) +} + interactiveMap.on('datasets:ready', function () { // testGlobalVisibility() - setTimeout(() => datasetsPlugin.removeDataset('flood-zones-cc'), 1000) - setTimeout(() => datasetsPlugin.removeDataset('flood-zones'), 3000) - setTimeout(() => datasetsPlugin.addDataset(datasetFloodZones), 5000) + // testAddRemoveDataset() }) \ No newline at end of file diff --git a/plugins/beta/datasets/src/components/LayersMenu/Layers.module.scss b/plugins/beta/datasets/src/components/LayersMenu/Layers.module.scss index ce035ba65..da6b1ba0f 100644 --- a/plugins/beta/datasets/src/components/LayersMenu/Layers.module.scss +++ b/plugins/beta/datasets/src/components/LayersMenu/Layers.module.scss @@ -51,10 +51,11 @@ } // GovUK style overide -.im-c-datasets-layers__item .govuk-checkboxes__item { +.im-c-datasets-layers__item .govuk-checkboxes__item, +.im-c-datasets-layers__item .govuk-radios__item { flex-wrap: nowrap; - .govuk-checkboxes__input { + .govuk-checkboxes__input, .govuk-radios__input { margin-left: -3px; } } diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx index d9221cef6..d3998299c 100644 --- a/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx +++ b/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx @@ -1,10 +1,12 @@ import React from 'react' import { setDatasetVisibility } from '../../api/setDatasetVisibility.js' import { LayersMenuCheckbox } from './LayersMenuCheckbox.jsx' +import { LayersRadioGroupWrapper } from './LayersRadioGroupWrapper.jsx' import { LayersMenuGroupWrapper } from './LayersMenuGroupWrapper.jsx' export const LayersMenu = ({ pluginState }) => { const { menu = [] } = pluginState + const handleDatasetChange = (e) => { const { value, checked } = e.target setDatasetVisibility({ pluginState }, checked, { datasetId: value }) @@ -15,18 +17,33 @@ export const LayersMenu = ({ pluginState }) => { return (
{// Each menuGroup - menu.map(menuGroup => - - {// Each menuGroupItem - menuGroup.items.map(menuGroupItem => - - ) - } - ) + menu.map(menuGroup => { + const { type } = menuGroup + if (type === 'checkbox') { + return ( + { + // Each menuGroupItem + menuGroup.items.map(menuGroupItem => ( + ) + ) + } + + ) + } else { + return ( + + ) + } + } + ) }
) diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.test.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.test.jsx new file mode 100644 index 000000000..0799c32b8 --- /dev/null +++ b/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.test.jsx @@ -0,0 +1,175 @@ +import { render } from '@testing-library/react' +import { LayersMenu } from './LayersMenu' +import { setDatasetVisibility } from '../../api/setDatasetVisibility.js' + +jest.mock('../../api/setDatasetVisibility.js', () => ({ + setDatasetVisibility: jest.fn() +})) + +let capturedOnChange = null + +jest.mock('./LayersMenuCheckbox.jsx', () => ({ + LayersMenuCheckbox: ({ menuGroupItem, onChange }) => { + capturedOnChange = onChange + return ( +
+ ) + } +})) + +jest.mock('./LayersRadioGroupWrapper.jsx', () => ({ + LayersRadioGroupWrapper: ({ menuGroup }) => ( +
+ ) +})) + +jest.mock('./LayersMenuGroupWrapper.jsx', () => ({ + LayersMenuGroupWrapper: ({ menuGroup, children }) => ( +
+ {children} +
+ ) +})) + +const makePluginState = (menu = [], extra = {}) => ({ + menu, + dispatch: jest.fn(), + ...extra +}) + +describe('LayersMenu', () => { + beforeEach(() => { + setDatasetVisibility.mockClear() + }) + + describe('container class', () => { + it('renders the base container class when no groups have a groupLabel', () => { + const pluginState = makePluginState([{ id: 'g1', type: 'checkbox', items: [] }]) + const { container } = render() + const div = container.firstChild + expect(div.className).toBe('im-c-datasets-layers') + }) + + it('adds the --has-groups modifier when at least one group has a groupLabel', () => { + const pluginState = makePluginState([{ id: 'g1', type: 'checkbox', groupLabel: 'My Group', items: [] }]) + const { container } = render() + const div = container.firstChild + expect(div.className).toBe('im-c-datasets-layers im-c-datasets-layers--has-groups') + }) + }) + + describe('empty menu', () => { + it('renders the container with no children when menu is empty', () => { + const pluginState = makePluginState([]) + const { container } = render() + expect(container.firstChild.children).toHaveLength(0) + }) + + it('uses an empty array when menu is not provided', () => { + const { container } = render() + expect(container.firstChild.children).toHaveLength(0) + }) + }) + + describe('checkbox groups', () => { + it('renders a LayersMenuGroupWrapper for each checkbox group', () => { + const pluginState = makePluginState([ + { id: 'g1', type: 'checkbox', items: [] }, + { id: 'g2', type: 'checkbox', items: [] } + ]) + const { getAllByTestId } = render() + expect(getAllByTestId('layers-menu-group-wrapper')).toHaveLength(2) + }) + + it('renders a LayersMenuCheckbox for each item in a checkbox group', () => { + const pluginState = makePluginState([ + { id: 'g1', type: 'checkbox', items: [{ id: 'item-a' }, { id: 'item-b' }] } + ]) + const { getAllByTestId } = render() + expect(getAllByTestId('layers-menu-checkbox')).toHaveLength(2) + }) + + it('passes the correct group to LayersMenuGroupWrapper', () => { + const group = { id: 'group-x', type: 'checkbox', items: [] } + const pluginState = makePluginState([group]) + const { getByTestId } = render() + expect(getByTestId('layers-menu-group-wrapper').dataset.groupId).toBe('group-x') + }) + + it('passes the correct item to LayersMenuCheckbox', () => { + const pluginState = makePluginState([ + { id: 'g1', type: 'checkbox', items: [{ id: 'item-1' }] } + ]) + const { getByTestId } = render() + expect(getByTestId('layers-menu-checkbox').dataset.itemId).toBe('item-1') + }) + }) + + describe('radio groups', () => { + it('renders a LayersRadioGroupWrapper for non-checkbox groups', () => { + const pluginState = makePluginState([ + { id: 'r1', type: 'radio', items: [{ id: 'opt1' }, { id: 'opt2' }] } + ]) + const { getAllByTestId } = render() + expect(getAllByTestId('layers-radio-group-wrapper')).toHaveLength(1) + }) + + it('passes the correct group to LayersRadioGroupWrapper', () => { + const pluginState = makePluginState([ + { id: 'radio-group-1', type: 'radio', items: [] } + ]) + const { getByTestId } = render() + expect(getByTestId('layers-radio-group-wrapper').dataset.groupId).toBe('radio-group-1') + }) + + it('does not render a LayersMenuGroupWrapper for radio groups', () => { + const pluginState = makePluginState([{ id: 'r1', type: 'radio', items: [] }]) + const { queryByTestId } = render() + expect(queryByTestId('layers-menu-group-wrapper')).toBeNull() + }) + }) + + describe('mixed groups', () => { + it('renders both checkbox and radio wrappers when both types are present', () => { + const pluginState = makePluginState([ + { id: 'c1', type: 'checkbox', items: [] }, + { id: 'r1', type: 'radio', items: [] } + ]) + const { getByTestId } = render() + expect(getByTestId('layers-menu-group-wrapper')).toBeTruthy() + expect(getByTestId('layers-radio-group-wrapper')).toBeTruthy() + }) + }) + + describe('handleDatasetChange', () => { + it('calls setDatasetVisibility with the correct arguments when a checkbox changes', () => { + const pluginState = makePluginState([ + { id: 'g1', type: 'checkbox', items: [{ id: 'dataset-abc' }] } + ]) + render() + capturedOnChange({ target: { value: 'dataset-abc', checked: true } }) + expect(setDatasetVisibility).toHaveBeenCalledTimes(1) + expect(setDatasetVisibility).toHaveBeenCalledWith( + { pluginState }, + true, + { datasetId: 'dataset-abc' } + ) + }) + + it('passes checked: false to setDatasetVisibility when unchecking', () => { + const pluginState = makePluginState([ + { id: 'g1', type: 'checkbox', items: [{ id: 'dataset-xyz' }] } + ]) + render() + capturedOnChange({ target: { value: 'dataset-xyz', checked: false } }) + expect(setDatasetVisibility).toHaveBeenCalledWith( + { pluginState }, + false, + { datasetId: 'dataset-xyz' } + ) + }) + }) +}) diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.test.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.test.jsx new file mode 100644 index 000000000..c0c7841eb --- /dev/null +++ b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.test.jsx @@ -0,0 +1,117 @@ +import { render, screen, act } from '@testing-library/react' +import { datasetRegistry } from '../../registry/datasetRegistry.js' +import { LayersMenuCheckbox } from './LayersMenuCheckbox' + +jest.mock('../../registry/datasetRegistry.js', () => ({ + datasetRegistry: { + getDataset: jest.fn() + } +})) + +const onChange = jest.fn() +const menuGroupItem = { id: 'dataset-1' } + +const baseDataset = { + id: 'dataset-1', + label: 'Dataset One', + visible: true, + isLocallyVisible: true, + isSublayer: false, + parentId: undefined +} + +beforeEach(() => { + datasetRegistry.getDataset.mockReset() + onChange.mockReset() +}) + +describe('LayersMenuCheckbox', () => { + describe('when the dataset is not in the registry', () => { + it('returns null', () => { + datasetRegistry.getDataset.mockReturnValue(undefined) + const { container } = render() + expect(container.firstChild).toBeNull() + }) + }) + + describe('when the dataset exists', () => { + it('renders a checkbox input', () => { + datasetRegistry.getDataset.mockReturnValue(baseDataset) + const { container } = render() + expect(container.querySelector('input[type="checkbox"]')).toBeTruthy() + }) + + it('renders the dataset label', () => { + datasetRegistry.getDataset.mockReturnValue(baseDataset) + render() + expect(screen.getByText('Dataset One')).toBeTruthy() + }) + + it('associates the label with the input via htmlFor', () => { + datasetRegistry.getDataset.mockReturnValue(baseDataset) + const { container } = render() + const label = container.querySelector('label') + const input = container.querySelector('input') + expect(label.htmlFor).toBe(input.id) + }) + + it('calls onChange when the checkbox changes', () => { + datasetRegistry.getDataset.mockReturnValue(baseDataset) + const { container } = render() + const input = container.querySelector('input') + const propsKey = Object.keys(input).find(k => k.startsWith('__reactProps')) + act(() => { input[propsKey].onChange({ target: { checked: true, value: input.value } }) }) + expect(onChange).toHaveBeenCalledTimes(1) + }) + }) + + describe('checked state', () => { + it('is checked when isLocallyVisible is true', () => { + datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, isLocallyVisible: true }) + const { container } = render() + expect(container.querySelector('input').checked).toBe(true) + }) + + it('is unchecked when isLocallyVisible is false', () => { + datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, isLocallyVisible: false }) + const { container } = render() + expect(container.querySelector('input').checked).toBe(false) + }) + }) + + describe('item class', () => { + it('does not include the --checked modifier when visible is true', () => { + datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, visible: true }) + const { container } = render() + expect(container.firstChild.className).not.toContain('im-c-datasets-layers__item--checked') + }) + + it('includes the --checked modifier when visible is false', () => { + datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, visible: false }) + const { container } = render() + expect(container.firstChild.className).toContain('im-c-datasets-layers__item--checked') + }) + }) + + describe('data attributes', () => { + it('uses the dataset id as data-dataset-id when not a sublayer', () => { + datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, isSublayer: false }) + const { container } = render() + const input = container.querySelector('input') + expect(input.dataset.datasetId).toBe('dataset-1') + expect(input.dataset.sublayerId).toBeUndefined() + }) + + it('uses parentId as data-dataset-id when a sublayer', () => { + datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, id: 'sub-1', isSublayer: true, parentId: 'parent-1' }) + const { container } = render() + expect(container.querySelector('input').dataset.datasetId).toBe('parent-1') + }) + + it('uses the sublayer id as data-sublayer-id when a sublayer', () => { + datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, id: 'sub-1', isSublayer: true, parentId: 'parent-1' }) + const { container } = render() + expect(container.querySelector('input').dataset.sublayerId).toBe('sub-1') + }) + }) +}) diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx index 8467486a5..912948ac3 100644 --- a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx +++ b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx @@ -2,9 +2,6 @@ export const LayersMenuGroupWrapper = ({ menuGroup, children }) => { if (!menuGroup.groupLabel) { return <>{children} } - // TODO - is there any need for im-c-datasets-layers-group--items-checked - it isn't used anywhere - // const anyItemsChecked = false - // const wrapperClass = `govuk-form-group im-c-datasets-layers-group${anyItemsChecked ? ' im-c-datasets-layers-group--items-checked' : ''}` const wrapperClass = 'govuk-form-group im-c-datasets-layers-group' return (
diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.test.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.test.jsx new file mode 100644 index 000000000..45865a3e4 --- /dev/null +++ b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.test.jsx @@ -0,0 +1,68 @@ +import { render, screen } from '@testing-library/react' +import { LayersMenuGroupWrapper } from './LayersMenuGroupWrapper' + +const child = Child content + +describe('LayersMenuGroupWrapper', () => { + describe('without a groupLabel', () => { + it('renders children directly without a wrapping div', () => { + const { getByTestId, container } = render( + {child} + ) + expect(getByTestId('child')).toBeTruthy() + expect(container.querySelector('.govuk-form-group')).toBeNull() + }) + + it('does not render a fieldset when groupLabel is absent', () => { + const { container } = render( + {child} + ) + expect(container.querySelector('fieldset')).toBeNull() + }) + + it('does not render a fieldset when groupLabel is an empty string', () => { + const { container } = render( + {child} + ) + expect(container.querySelector('fieldset')).toBeNull() + }) + }) + + describe('with a groupLabel', () => { + it('renders the outer wrapper with the correct classes', () => { + const { container } = render( + {child} + ) + const wrapper = container.querySelector('.govuk-form-group.im-c-datasets-layers-group') + expect(wrapper).toBeTruthy() + }) + + it('renders a fieldset with the correct class', () => { + const { container } = render( + {child} + ) + expect(container.querySelector('fieldset.im-c-datasets-layers-group__fieldset')).toBeTruthy() + }) + + it('renders the groupLabel as the legend text', () => { + render( + {child} + ) + expect(screen.getByText('My Group')).toBeTruthy() + }) + + it('renders the legend with the correct class', () => { + const { container } = render( + {child} + ) + expect(container.querySelector('legend.im-c-datasets-layers-group__legend')).toBeTruthy() + }) + + it('renders children inside the fieldset', () => { + const { getByTestId } = render( + {child} + ) + expect(getByTestId('child')).toBeTruthy() + }) + }) +}) diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.jsx new file mode 100644 index 000000000..2b74090ea --- /dev/null +++ b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.jsx @@ -0,0 +1,28 @@ +import { isVisibleWhen } from '../../registry/isVisibleWhen.js' + +export const LayersMenuRadio = ({ menuState, menuGroupItem, checked, name, onChange }) => { + const itemClass = 'im-c-datasets-layers__item govuk-radios govuk-radios--small"' + const { visibleWhen } = menuGroupItem + const visible = visibleWhen ? isVisibleWhen(visibleWhen) : true + if (!visible) { + return null + } + return ( +
+
+ + +
+
+ ) +} diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx new file mode 100644 index 000000000..55b2c15af --- /dev/null +++ b/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx @@ -0,0 +1,105 @@ +import { render, screen, act } from '@testing-library/react' +import { isVisibleWhen } from '../../registry/isVisibleWhen.js' +import { LayersMenuRadio } from './LayersMenuRadio' + +jest.mock('../../registry/isVisibleWhen.js', () => ({ + isVisibleWhen: jest.fn() +})) + +const onChange = jest.fn() +const baseItem = { id: 'radio-1', label: 'Option One' } + +beforeEach(() => { + isVisibleWhen.mockReturnValue(true) + onChange.mockReset() +}) + +describe('LayersMenuRadio', () => { + describe('visibility', () => { + it('renders when visibleWhen is not set', () => { + const { container } = render( + + ) + expect(container.querySelector('input[type="radio"]')).toBeTruthy() + }) + + it('does not call isVisibleWhen when visibleWhen is not set', () => { + render() + expect(isVisibleWhen).not.toHaveBeenCalled() + }) + + it('calls isVisibleWhen with the visibleWhen value', () => { + const visibleWhen = { menu: ['someValue'] } + render() + expect(isVisibleWhen).toHaveBeenCalledWith(visibleWhen) + }) + + it('returns null when isVisibleWhen resolves to false', () => { + isVisibleWhen.mockReturnValue(false) + const { container } = render( + + ) + expect(container.firstChild).toBeNull() + }) + + it('renders when isVisibleWhen resolves to true', () => { + isVisibleWhen.mockReturnValue(true) + const { container } = render( + + ) + expect(container.querySelector('input[type="radio"]')).toBeTruthy() + }) + }) + + describe('rendered output', () => { + it('renders the item label text', () => { + render() + expect(screen.getByText('Option One')).toBeTruthy() + }) + + it('sets the name attribute on the input', () => { + const { container } = render( + + ) + expect(container.querySelector('input').name).toBe('my-radio-group') + }) + + it('sets checked to true when checked prop is true', () => { + const { container } = render( + + ) + expect(container.querySelector('input').checked).toBe(true) + }) + + it('sets checked to false when checked prop is false', () => { + const { container } = render( + + ) + expect(container.querySelector('input').checked).toBe(false) + }) + + it('sets the input value to the item id', () => { + const { container } = render( + + ) + expect(container.querySelector('input').value).toBe('radio-1') + }) + + it('associates the label with the input via htmlFor', () => { + const { container } = render( + + ) + expect(container.querySelector('label').htmlFor).toBe('radio-1') + }) + + it('calls onChange when the radio input changes', () => { + const { container } = render( + + ) + const input = container.querySelector('input') + const propsKey = Object.keys(input).find(k => k.startsWith('__reactProps')) + act(() => { input[propsKey].onChange({ target: { value: 'radio-1' } }) }) + expect(onChange).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx new file mode 100644 index 000000000..493d51430 --- /dev/null +++ b/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx @@ -0,0 +1,40 @@ +import React, { useState } from 'react' +import { isVisibleWhen } from '../../registry/isVisibleWhen.js' +import { LayersMenuRadio } from './LayersMenuRadio.jsx' + +export const LayersRadioGroupWrapper = ({ pluginState, menuGroup }) => { + const { id, items, visibleWhen } = menuGroup + const visible = visibleWhen ? isVisibleWhen(visibleWhen) : true + if (!visible) { + return null + } + + const { menuState, dispatch } = pluginState + const [value, setValue] = useState(menuState[id]) + const handleChange = (event) => { + setValue(event.target.value) + dispatch({ type: 'UPDATE_MENU_STATE', payload: { [id]: event.target.value } }) + } + + const wrapperClass = 'govuk-form-group im-c-datasets-layers-group' + return ( +
+
+ + {menuGroup.label} + +
+ {items.map((menuGroupItem) => + + )} +
+
+
+ ) +} diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx b/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx new file mode 100644 index 000000000..24095d3e1 --- /dev/null +++ b/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx @@ -0,0 +1,136 @@ +import { render, screen, act } from '@testing-library/react' +import { isVisibleWhen } from '../../registry/isVisibleWhen.js' +import { LayersRadioGroupWrapper } from './LayersRadioGroupWrapper' + +jest.mock('../../registry/isVisibleWhen.js', () => ({ + isVisibleWhen: jest.fn() +})) + +let capturedOnChange = null + +jest.mock('./LayersMenuRadio.jsx', () => ({ + LayersMenuRadio: ({ menuGroupItem, checked, name, onChange }) => { + capturedOnChange = onChange + return ( +
+ ) + } +})) + +const makePluginState = (menuState = {}) => ({ + menuState, + dispatch: jest.fn() +}) + +const baseGroup = { + id: 'group-1', + label: 'My Group', + items: [{ id: 'opt-a' }, { id: 'opt-b' }] +} + +beforeEach(() => { + isVisibleWhen.mockClear() + isVisibleWhen.mockReturnValue(true) + capturedOnChange = null +}) + +describe('LayersRadioGroupWrapper', () => { + describe('visibility', () => { + it('returns null when isVisibleWhen resolves to false', () => { + isVisibleWhen.mockReturnValue(false) + const { container } = render( + + ) + expect(container.firstChild).toBeNull() + }) + + it('renders when visibleWhen is not set', () => { + const { container } = render( + + ) + expect(container.querySelector('.govuk-form-group')).toBeTruthy() + }) + + it('does not call isVisibleWhen when visibleWhen is not set', () => { + render() + expect(isVisibleWhen).not.toHaveBeenCalled() + }) + + it('calls isVisibleWhen with the visibleWhen value', () => { + const visibleWhen = { menu: ['someValue'] } + render() + expect(isVisibleWhen).toHaveBeenCalledWith(visibleWhen) + }) + }) + + describe('rendered output', () => { + it('renders the group label as the legend', () => { + render() + expect(screen.getByText('My Group')).toBeTruthy() + }) + + it('renders a LayersMenuRadio for each item', () => { + const { getAllByTestId } = render( + + ) + expect(getAllByTestId('layers-menu-radio')).toHaveLength(2) + }) + + it('passes the group id as the name to each radio item', () => { + const { getAllByTestId } = render( + + ) + getAllByTestId('layers-menu-radio').forEach(radio => { + expect(radio.dataset.name).toBe('group-1') + }) + }) + }) + + describe('checked state', () => { + it('marks the item matching menuState as checked', () => { + const { getAllByTestId } = render( + + ) + const radios = getAllByTestId('layers-menu-radio') + expect(radios[0].dataset.checked).toBe('true') + expect(radios[1].dataset.checked).toBe('false') + }) + + it('marks no item as checked when menuState has no value for the group', () => { + const { getAllByTestId } = render( + + ) + getAllByTestId('layers-menu-radio').forEach(radio => { + expect(radio.dataset.checked).toBe('false') + }) + }) + }) + + describe('handleChange', () => { + it('dispatches UPDATE_MENU_STATE with the selected item id', () => { + const pluginState = makePluginState({ 'group-1': 'opt-a' }) + render() + act(() => { capturedOnChange({ target: { value: 'opt-b' } }) }) + expect(pluginState.dispatch).toHaveBeenCalledWith({ + type: 'UPDATE_MENU_STATE', + payload: { 'group-1': 'opt-b' } + }) + }) + + it('dispatches using the group id as the payload key', () => { + const group = { ...baseGroup, id: 'another-group' } + const pluginState = makePluginState({ 'another-group': 'opt-a' }) + render() + act(() => { capturedOnChange({ target: { value: 'opt-b' } }) }) + expect(pluginState.dispatch).toHaveBeenCalledWith({ + type: 'UPDATE_MENU_STATE', + payload: { 'another-group': 'opt-b' } + }) + }) + }) +}) diff --git a/plugins/beta/datasets/src/initialise/DatasetsInit.jsx b/plugins/beta/datasets/src/initialise/DatasetsInit.jsx index 36964be12..81f88be46 100755 --- a/plugins/beta/datasets/src/initialise/DatasetsInit.jsx +++ b/plugins/beta/datasets/src/initialise/DatasetsInit.jsx @@ -3,6 +3,7 @@ import { useEffect, useRef } from 'react' import { EVENTS } from '../../../../../src/config/events.js' import { initialiseDatasets } from './initialiseDatasets.js' import { datasetRegistry } from '../registry/datasetRegistry.js' +import { setMenuState } from '../registry/isVisibleWhen.js' import { attachGlobalState } from '../registry/globalDataset.js' import { loadLayerAdapter, layerAdapter } from '../adapters/loadLayerAdapter.js' @@ -49,6 +50,8 @@ export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, m initDatasets() }, [isBaseMapReady, appState.mode]) + useEffect(() => setMenuState(pluginState.menuState), [pluginState.menuState]) + useEffect(() => datasetRegistry.attach(pluginState.mappedDatasets, pluginState.orderedDatasets), [pluginState.mappedDatasets, pluginState.orderedDatasets]) diff --git a/plugins/beta/datasets/src/initialise/defaults.js b/plugins/beta/datasets/src/initialise/defaults.js index 7153f2ddc..62b6e66e9 100644 --- a/plugins/beta/datasets/src/initialise/defaults.js +++ b/plugins/beta/datasets/src/initialise/defaults.js @@ -7,7 +7,6 @@ const datasetDefaults = { style: { stroke: '#d4351c', strokeWidth: 2, - fill: 'transparent', symbolDescription: 'red outline' } } diff --git a/plugins/beta/datasets/src/initialise/defaults.test.js b/plugins/beta/datasets/src/initialise/defaults.test.js index 626add113..6e25a2715 100644 --- a/plugins/beta/datasets/src/initialise/defaults.test.js +++ b/plugins/beta/datasets/src/initialise/defaults.test.js @@ -15,7 +15,6 @@ describe('datasetDefaults', () => { expect(datasetDefaults.style).toMatchObject({ stroke: '#d4351c', strokeWidth: 2, - fill: 'transparent', symbolDescription: 'red outline' }) }) diff --git a/plugins/beta/datasets/src/initialise/initialiseDatasets.js b/plugins/beta/datasets/src/initialise/initialiseDatasets.js index ecd97f1f7..cda72920a 100644 --- a/plugins/beta/datasets/src/initialise/initialiseDatasets.js +++ b/plugins/beta/datasets/src/initialise/initialiseDatasets.js @@ -3,6 +3,9 @@ import { createDynamicSource } from '../fetch/createDynamicSource.js' import { applyDatasetDefaults, datasetDefaults } from './defaults.js' import { mappedDatasetsReducer } from '../reducers/mappedDatasetsReducer.js' import { datasetRegistry } from '../registry/datasetRegistry.js' +import { setMenuState } from '../registry/isVisibleWhen.js' +import { buildMenuState } from '../reducers/menuStateReducer.js' +import { datasetsToMenu } from '../reducers/datasetsToMenu.js' export const initialiseDatasets = ({ adapter, @@ -28,7 +31,10 @@ export const initialiseDatasets = ({ datasetRegistry.attachCreateDataset(adapter.createDataset) } datasetRegistry.attach(mappedDatasets, orderedDatasets, mapStyle) - adapter.init(mapStyle).then(() => { + const menu = pluginConfig.menu || datasetsToMenu({ datasets: processedDatasets }) + setMenuState(buildMenuState(menu)) // Must be called before adapter.init so that menuState is set before any datasets are checked for visibility + + adapter.init().then(() => { datasetRegistry.forEachDataset(registryDataset => { if (!registryDataset.hasDynamicGeoJSON) { return @@ -43,6 +49,7 @@ export const initialiseDatasets = ({ }) adapter.attachDynamicSources(dynamicSources) // TODO - apply dynamic source defaults here, and include in mappedDatasets + dispatch({ type: 'SET_MENU', payload: { menu } }) dispatch({ type: 'SET_DATASETS', payload: { datasets: processedDatasets, mappedDatasets, orderedDatasets } }) eventBus.emit('datasets:ready') }) diff --git a/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js b/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js index 783028052..9b1005dc8 100644 --- a/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js +++ b/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js @@ -61,7 +61,7 @@ describe('initialiseDatasets', () => { it('calls adapter.init with mapStyle', () => { const args = makeArgs() initialiseDatasets(args) - expect(args.adapter.init).toHaveBeenCalledWith(args.mapStyle) + expect(args.adapter.init).toHaveBeenCalledWith() }) it('attaches mappedDatasets to datasetRegistry', () => { diff --git a/plugins/beta/datasets/src/reducers/menuStateReducer.js b/plugins/beta/datasets/src/reducers/menuStateReducer.js new file mode 100644 index 000000000..839e771be --- /dev/null +++ b/plugins/beta/datasets/src/reducers/menuStateReducer.js @@ -0,0 +1,9 @@ +export const buildMenuState = (menu) => { + const menuState = {} + menu.forEach(menuGroup => { + if (menuGroup.type === 'radio') { + menuState[menuGroup.id] = menuGroup.value || menuGroup.items?.[0].value + } + }) + return menuState +} diff --git a/plugins/beta/datasets/src/reducers/pluginState.js b/plugins/beta/datasets/src/reducers/pluginState.js index cee81ca7f..33f894a79 100755 --- a/plugins/beta/datasets/src/reducers/pluginState.js +++ b/plugins/beta/datasets/src/reducers/pluginState.js @@ -1,4 +1,5 @@ -import { datasetsToMenu, addDatasetToMenu, removeDatasetsFromMenu } from './datasetsToMenu.js' +import { addDatasetToMenu, removeDatasetsFromMenu } from './datasetsToMenu.js' +import { buildMenuState } from './menuStateReducer.js' import { mappedDatasetsReducer } from './mappedDatasetsReducer.js' import { logger } from '../../../../../src/services/logger.js' @@ -15,7 +16,9 @@ const initialState = { items: [], hasGroups: false }, - actionsArray: [] + actionsArray: [], + menu: [], + menuState: {} } const validateDatasetExists = (state, datasetId, prefix, suffix = 'not found') => { @@ -41,14 +44,31 @@ const setGlobalState = (state, payload) => { }) } +const updateMenuState = (state, payload) => { + return addAction('applyGlobalVisibility', [], { + ...state, + menuState: { ...state.menuState, ...payload } + }) +} + +const setMenu = (state, payload) => { + const { menu } = payload + // build the initial menuState for radios from the menu + const menuState = buildMenuState(menu) + return { + ...state, + menu, + menuState + } +} + const setDatasets = (state, payload) => { - const { datasets, mappedDatasets, orderedDatasets } = payload - const menu = payload.menu || datasetsToMenu({ datasets }) + const { mappedDatasets, orderedDatasets } = payload + return { ...state, mappedDatasets, - orderedDatasets, - menu + orderedDatasets } } @@ -93,6 +113,9 @@ const setDatasetVisibility = (state, payload) => { if (!validateDatasetExists(state, datasetId, 'setDatasetVisibility')) { return state } + if (state.mappedDatasets[datasetId].visible === visible) { + return state + } return addAction('applyDatasetVisibility', [datasetId, visible], { ...state, @@ -197,7 +220,9 @@ const actions = { HIDE_FEATURES: hideFeatures, SHOW_FEATURES: showFeatures, REMOVE_ADAPTER_ACTIONS: removeAdapterActions, - SET_GLOBAL_STATE: setGlobalState + SET_GLOBAL_STATE: setGlobalState, + SET_MENU: setMenu, + UPDATE_MENU_STATE: updateMenuState } export { diff --git a/plugins/beta/datasets/src/reducers/pluginState.test.js b/plugins/beta/datasets/src/reducers/pluginState.test.js index 9a608ef8d..c0d927530 100644 --- a/plugins/beta/datasets/src/reducers/pluginState.test.js +++ b/plugins/beta/datasets/src/reducers/pluginState.test.js @@ -54,13 +54,12 @@ describe('SET_DATASETS', () => { const payload = { datasets: [{ id: 'parks', label: 'Parks', showInMenu: true }], mappedDatasets: { parks: { id: 'parks', label: 'Parks' } }, - orderedDatasets: ['parks'], - menu: [{ id: 'parks', label: 'Parks', type: 'item' }] + orderedDatasets: ['parks'] } const result = actions.SET_DATASETS(state, payload) expect(result.mappedDatasets).toEqual(payload.mappedDatasets) expect(result.orderedDatasets).toEqual(payload.orderedDatasets) - expect(result.menu).toEqual(payload.menu) + expect(result.menu).toEqual([]) }) it('derives menu from datasets when menu is not in payload', () => { diff --git a/plugins/beta/datasets/src/registry/dataset.js b/plugins/beta/datasets/src/registry/dataset.js index a8bd74235..36d1122d9 100644 --- a/plugins/beta/datasets/src/registry/dataset.js +++ b/plugins/beta/datasets/src/registry/dataset.js @@ -1,4 +1,5 @@ import { datasetRegistry } from './datasetRegistry.js' +import { isVisibleWhen } from './isVisibleWhen.js' import { hasCustomVisualStyle } from '../initialise/defaults.js' import { hasPattern } from '../../../../../src/utils/patternUtils.js' import { DynamicGeoJson } from './dynamicGeoJson.js' @@ -96,9 +97,9 @@ export class Dataset { get esriStyleLayerId () { return this._datasetDefinition.esriStyleLayerId } get visibility () { - const visible = this.visible - if (visible && this.visibleWhen?.mapStyleId) { - return this.visibleWhen?.mapStyleId.includes(datasetRegistry.mapStyle.id) ? 'visible' : 'none' + const { visible, visibleWhen } = this + if (visible && visibleWhen) { + return isVisibleWhen(visibleWhen) ? 'visible' : 'none' } return visible ? 'visible' : 'none' } diff --git a/plugins/beta/datasets/src/registry/isVisibleWhen.js b/plugins/beta/datasets/src/registry/isVisibleWhen.js new file mode 100644 index 000000000..b2fd1cb97 --- /dev/null +++ b/plugins/beta/datasets/src/registry/isVisibleWhen.js @@ -0,0 +1,55 @@ +import { datasetRegistry } from './datasetRegistry.js' + +let _menuState = {} +export const setMenuState = (menuState) => (_menuState = menuState) + +const _isVisibleWhenMenuCheck = (menuVisibleWhen) => { + for (const [key, valueArray] of Object.entries(menuVisibleWhen)) { + const menuValue = _menuState[key] + if (!valueArray.includes(menuValue)) { + return false + } + } + return true +} + +const _isVisibleWhenMapStyleCheck = (visibleWhenValue) => { + const mapStylesArray = Array.isArray(visibleWhenValue) ? visibleWhenValue : [visibleWhenValue] + return mapStylesArray.includes(_getMapStyleId()) +} + +// exported so it can be mocked and overridden when testing +export const _getMapStyleId = () => datasetRegistry.mapStyle.id + +/** + * receives a visibleWhen boolean or object + * and returns a boolean indicating whether the dataset should be visible + * if visibleWhen is undefined, it returns true + * if visibleWhen is a boolean, it returns that boolean + * if visibleWhen is an object, it checks the properties of the object, against the relevant pluginState properties, + * and returns true if all properties are satisfied: + * @param {boolean|object} visibleWhen - the visibleWhen property of a dataset + * @returns {boolean} - true if the dataset should be visible, false otherwise + */ +export const isVisibleWhen = (visibleWhen) => { + if (visibleWhen === undefined || visibleWhen === null) { + return true + } + if (typeof visibleWhen === 'boolean') { + return visibleWhen + } + if (typeof visibleWhen === 'object') { + // check each property of the visibleWhen object against the relevant pluginState property + for (const [visibleWhenKey, visibleWhenValue] of Object.entries(visibleWhen)) { + if (visibleWhenKey === 'mapStyleId' && !_isVisibleWhenMapStyleCheck(visibleWhenValue)) { + return false + } + if (visibleWhenKey === 'menu' && !_isVisibleWhenMenuCheck(visibleWhenValue)) { + return false + } + } + return true + } + // Fallback to true if visibleWhen is incorrectly configured + return true +} diff --git a/plugins/beta/datasets/src/registry/isVisibleWhen.test.js b/plugins/beta/datasets/src/registry/isVisibleWhen.test.js new file mode 100644 index 000000000..8d35bdd02 --- /dev/null +++ b/plugins/beta/datasets/src/registry/isVisibleWhen.test.js @@ -0,0 +1,67 @@ +import { isVisibleWhen, setMenuState } from './isVisibleWhen.js' +import { datasetRegistry } from './datasetRegistry.js' + +describe('isVisibleWhen', () => { + beforeEach(() => { + // reset menu state before each test + setMenuState({}) + }) + + it('returns true if visibleWhen is undefined', () => { + expect(isVisibleWhen(undefined)).toBe(true) + }) + + it('returns true if visibleWhen is null', () => { + expect(isVisibleWhen(null)).toBe(true) + }) + + it('returns the boolean value if visibleWhen is a boolean', () => { + expect(isVisibleWhen(true)).toBe(true) + expect(isVisibleWhen(false)).toBe(false) + }) + + it('returns true if all properties of visibleWhen object are satisfied', () => { + setMenuState({ datasets: 'floodZones', timeframe: 'climateChange' }) + const visibleWhen = { menu: { datasets: ['floodZones'], timeframe: ['climateChange'] } } + expect(isVisibleWhen(visibleWhen)).toBe(true) + }) + + it('returns false if any property of visibleWhen object is not satisfied', () => { + setMenuState({ datasets: 'floodZones', timeframe: 'climateChange' }) + const visibleWhen = { menu: { datasets: ['floodZones'], timeframe: ['presentDay'] } } + expect(isVisibleWhen(visibleWhen)).toBe(false) + }) + + it('returns true if mapStyleId is satisfied', () => { + const mapStyleId = 'outdoor' + datasetRegistry.attach([], [], { id: mapStyleId }) + const visibleWhen = { mapStyleId } + expect(isVisibleWhen(visibleWhen)).toBe(true) + }) + + it('returns false if mapStyleId is not satisfied', () => { + const mapStyleId = 'outdoor' + datasetRegistry.attach([], [], { id: mapStyleId }) + const visibleWhen = { mapStyleId: 'dark' } + expect(isVisibleWhen(visibleWhen)).toBe(false) + }) + + it('returns true if mapStyleId is an array and one of the values is satisfied', () => { + const mapStyleId = 'outdoor' + datasetRegistry.attach([], [], { id: mapStyleId }) + const visibleWhen = { mapStyleId: ['dark', mapStyleId] } + expect(isVisibleWhen(visibleWhen)).toBe(true) + }) + + it('returns false if mapStyleId is an array and none of the values are satisfied', () => { + const mapStyleId = 'outdoor' + datasetRegistry.attach([], [], { id: mapStyleId }) + const visibleWhen = { mapStyleId: ['dark', 'black-and-white'] } + expect(isVisibleWhen(visibleWhen)).toBe(false) + }) + + it('returns true if visibleWhen is incorrectly configured', () => { + const visibleWhen = 'incorrectly-configured' + expect(isVisibleWhen(visibleWhen)).toBe(true) + }) +}) From 15ac98da7421c1d5465e38a650e554fa708d2296 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Tue, 14 Jul 2026 11:54:26 +0100 Subject: [PATCH 38/53] IM-401 moved plugins out of /beta --- .../{beta => }/datasets/src/adapters/esri/esriLayerAdapter.js | 0 .../datasets/src/adapters/esri/esriLayerAdapter.test.js | 0 .../datasets/src/adapters/esri/registry/esriDataset.js | 0 .../datasets/src/adapters/esri/registry/esriDataset.test.js | 0 plugins/{beta => }/datasets/src/adapters/layerAdapter.js | 0 plugins/{beta => }/datasets/src/adapters/loadLayerAdapter.js | 0 .../{beta => }/datasets/src/adapters/maplibre/layerBuilders.js | 0 .../datasets/src/adapters/maplibre/layerBuilders.test.js | 0 .../datasets/src/adapters/maplibre/maplibreLayerAdapter.js | 0 .../datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js | 0 .../datasets/src/adapters/maplibre/registry/mapLibreDataset.js | 0 .../src/adapters/maplibre/registry/mapLibreDataset.test.js | 0 plugins/{beta => }/datasets/src/api/addDataset.js | 0 plugins/{beta => }/datasets/src/api/addDataset.test.js | 0 plugins/{beta => }/datasets/src/api/getOpacity.js | 0 plugins/{beta => }/datasets/src/api/getOpacity.test.js | 0 plugins/{beta => }/datasets/src/api/getStyle.js | 0 plugins/{beta => }/datasets/src/api/getStyle.test.js | 0 plugins/{beta => }/datasets/src/api/removeDataset.js | 0 plugins/{beta => }/datasets/src/api/removeDataset.test.js | 0 plugins/{beta => }/datasets/src/api/setData.js | 0 plugins/{beta => }/datasets/src/api/setData.test.js | 0 plugins/{beta => }/datasets/src/api/setDatasetVisibility.js | 0 .../{beta => }/datasets/src/api/setDatasetVisibility.test.js | 0 plugins/{beta => }/datasets/src/api/setFeatureVisibility.js | 0 .../{beta => }/datasets/src/api/setFeatureVisibility.test.js | 0 plugins/{beta => }/datasets/src/api/setGlobals.js | 0 plugins/{beta => }/datasets/src/api/setGlobals.test.js | 0 plugins/{beta => }/datasets/src/api/setOpacity.js | 0 plugins/{beta => }/datasets/src/api/setOpacity.test.js | 0 plugins/{beta => }/datasets/src/api/setStyle.js | 0 plugins/{beta => }/datasets/src/api/setStyle.test.js | 0 plugins/{beta => }/datasets/src/components/Key/EmptyKey.jsx | 0 .../{beta => }/datasets/src/components/Key/EmptyKey.test.jsx | 0 plugins/{beta => }/datasets/src/components/Key/Key.jsx | 0 plugins/{beta => }/datasets/src/components/Key/Key.module.scss | 0 plugins/{beta => }/datasets/src/components/Key/KeyGroupItem.jsx | 0 plugins/{beta => }/datasets/src/components/Key/KeyItem.jsx | 0 plugins/{beta => }/datasets/src/components/Key/KeySvg.jsx | 0 plugins/{beta => }/datasets/src/components/Key/KeySvg.test.jsx | 0 plugins/{beta => }/datasets/src/components/Key/KeySvgLine.jsx | 0 .../{beta => }/datasets/src/components/Key/KeySvgLine.test.jsx | 0 .../{beta => }/datasets/src/components/Key/KeySvgPattern.jsx | 0 .../datasets/src/components/Key/KeySvgPattern.test.jsx | 0 plugins/{beta => }/datasets/src/components/Key/KeySvgRect.jsx | 0 .../{beta => }/datasets/src/components/Key/KeySvgRect.test.jsx | 0 plugins/{beta => }/datasets/src/components/Key/KeySvgSymbol.jsx | 0 .../datasets/src/components/Key/KeySvgSymbol.test.jsx | 0 plugins/{beta => }/datasets/src/components/Key/svgProperties.js | 0 .../datasets/src/components/Key/svgProperties.test.js | 0 .../datasets/src/components/LayersMenu/Layers.module.scss | 0 .../datasets/src/components/LayersMenu/LayersMenu.jsx | 0 .../datasets/src/components/LayersMenu/LayersMenu.test.jsx | 2 +- .../datasets/src/components/LayersMenu/LayersMenuCheckbox.jsx | 0 .../src/components/LayersMenu/LayersMenuCheckbox.test.jsx | 2 +- .../src/components/LayersMenu/LayersMenuGroupWrapper.jsx | 0 .../src/components/LayersMenu/LayersMenuGroupWrapper.test.jsx | 0 .../datasets/src/components/LayersMenu/LayersMenuRadio.jsx | 0 .../datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx | 2 +- .../src/components/LayersMenu/LayersRadioGroupWrapper.jsx | 0 .../src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx | 2 +- plugins/{beta => }/datasets/src/datasets.scss | 0 plugins/{beta => }/datasets/src/fetch/createDynamicSource.js | 0 plugins/{beta => }/datasets/src/fetch/fetchGeoJSON.js | 0 plugins/{beta => }/datasets/src/index.js | 0 plugins/{beta => }/datasets/src/initialise/DatasetsInit.jsx | 0 plugins/{beta => }/datasets/src/initialise/defaults.js | 0 plugins/{beta => }/datasets/src/initialise/defaults.test.js | 0 .../{beta => }/datasets/src/initialise/initialiseDatasets.js | 0 .../datasets/src/initialise/initialiseDatasets.test.js | 0 plugins/{beta => }/datasets/src/manifest.js | 0 .../{beta => }/datasets/src/reducers/__data__/demoDatasets.js | 0 .../{beta => }/datasets/src/reducers/__data__/esriDatasets.js | 0 plugins/{beta => }/datasets/src/reducers/datasetsToMenu.js | 0 plugins/{beta => }/datasets/src/reducers/datasetsToMenu.test.js | 0 .../{beta => }/datasets/src/reducers/mappedDatasetsReducer.js | 0 .../datasets/src/reducers/mappedDatasetsReducer.test.js | 0 plugins/{beta => }/datasets/src/reducers/menuStateReducer.js | 0 plugins/{beta => }/datasets/src/reducers/pluginState.js | 0 plugins/{beta => }/datasets/src/reducers/pluginState.test.js | 0 .../datasets/src/registry/__mocks__/datasetRegistry.js | 0 plugins/{beta => }/datasets/src/registry/createDataset.js | 0 plugins/{beta => }/datasets/src/registry/dataset.js | 2 +- plugins/{beta => }/datasets/src/registry/dataset.test.js | 0 .../{beta => }/datasets/src/registry/datasetDefinitionCache.js | 0 plugins/{beta => }/datasets/src/registry/datasetRegistry.js | 0 .../{beta => }/datasets/src/registry/datasetRegistry.test.js | 0 plugins/{beta => }/datasets/src/registry/dynamicGeoJson.js | 0 plugins/{beta => }/datasets/src/registry/globalDataset.js | 0 plugins/{beta => }/datasets/src/registry/isVisibleWhen.js | 0 plugins/{beta => }/datasets/src/registry/isVisibleWhen.test.js | 0 plugins/{beta => }/datasets/src/utils/bbox.js | 0 plugins/{beta => }/datasets/src/utils/debounce.js | 0 93 files changed, 5 insertions(+), 5 deletions(-) rename plugins/{beta => }/datasets/src/adapters/esri/esriLayerAdapter.js (100%) rename plugins/{beta => }/datasets/src/adapters/esri/esriLayerAdapter.test.js (100%) rename plugins/{beta => }/datasets/src/adapters/esri/registry/esriDataset.js (100%) rename plugins/{beta => }/datasets/src/adapters/esri/registry/esriDataset.test.js (100%) rename plugins/{beta => }/datasets/src/adapters/layerAdapter.js (100%) rename plugins/{beta => }/datasets/src/adapters/loadLayerAdapter.js (100%) rename plugins/{beta => }/datasets/src/adapters/maplibre/layerBuilders.js (100%) rename plugins/{beta => }/datasets/src/adapters/maplibre/layerBuilders.test.js (100%) rename plugins/{beta => }/datasets/src/adapters/maplibre/maplibreLayerAdapter.js (100%) rename plugins/{beta => }/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js (100%) rename plugins/{beta => }/datasets/src/adapters/maplibre/registry/mapLibreDataset.js (100%) rename plugins/{beta => }/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js (100%) rename plugins/{beta => }/datasets/src/api/addDataset.js (100%) rename plugins/{beta => }/datasets/src/api/addDataset.test.js (100%) rename plugins/{beta => }/datasets/src/api/getOpacity.js (100%) rename plugins/{beta => }/datasets/src/api/getOpacity.test.js (100%) rename plugins/{beta => }/datasets/src/api/getStyle.js (100%) rename plugins/{beta => }/datasets/src/api/getStyle.test.js (100%) rename plugins/{beta => }/datasets/src/api/removeDataset.js (100%) rename plugins/{beta => }/datasets/src/api/removeDataset.test.js (100%) rename plugins/{beta => }/datasets/src/api/setData.js (100%) rename plugins/{beta => }/datasets/src/api/setData.test.js (100%) rename plugins/{beta => }/datasets/src/api/setDatasetVisibility.js (100%) rename plugins/{beta => }/datasets/src/api/setDatasetVisibility.test.js (100%) rename plugins/{beta => }/datasets/src/api/setFeatureVisibility.js (100%) rename plugins/{beta => }/datasets/src/api/setFeatureVisibility.test.js (100%) rename plugins/{beta => }/datasets/src/api/setGlobals.js (100%) rename plugins/{beta => }/datasets/src/api/setGlobals.test.js (100%) rename plugins/{beta => }/datasets/src/api/setOpacity.js (100%) rename plugins/{beta => }/datasets/src/api/setOpacity.test.js (100%) rename plugins/{beta => }/datasets/src/api/setStyle.js (100%) rename plugins/{beta => }/datasets/src/api/setStyle.test.js (100%) rename plugins/{beta => }/datasets/src/components/Key/EmptyKey.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/EmptyKey.test.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/Key.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/Key.module.scss (100%) rename plugins/{beta => }/datasets/src/components/Key/KeyGroupItem.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/KeyItem.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/KeySvg.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/KeySvg.test.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/KeySvgLine.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/KeySvgLine.test.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/KeySvgPattern.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/KeySvgPattern.test.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/KeySvgRect.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/KeySvgRect.test.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/KeySvgSymbol.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/KeySvgSymbol.test.jsx (100%) rename plugins/{beta => }/datasets/src/components/Key/svgProperties.js (100%) rename plugins/{beta => }/datasets/src/components/Key/svgProperties.test.js (100%) rename plugins/{beta => }/datasets/src/components/LayersMenu/Layers.module.scss (100%) rename plugins/{beta => }/datasets/src/components/LayersMenu/LayersMenu.jsx (100%) rename plugins/{beta => }/datasets/src/components/LayersMenu/LayersMenu.test.jsx (99%) rename plugins/{beta => }/datasets/src/components/LayersMenu/LayersMenuCheckbox.jsx (100%) rename plugins/{beta => }/datasets/src/components/LayersMenu/LayersMenuCheckbox.test.jsx (98%) rename plugins/{beta => }/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx (100%) rename plugins/{beta => }/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.test.jsx (100%) rename plugins/{beta => }/datasets/src/components/LayersMenu/LayersMenuRadio.jsx (100%) rename plugins/{beta => }/datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx (98%) rename plugins/{beta => }/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx (100%) rename plugins/{beta => }/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx (99%) rename plugins/{beta => }/datasets/src/datasets.scss (100%) rename plugins/{beta => }/datasets/src/fetch/createDynamicSource.js (100%) rename plugins/{beta => }/datasets/src/fetch/fetchGeoJSON.js (100%) rename plugins/{beta => }/datasets/src/index.js (100%) rename plugins/{beta => }/datasets/src/initialise/DatasetsInit.jsx (100%) rename plugins/{beta => }/datasets/src/initialise/defaults.js (100%) rename plugins/{beta => }/datasets/src/initialise/defaults.test.js (100%) rename plugins/{beta => }/datasets/src/initialise/initialiseDatasets.js (100%) rename plugins/{beta => }/datasets/src/initialise/initialiseDatasets.test.js (100%) rename plugins/{beta => }/datasets/src/manifest.js (100%) rename plugins/{beta => }/datasets/src/reducers/__data__/demoDatasets.js (100%) rename plugins/{beta => }/datasets/src/reducers/__data__/esriDatasets.js (100%) rename plugins/{beta => }/datasets/src/reducers/datasetsToMenu.js (100%) rename plugins/{beta => }/datasets/src/reducers/datasetsToMenu.test.js (100%) rename plugins/{beta => }/datasets/src/reducers/mappedDatasetsReducer.js (100%) rename plugins/{beta => }/datasets/src/reducers/mappedDatasetsReducer.test.js (100%) rename plugins/{beta => }/datasets/src/reducers/menuStateReducer.js (100%) rename plugins/{beta => }/datasets/src/reducers/pluginState.js (100%) rename plugins/{beta => }/datasets/src/reducers/pluginState.test.js (100%) rename plugins/{beta => }/datasets/src/registry/__mocks__/datasetRegistry.js (100%) rename plugins/{beta => }/datasets/src/registry/createDataset.js (100%) rename plugins/{beta => }/datasets/src/registry/dataset.js (98%) rename plugins/{beta => }/datasets/src/registry/dataset.test.js (100%) rename plugins/{beta => }/datasets/src/registry/datasetDefinitionCache.js (100%) rename plugins/{beta => }/datasets/src/registry/datasetRegistry.js (100%) rename plugins/{beta => }/datasets/src/registry/datasetRegistry.test.js (100%) rename plugins/{beta => }/datasets/src/registry/dynamicGeoJson.js (100%) rename plugins/{beta => }/datasets/src/registry/globalDataset.js (100%) rename plugins/{beta => }/datasets/src/registry/isVisibleWhen.js (100%) rename plugins/{beta => }/datasets/src/registry/isVisibleWhen.test.js (100%) rename plugins/{beta => }/datasets/src/utils/bbox.js (100%) rename plugins/{beta => }/datasets/src/utils/debounce.js (100%) diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js similarity index 100% rename from plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js rename to plugins/datasets/src/adapters/esri/esriLayerAdapter.js diff --git a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js b/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js similarity index 100% rename from plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.test.js rename to plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js diff --git a/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.js b/plugins/datasets/src/adapters/esri/registry/esriDataset.js similarity index 100% rename from plugins/beta/datasets/src/adapters/esri/registry/esriDataset.js rename to plugins/datasets/src/adapters/esri/registry/esriDataset.js diff --git a/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.test.js b/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js similarity index 100% rename from plugins/beta/datasets/src/adapters/esri/registry/esriDataset.test.js rename to plugins/datasets/src/adapters/esri/registry/esriDataset.test.js diff --git a/plugins/beta/datasets/src/adapters/layerAdapter.js b/plugins/datasets/src/adapters/layerAdapter.js similarity index 100% rename from plugins/beta/datasets/src/adapters/layerAdapter.js rename to plugins/datasets/src/adapters/layerAdapter.js diff --git a/plugins/beta/datasets/src/adapters/loadLayerAdapter.js b/plugins/datasets/src/adapters/loadLayerAdapter.js similarity index 100% rename from plugins/beta/datasets/src/adapters/loadLayerAdapter.js rename to plugins/datasets/src/adapters/loadLayerAdapter.js diff --git a/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js b/plugins/datasets/src/adapters/maplibre/layerBuilders.js similarity index 100% rename from plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js rename to plugins/datasets/src/adapters/maplibre/layerBuilders.js diff --git a/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.test.js b/plugins/datasets/src/adapters/maplibre/layerBuilders.test.js similarity index 100% rename from plugins/beta/datasets/src/adapters/maplibre/layerBuilders.test.js rename to plugins/datasets/src/adapters/maplibre/layerBuilders.test.js diff --git a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js b/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js similarity index 100% rename from plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js rename to plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js diff --git a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js b/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js similarity index 100% rename from plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js rename to plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js diff --git a/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.js b/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.js similarity index 100% rename from plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.js rename to plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.js diff --git a/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js b/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js similarity index 100% rename from plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js rename to plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js diff --git a/plugins/beta/datasets/src/api/addDataset.js b/plugins/datasets/src/api/addDataset.js similarity index 100% rename from plugins/beta/datasets/src/api/addDataset.js rename to plugins/datasets/src/api/addDataset.js diff --git a/plugins/beta/datasets/src/api/addDataset.test.js b/plugins/datasets/src/api/addDataset.test.js similarity index 100% rename from plugins/beta/datasets/src/api/addDataset.test.js rename to plugins/datasets/src/api/addDataset.test.js diff --git a/plugins/beta/datasets/src/api/getOpacity.js b/plugins/datasets/src/api/getOpacity.js similarity index 100% rename from plugins/beta/datasets/src/api/getOpacity.js rename to plugins/datasets/src/api/getOpacity.js diff --git a/plugins/beta/datasets/src/api/getOpacity.test.js b/plugins/datasets/src/api/getOpacity.test.js similarity index 100% rename from plugins/beta/datasets/src/api/getOpacity.test.js rename to plugins/datasets/src/api/getOpacity.test.js diff --git a/plugins/beta/datasets/src/api/getStyle.js b/plugins/datasets/src/api/getStyle.js similarity index 100% rename from plugins/beta/datasets/src/api/getStyle.js rename to plugins/datasets/src/api/getStyle.js diff --git a/plugins/beta/datasets/src/api/getStyle.test.js b/plugins/datasets/src/api/getStyle.test.js similarity index 100% rename from plugins/beta/datasets/src/api/getStyle.test.js rename to plugins/datasets/src/api/getStyle.test.js diff --git a/plugins/beta/datasets/src/api/removeDataset.js b/plugins/datasets/src/api/removeDataset.js similarity index 100% rename from plugins/beta/datasets/src/api/removeDataset.js rename to plugins/datasets/src/api/removeDataset.js diff --git a/plugins/beta/datasets/src/api/removeDataset.test.js b/plugins/datasets/src/api/removeDataset.test.js similarity index 100% rename from plugins/beta/datasets/src/api/removeDataset.test.js rename to plugins/datasets/src/api/removeDataset.test.js diff --git a/plugins/beta/datasets/src/api/setData.js b/plugins/datasets/src/api/setData.js similarity index 100% rename from plugins/beta/datasets/src/api/setData.js rename to plugins/datasets/src/api/setData.js diff --git a/plugins/beta/datasets/src/api/setData.test.js b/plugins/datasets/src/api/setData.test.js similarity index 100% rename from plugins/beta/datasets/src/api/setData.test.js rename to plugins/datasets/src/api/setData.test.js diff --git a/plugins/beta/datasets/src/api/setDatasetVisibility.js b/plugins/datasets/src/api/setDatasetVisibility.js similarity index 100% rename from plugins/beta/datasets/src/api/setDatasetVisibility.js rename to plugins/datasets/src/api/setDatasetVisibility.js diff --git a/plugins/beta/datasets/src/api/setDatasetVisibility.test.js b/plugins/datasets/src/api/setDatasetVisibility.test.js similarity index 100% rename from plugins/beta/datasets/src/api/setDatasetVisibility.test.js rename to plugins/datasets/src/api/setDatasetVisibility.test.js diff --git a/plugins/beta/datasets/src/api/setFeatureVisibility.js b/plugins/datasets/src/api/setFeatureVisibility.js similarity index 100% rename from plugins/beta/datasets/src/api/setFeatureVisibility.js rename to plugins/datasets/src/api/setFeatureVisibility.js diff --git a/plugins/beta/datasets/src/api/setFeatureVisibility.test.js b/plugins/datasets/src/api/setFeatureVisibility.test.js similarity index 100% rename from plugins/beta/datasets/src/api/setFeatureVisibility.test.js rename to plugins/datasets/src/api/setFeatureVisibility.test.js diff --git a/plugins/beta/datasets/src/api/setGlobals.js b/plugins/datasets/src/api/setGlobals.js similarity index 100% rename from plugins/beta/datasets/src/api/setGlobals.js rename to plugins/datasets/src/api/setGlobals.js diff --git a/plugins/beta/datasets/src/api/setGlobals.test.js b/plugins/datasets/src/api/setGlobals.test.js similarity index 100% rename from plugins/beta/datasets/src/api/setGlobals.test.js rename to plugins/datasets/src/api/setGlobals.test.js diff --git a/plugins/beta/datasets/src/api/setOpacity.js b/plugins/datasets/src/api/setOpacity.js similarity index 100% rename from plugins/beta/datasets/src/api/setOpacity.js rename to plugins/datasets/src/api/setOpacity.js diff --git a/plugins/beta/datasets/src/api/setOpacity.test.js b/plugins/datasets/src/api/setOpacity.test.js similarity index 100% rename from plugins/beta/datasets/src/api/setOpacity.test.js rename to plugins/datasets/src/api/setOpacity.test.js diff --git a/plugins/beta/datasets/src/api/setStyle.js b/plugins/datasets/src/api/setStyle.js similarity index 100% rename from plugins/beta/datasets/src/api/setStyle.js rename to plugins/datasets/src/api/setStyle.js diff --git a/plugins/beta/datasets/src/api/setStyle.test.js b/plugins/datasets/src/api/setStyle.test.js similarity index 100% rename from plugins/beta/datasets/src/api/setStyle.test.js rename to plugins/datasets/src/api/setStyle.test.js diff --git a/plugins/beta/datasets/src/components/Key/EmptyKey.jsx b/plugins/datasets/src/components/Key/EmptyKey.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/EmptyKey.jsx rename to plugins/datasets/src/components/Key/EmptyKey.jsx diff --git a/plugins/beta/datasets/src/components/Key/EmptyKey.test.jsx b/plugins/datasets/src/components/Key/EmptyKey.test.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/EmptyKey.test.jsx rename to plugins/datasets/src/components/Key/EmptyKey.test.jsx diff --git a/plugins/beta/datasets/src/components/Key/Key.jsx b/plugins/datasets/src/components/Key/Key.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/Key.jsx rename to plugins/datasets/src/components/Key/Key.jsx diff --git a/plugins/beta/datasets/src/components/Key/Key.module.scss b/plugins/datasets/src/components/Key/Key.module.scss similarity index 100% rename from plugins/beta/datasets/src/components/Key/Key.module.scss rename to plugins/datasets/src/components/Key/Key.module.scss diff --git a/plugins/beta/datasets/src/components/Key/KeyGroupItem.jsx b/plugins/datasets/src/components/Key/KeyGroupItem.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/KeyGroupItem.jsx rename to plugins/datasets/src/components/Key/KeyGroupItem.jsx diff --git a/plugins/beta/datasets/src/components/Key/KeyItem.jsx b/plugins/datasets/src/components/Key/KeyItem.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/KeyItem.jsx rename to plugins/datasets/src/components/Key/KeyItem.jsx diff --git a/plugins/beta/datasets/src/components/Key/KeySvg.jsx b/plugins/datasets/src/components/Key/KeySvg.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/KeySvg.jsx rename to plugins/datasets/src/components/Key/KeySvg.jsx diff --git a/plugins/beta/datasets/src/components/Key/KeySvg.test.jsx b/plugins/datasets/src/components/Key/KeySvg.test.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/KeySvg.test.jsx rename to plugins/datasets/src/components/Key/KeySvg.test.jsx diff --git a/plugins/beta/datasets/src/components/Key/KeySvgLine.jsx b/plugins/datasets/src/components/Key/KeySvgLine.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/KeySvgLine.jsx rename to plugins/datasets/src/components/Key/KeySvgLine.jsx diff --git a/plugins/beta/datasets/src/components/Key/KeySvgLine.test.jsx b/plugins/datasets/src/components/Key/KeySvgLine.test.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/KeySvgLine.test.jsx rename to plugins/datasets/src/components/Key/KeySvgLine.test.jsx diff --git a/plugins/beta/datasets/src/components/Key/KeySvgPattern.jsx b/plugins/datasets/src/components/Key/KeySvgPattern.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/KeySvgPattern.jsx rename to plugins/datasets/src/components/Key/KeySvgPattern.jsx diff --git a/plugins/beta/datasets/src/components/Key/KeySvgPattern.test.jsx b/plugins/datasets/src/components/Key/KeySvgPattern.test.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/KeySvgPattern.test.jsx rename to plugins/datasets/src/components/Key/KeySvgPattern.test.jsx diff --git a/plugins/beta/datasets/src/components/Key/KeySvgRect.jsx b/plugins/datasets/src/components/Key/KeySvgRect.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/KeySvgRect.jsx rename to plugins/datasets/src/components/Key/KeySvgRect.jsx diff --git a/plugins/beta/datasets/src/components/Key/KeySvgRect.test.jsx b/plugins/datasets/src/components/Key/KeySvgRect.test.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/KeySvgRect.test.jsx rename to plugins/datasets/src/components/Key/KeySvgRect.test.jsx diff --git a/plugins/beta/datasets/src/components/Key/KeySvgSymbol.jsx b/plugins/datasets/src/components/Key/KeySvgSymbol.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/KeySvgSymbol.jsx rename to plugins/datasets/src/components/Key/KeySvgSymbol.jsx diff --git a/plugins/beta/datasets/src/components/Key/KeySvgSymbol.test.jsx b/plugins/datasets/src/components/Key/KeySvgSymbol.test.jsx similarity index 100% rename from plugins/beta/datasets/src/components/Key/KeySvgSymbol.test.jsx rename to plugins/datasets/src/components/Key/KeySvgSymbol.test.jsx diff --git a/plugins/beta/datasets/src/components/Key/svgProperties.js b/plugins/datasets/src/components/Key/svgProperties.js similarity index 100% rename from plugins/beta/datasets/src/components/Key/svgProperties.js rename to plugins/datasets/src/components/Key/svgProperties.js diff --git a/plugins/beta/datasets/src/components/Key/svgProperties.test.js b/plugins/datasets/src/components/Key/svgProperties.test.js similarity index 100% rename from plugins/beta/datasets/src/components/Key/svgProperties.test.js rename to plugins/datasets/src/components/Key/svgProperties.test.js diff --git a/plugins/beta/datasets/src/components/LayersMenu/Layers.module.scss b/plugins/datasets/src/components/LayersMenu/Layers.module.scss similarity index 100% rename from plugins/beta/datasets/src/components/LayersMenu/Layers.module.scss rename to plugins/datasets/src/components/LayersMenu/Layers.module.scss diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx b/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx similarity index 100% rename from plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx rename to plugins/datasets/src/components/LayersMenu/LayersMenu.jsx diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.test.jsx b/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx similarity index 99% rename from plugins/beta/datasets/src/components/LayersMenu/LayersMenu.test.jsx rename to plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx index 0799c32b8..ad5c1169f 100644 --- a/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.test.jsx +++ b/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx @@ -1,5 +1,5 @@ import { render } from '@testing-library/react' -import { LayersMenu } from './LayersMenu' +import { LayersMenu } from './LayersMenu.jsx' import { setDatasetVisibility } from '../../api/setDatasetVisibility.js' jest.mock('../../api/setDatasetVisibility.js', () => ({ diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.jsx b/plugins/datasets/src/components/LayersMenu/LayersMenuCheckbox.jsx similarity index 100% rename from plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.jsx rename to plugins/datasets/src/components/LayersMenu/LayersMenuCheckbox.jsx diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.test.jsx b/plugins/datasets/src/components/LayersMenu/LayersMenuCheckbox.test.jsx similarity index 98% rename from plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.test.jsx rename to plugins/datasets/src/components/LayersMenu/LayersMenuCheckbox.test.jsx index c0c7841eb..6f26b0be7 100644 --- a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.test.jsx +++ b/plugins/datasets/src/components/LayersMenu/LayersMenuCheckbox.test.jsx @@ -1,6 +1,6 @@ import { render, screen, act } from '@testing-library/react' import { datasetRegistry } from '../../registry/datasetRegistry.js' -import { LayersMenuCheckbox } from './LayersMenuCheckbox' +import { LayersMenuCheckbox } from './LayersMenuCheckbox.jsx' jest.mock('../../registry/datasetRegistry.js', () => ({ datasetRegistry: { diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx b/plugins/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx similarity index 100% rename from plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx rename to plugins/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.test.jsx b/plugins/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.test.jsx similarity index 100% rename from plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.test.jsx rename to plugins/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.test.jsx diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.jsx b/plugins/datasets/src/components/LayersMenu/LayersMenuRadio.jsx similarity index 100% rename from plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.jsx rename to plugins/datasets/src/components/LayersMenu/LayersMenuRadio.jsx diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx b/plugins/datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx similarity index 98% rename from plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx rename to plugins/datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx index 55b2c15af..fc62b4edd 100644 --- a/plugins/beta/datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx +++ b/plugins/datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx @@ -1,6 +1,6 @@ import { render, screen, act } from '@testing-library/react' import { isVisibleWhen } from '../../registry/isVisibleWhen.js' -import { LayersMenuRadio } from './LayersMenuRadio' +import { LayersMenuRadio } from './LayersMenuRadio.jsx' jest.mock('../../registry/isVisibleWhen.js', () => ({ isVisibleWhen: jest.fn() diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx b/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx similarity index 100% rename from plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx rename to plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx diff --git a/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx b/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx similarity index 99% rename from plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx rename to plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx index 24095d3e1..7e2004d3d 100644 --- a/plugins/beta/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx +++ b/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx @@ -1,6 +1,6 @@ import { render, screen, act } from '@testing-library/react' import { isVisibleWhen } from '../../registry/isVisibleWhen.js' -import { LayersRadioGroupWrapper } from './LayersRadioGroupWrapper' +import { LayersRadioGroupWrapper } from './LayersRadioGroupWrapper.jsx' jest.mock('../../registry/isVisibleWhen.js', () => ({ isVisibleWhen: jest.fn() diff --git a/plugins/beta/datasets/src/datasets.scss b/plugins/datasets/src/datasets.scss similarity index 100% rename from plugins/beta/datasets/src/datasets.scss rename to plugins/datasets/src/datasets.scss diff --git a/plugins/beta/datasets/src/fetch/createDynamicSource.js b/plugins/datasets/src/fetch/createDynamicSource.js similarity index 100% rename from plugins/beta/datasets/src/fetch/createDynamicSource.js rename to plugins/datasets/src/fetch/createDynamicSource.js diff --git a/plugins/beta/datasets/src/fetch/fetchGeoJSON.js b/plugins/datasets/src/fetch/fetchGeoJSON.js similarity index 100% rename from plugins/beta/datasets/src/fetch/fetchGeoJSON.js rename to plugins/datasets/src/fetch/fetchGeoJSON.js diff --git a/plugins/beta/datasets/src/index.js b/plugins/datasets/src/index.js similarity index 100% rename from plugins/beta/datasets/src/index.js rename to plugins/datasets/src/index.js diff --git a/plugins/beta/datasets/src/initialise/DatasetsInit.jsx b/plugins/datasets/src/initialise/DatasetsInit.jsx similarity index 100% rename from plugins/beta/datasets/src/initialise/DatasetsInit.jsx rename to plugins/datasets/src/initialise/DatasetsInit.jsx diff --git a/plugins/beta/datasets/src/initialise/defaults.js b/plugins/datasets/src/initialise/defaults.js similarity index 100% rename from plugins/beta/datasets/src/initialise/defaults.js rename to plugins/datasets/src/initialise/defaults.js diff --git a/plugins/beta/datasets/src/initialise/defaults.test.js b/plugins/datasets/src/initialise/defaults.test.js similarity index 100% rename from plugins/beta/datasets/src/initialise/defaults.test.js rename to plugins/datasets/src/initialise/defaults.test.js diff --git a/plugins/beta/datasets/src/initialise/initialiseDatasets.js b/plugins/datasets/src/initialise/initialiseDatasets.js similarity index 100% rename from plugins/beta/datasets/src/initialise/initialiseDatasets.js rename to plugins/datasets/src/initialise/initialiseDatasets.js diff --git a/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js b/plugins/datasets/src/initialise/initialiseDatasets.test.js similarity index 100% rename from plugins/beta/datasets/src/initialise/initialiseDatasets.test.js rename to plugins/datasets/src/initialise/initialiseDatasets.test.js diff --git a/plugins/beta/datasets/src/manifest.js b/plugins/datasets/src/manifest.js similarity index 100% rename from plugins/beta/datasets/src/manifest.js rename to plugins/datasets/src/manifest.js diff --git a/plugins/beta/datasets/src/reducers/__data__/demoDatasets.js b/plugins/datasets/src/reducers/__data__/demoDatasets.js similarity index 100% rename from plugins/beta/datasets/src/reducers/__data__/demoDatasets.js rename to plugins/datasets/src/reducers/__data__/demoDatasets.js diff --git a/plugins/beta/datasets/src/reducers/__data__/esriDatasets.js b/plugins/datasets/src/reducers/__data__/esriDatasets.js similarity index 100% rename from plugins/beta/datasets/src/reducers/__data__/esriDatasets.js rename to plugins/datasets/src/reducers/__data__/esriDatasets.js diff --git a/plugins/beta/datasets/src/reducers/datasetsToMenu.js b/plugins/datasets/src/reducers/datasetsToMenu.js similarity index 100% rename from plugins/beta/datasets/src/reducers/datasetsToMenu.js rename to plugins/datasets/src/reducers/datasetsToMenu.js diff --git a/plugins/beta/datasets/src/reducers/datasetsToMenu.test.js b/plugins/datasets/src/reducers/datasetsToMenu.test.js similarity index 100% rename from plugins/beta/datasets/src/reducers/datasetsToMenu.test.js rename to plugins/datasets/src/reducers/datasetsToMenu.test.js diff --git a/plugins/beta/datasets/src/reducers/mappedDatasetsReducer.js b/plugins/datasets/src/reducers/mappedDatasetsReducer.js similarity index 100% rename from plugins/beta/datasets/src/reducers/mappedDatasetsReducer.js rename to plugins/datasets/src/reducers/mappedDatasetsReducer.js diff --git a/plugins/beta/datasets/src/reducers/mappedDatasetsReducer.test.js b/plugins/datasets/src/reducers/mappedDatasetsReducer.test.js similarity index 100% rename from plugins/beta/datasets/src/reducers/mappedDatasetsReducer.test.js rename to plugins/datasets/src/reducers/mappedDatasetsReducer.test.js diff --git a/plugins/beta/datasets/src/reducers/menuStateReducer.js b/plugins/datasets/src/reducers/menuStateReducer.js similarity index 100% rename from plugins/beta/datasets/src/reducers/menuStateReducer.js rename to plugins/datasets/src/reducers/menuStateReducer.js diff --git a/plugins/beta/datasets/src/reducers/pluginState.js b/plugins/datasets/src/reducers/pluginState.js similarity index 100% rename from plugins/beta/datasets/src/reducers/pluginState.js rename to plugins/datasets/src/reducers/pluginState.js diff --git a/plugins/beta/datasets/src/reducers/pluginState.test.js b/plugins/datasets/src/reducers/pluginState.test.js similarity index 100% rename from plugins/beta/datasets/src/reducers/pluginState.test.js rename to plugins/datasets/src/reducers/pluginState.test.js diff --git a/plugins/beta/datasets/src/registry/__mocks__/datasetRegistry.js b/plugins/datasets/src/registry/__mocks__/datasetRegistry.js similarity index 100% rename from plugins/beta/datasets/src/registry/__mocks__/datasetRegistry.js rename to plugins/datasets/src/registry/__mocks__/datasetRegistry.js diff --git a/plugins/beta/datasets/src/registry/createDataset.js b/plugins/datasets/src/registry/createDataset.js similarity index 100% rename from plugins/beta/datasets/src/registry/createDataset.js rename to plugins/datasets/src/registry/createDataset.js diff --git a/plugins/beta/datasets/src/registry/dataset.js b/plugins/datasets/src/registry/dataset.js similarity index 98% rename from plugins/beta/datasets/src/registry/dataset.js rename to plugins/datasets/src/registry/dataset.js index 36d1122d9..9d14997d9 100644 --- a/plugins/beta/datasets/src/registry/dataset.js +++ b/plugins/datasets/src/registry/dataset.js @@ -1,7 +1,7 @@ import { datasetRegistry } from './datasetRegistry.js' import { isVisibleWhen } from './isVisibleWhen.js' import { hasCustomVisualStyle } from '../initialise/defaults.js' -import { hasPattern } from '../../../../../src/utils/patternUtils.js' +import { hasPattern } from '../../../../src/utils/patternUtils.js' import { DynamicGeoJson } from './dynamicGeoJson.js' import { calculateOpacity, getGlobalVisibility } from './globalDataset.js' diff --git a/plugins/beta/datasets/src/registry/dataset.test.js b/plugins/datasets/src/registry/dataset.test.js similarity index 100% rename from plugins/beta/datasets/src/registry/dataset.test.js rename to plugins/datasets/src/registry/dataset.test.js diff --git a/plugins/beta/datasets/src/registry/datasetDefinitionCache.js b/plugins/datasets/src/registry/datasetDefinitionCache.js similarity index 100% rename from plugins/beta/datasets/src/registry/datasetDefinitionCache.js rename to plugins/datasets/src/registry/datasetDefinitionCache.js diff --git a/plugins/beta/datasets/src/registry/datasetRegistry.js b/plugins/datasets/src/registry/datasetRegistry.js similarity index 100% rename from plugins/beta/datasets/src/registry/datasetRegistry.js rename to plugins/datasets/src/registry/datasetRegistry.js diff --git a/plugins/beta/datasets/src/registry/datasetRegistry.test.js b/plugins/datasets/src/registry/datasetRegistry.test.js similarity index 100% rename from plugins/beta/datasets/src/registry/datasetRegistry.test.js rename to plugins/datasets/src/registry/datasetRegistry.test.js diff --git a/plugins/beta/datasets/src/registry/dynamicGeoJson.js b/plugins/datasets/src/registry/dynamicGeoJson.js similarity index 100% rename from plugins/beta/datasets/src/registry/dynamicGeoJson.js rename to plugins/datasets/src/registry/dynamicGeoJson.js diff --git a/plugins/beta/datasets/src/registry/globalDataset.js b/plugins/datasets/src/registry/globalDataset.js similarity index 100% rename from plugins/beta/datasets/src/registry/globalDataset.js rename to plugins/datasets/src/registry/globalDataset.js diff --git a/plugins/beta/datasets/src/registry/isVisibleWhen.js b/plugins/datasets/src/registry/isVisibleWhen.js similarity index 100% rename from plugins/beta/datasets/src/registry/isVisibleWhen.js rename to plugins/datasets/src/registry/isVisibleWhen.js diff --git a/plugins/beta/datasets/src/registry/isVisibleWhen.test.js b/plugins/datasets/src/registry/isVisibleWhen.test.js similarity index 100% rename from plugins/beta/datasets/src/registry/isVisibleWhen.test.js rename to plugins/datasets/src/registry/isVisibleWhen.test.js diff --git a/plugins/beta/datasets/src/utils/bbox.js b/plugins/datasets/src/utils/bbox.js similarity index 100% rename from plugins/beta/datasets/src/utils/bbox.js rename to plugins/datasets/src/utils/bbox.js diff --git a/plugins/beta/datasets/src/utils/debounce.js b/plugins/datasets/src/utils/debounce.js similarity index 100% rename from plugins/beta/datasets/src/utils/debounce.js rename to plugins/datasets/src/utils/debounce.js From 6cd4409ece4d87d1e9681247c8198234f1e2aca8 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Tue, 14 Jul 2026 11:55:03 +0100 Subject: [PATCH 39/53] IM-401 ammended paths to and from plugins/datasets --- assets/templates/add-polygons.njk | 4 ++-- assets/templates/add-symbols.njk | 4 ++-- demo/DemoMapPolygons.js | 2 +- demo/DemoMapSymbols.js | 2 +- demo/js/draw.js | 2 +- demo/js/esm.js | 2 +- demo/js/esri-datasets.js | 2 +- demo/js/farming.js | 2 +- demo/js/gep.js | 2 +- demo/js/index.js | 4 ++-- demo/js/ml-datasets.js | 2 +- demo/umd.html | 4 ++-- docs/plugins/datasets.md | 6 +++--- govuk-prototype-kit.config.json | 2 +- jest.config.mjs | 2 +- package.json | 6 +++--- plugins/datasets/src/adapters/esri/esriLayerAdapter.js | 2 +- .../src/adapters/esri/esriLayerAdapter.test.js | 2 +- .../datasets/src/adapters/esri/registry/esriDataset.js | 2 +- .../datasets/src/adapters/maplibre/layerBuilders.js | 4 ++-- .../src/adapters/maplibre/maplibreLayerAdapter.test.js | 4 ++-- .../src/adapters/maplibre/registry/mapLibreDataset.js | 6 +++--- .../adapters/maplibre/registry/mapLibreDataset.test.js | 4 ++-- plugins/datasets/src/api/getOpacity.js | 2 +- plugins/datasets/src/api/getOpacity.test.js | 4 ++-- plugins/datasets/src/api/getStyle.js | 2 +- plugins/datasets/src/api/getStyle.test.js | 4 ++-- plugins/datasets/src/api/setData.js | 2 +- plugins/datasets/src/api/setData.test.js | 4 ++-- plugins/datasets/src/api/setFeatureVisibility.js | 2 +- plugins/datasets/src/api/setFeatureVisibility.test.js | 4 ++-- plugins/datasets/src/api/setGlobals.js | 2 +- plugins/datasets/src/api/setGlobals.test.js | 4 ++-- plugins/datasets/src/components/Key/KeyItem.jsx | 2 +- plugins/datasets/src/components/Key/KeySvg.test.jsx | 4 ++-- plugins/datasets/src/components/Key/KeySvgLine.jsx | 2 +- .../datasets/src/components/Key/KeySvgLine.test.jsx | 4 ++-- .../datasets/src/components/Key/KeySvgPattern.test.jsx | 2 +- plugins/datasets/src/components/Key/KeySvgRect.jsx | 2 +- .../datasets/src/components/Key/KeySvgRect.test.jsx | 4 ++-- plugins/datasets/src/components/Key/KeySvgSymbol.jsx | 2 +- .../datasets/src/components/Key/KeySvgSymbol.test.jsx | 4 ++-- plugins/datasets/src/initialise/DatasetsInit.jsx | 2 +- plugins/datasets/src/reducers/pluginState.js | 2 +- plugins/datasets/src/reducers/pluginState.test.js | 4 ++-- rollup.esm.mjs | 10 ++++++---- webpack.umd.mjs | 2 +- 47 files changed, 75 insertions(+), 73 deletions(-) diff --git a/assets/templates/add-polygons.njk b/assets/templates/add-polygons.njk index d8a1bed0e..a3aee3051 100644 --- a/assets/templates/add-polygons.njk +++ b/assets/templates/add-polygons.njk @@ -7,7 +7,7 @@ {% block head %} - + {% endblock %} {% block bodyStart %} @@ -39,7 +39,7 @@ - + - + - + diff --git a/docs/plugins/datasets.md b/docs/plugins/datasets.md index f546cbb2b..ec50cfc9a 100644 --- a/docs/plugins/datasets.md +++ b/docs/plugins/datasets.md @@ -33,10 +33,10 @@ const interactiveMap = new InteractiveMap({ ## UMD usage -Copy the entire `plugins/beta/datasets/dist/umd/` directory to `/your-assets-path/plugins/beta/datasets/umd/`. The plugin uses dynamic imports, so all files in the directory must be served from the same location. Then add the script tag: +Copy the entire `plugins/datasets/dist/umd/` directory to `/your-assets-path/plugins/datasets/umd/`. The plugin uses dynamic imports, so all files in the directory must be served from the same location. Then add the script tag: ```html - + ``` ```js @@ -68,7 +68,7 @@ const interactiveMap = new defra.InteractiveMap('map', { > [!NOTE] > **GOV.UK Prototype Kit** — skip the copy step. All files are served automatically. Use this path instead: > ```html -> +> > ``` ## Options diff --git a/govuk-prototype-kit.config.json b/govuk-prototype-kit.config.json index d648d9f3a..196bed9d6 100644 --- a/govuk-prototype-kit.config.json +++ b/govuk-prototype-kit.config.json @@ -62,7 +62,7 @@ "/dist/umd/", "/providers/maplibre/dist", "/providers/beta/open-names/dist", - "/plugins/beta/datasets/dist", + "/plugins/datasets/dist", "/plugins/interact/dist", "/plugins/beta/map-styles/dist", "/plugins/beta/scale-bar/dist", diff --git a/jest.config.mjs b/jest.config.mjs index 491ac260a..02f1cd4dd 100755 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -27,7 +27,7 @@ export default { '/coverage', '/demo', '/src/test-utils.js', - '/plugins/beta/datasets/', + '/plugins/datasets/', '/providers/beta/', '/plugins/beta/draw-es', '/plugins/beta/draw-ml', diff --git a/package.json b/package.json index 3c5a611fd..6d701137f 100755 --- a/package.json +++ b/package.json @@ -45,10 +45,10 @@ "require": "./plugins/interact/dist/umd/index.js" }, "./plugins/datasets": { - "import": "./plugins/beta/datasets/dist/esm/index.js" + "import": "./plugins/datasets/dist/esm/index.js" }, "./plugins/datasets/adapters/maplibre": { - "import": "./plugins/beta/datasets/dist/adapters/maplibre/esm/index.js" + "import": "./plugins/datasets/dist/adapters/maplibre/esm/index.js" }, "./plugins/map-styles": { "import": "./plugins/beta/map-styles/dist/esm/index.js", @@ -68,7 +68,7 @@ "./css": "./dist/css/index.css", "./plugins/draw-ml/css": "./plugins/beta/draw-ml/dist/css/index.css", "./plugins/scale-bar/css": "./plugins/beta/scale-bar/dist/css/index.css", - "./plugins/datasets/css": "./plugins/beta/datasets/dist/css/index.css", + "./plugins/datasets/css": "./plugins/datasets/dist/css/index.css", "./plugins/frame/css": "./plugins/beta/frame/dist/css/index.css", "./plugins/map-styles/css": "./plugins/beta/map-styles/dist/css/index.css", "./plugins/search/css": "./plugins/search/dist/css/index.css", diff --git a/plugins/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js index 781b214d5..3cc304b5e 100644 --- a/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js @@ -3,7 +3,7 @@ import GroupLayer from '@arcgis/core/layers/GroupLayer.js' import { LayerAdapter } from '../layerAdapter.js' import { datasetRegistry } from '../../registry/datasetRegistry.js' import { EsriDataset } from './registry/esriDataset.js' -import { logger } from '../../../../../../src/services/logger.js' +import { logger } from '../../../../../src/services/logger.js' export default class EsriLayerAdapter extends LayerAdapter { constructor (mapProvider, symbolRegistry, patternRegistry) { diff --git a/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js b/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js index 10bb557d9..9194185d8 100644 --- a/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +++ b/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js @@ -3,7 +3,7 @@ import EsriLayerAdapter from './esriLayerAdapter.js' import { datasetRegistry } from '../../registry/datasetRegistry.js' jest.mock('../../registry/datasetRegistry.js') -jest.mock('../../../../../../src/services/logger.js') +jest.mock('../../../../../src/services/logger.js') jest.mock('@arcgis/core/layers/VectorTileLayer.js', () => jest.fn().mockImplementation((opts = {}) => ({ diff --git a/plugins/datasets/src/adapters/esri/registry/esriDataset.js b/plugins/datasets/src/adapters/esri/registry/esriDataset.js index e34667166..322b4318f 100644 --- a/plugins/datasets/src/adapters/esri/registry/esriDataset.js +++ b/plugins/datasets/src/adapters/esri/registry/esriDataset.js @@ -1,6 +1,6 @@ import { Dataset } from '../../../registry/dataset.js' import { datasetRegistry } from '../../../registry/datasetRegistry.js' -import { getValueForStyle } from '../../../../../../../src/utils/getValueForStyle.js' +import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle.js' export class EsriDataset extends Dataset { applyLayerPaintProperties (layerPaintProperties) { diff --git a/plugins/datasets/src/adapters/maplibre/layerBuilders.js b/plugins/datasets/src/adapters/maplibre/layerBuilders.js index f4c59a6c0..fba157634 100644 --- a/plugins/datasets/src/adapters/maplibre/layerBuilders.js +++ b/plugins/datasets/src/adapters/maplibre/layerBuilders.js @@ -1,5 +1,5 @@ -import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle.js' -import { getSymbolAnchor } from '../../../../../../src/utils/symbolUtils.js' +import { getValueForStyle } from '../../../../../src/utils/getValueForStyle.js' +import { getSymbolAnchor } from '../../../../../src/utils/symbolUtils.js' // ─── Fill layer ─────────────────────────────────────────────────────────────── diff --git a/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js b/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js index 66ad72fd5..2199a1fa1 100644 --- a/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +++ b/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js @@ -1,7 +1,7 @@ import MaplibreLayerAdapter from './maplibreLayerAdapter.js' import { datasetRegistry } from '../../registry/datasetRegistry.js' -import { symbolRegistry } from '../../../../../../src/services/symbolRegistry.js' -import { patternRegistry } from '../../../../../../src/services/patternRegistry.js' +import { symbolRegistry } from '../../../../../src/services/symbolRegistry.js' +import { patternRegistry } from '../../../../../src/services/patternRegistry.js' jest.mock('../../registry/datasetRegistry.js') diff --git a/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.js b/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.js index a274fe376..5538472af 100644 --- a/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.js +++ b/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.js @@ -1,7 +1,7 @@ import { Dataset } from '../../../registry/dataset.js' -import { hashString } from '../../../../../../../src/utils/patternUtils.js' -import { anchorToMaplibre } from '../../../../../../../providers/maplibre/src/utils/symbolImages.js' -import { logger } from '../../../../../../../src/services/logger.js' +import { hashString } from '../../../../../../src/utils/patternUtils.js' +import { anchorToMaplibre } from '../../../../../../providers/maplibre/src/utils/symbolImages.js' +import { logger } from '../../../../../../src/services/logger.js' const MAX_TILE_ZOOM = 22 export class MapLibreDataset extends Dataset { diff --git a/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js b/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js index dd8c28f33..cecfb52ad 100644 --- a/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js +++ b/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js @@ -1,10 +1,10 @@ import { MapLibreDataset } from './mapLibreDataset.js' import { datasetRegistry } from '../../../registry/datasetRegistry.js' -import { logger } from '../../../../../../../src/services/logger.js' +import { logger } from '../../../../../../src/services/logger.js' // Use the mock datasetRegistry with the demo datasets attached before each test // so we can test Dataset methods that depend on parent/sublayer relationships and styles jest.mock('../../../registry/datasetRegistry.js') -jest.mock('../../../../../../../src/services/logger.js') +jest.mock('../../../../../../src/services/logger.js') describe('MapLibreDataset', () => { beforeEach(() => { diff --git a/plugins/datasets/src/api/getOpacity.js b/plugins/datasets/src/api/getOpacity.js index 53b9fb61b..58c9b1c15 100644 --- a/plugins/datasets/src/api/getOpacity.js +++ b/plugins/datasets/src/api/getOpacity.js @@ -1,4 +1,4 @@ -import { logger } from '../../../../../src/services/logger.js' +import { logger } from '../../../../src/services/logger.js' import { datasetRegistry } from '../registry/datasetRegistry.js' export const getOpacity = ({ pluginState: { globals } }, options = {}) => { diff --git a/plugins/datasets/src/api/getOpacity.test.js b/plugins/datasets/src/api/getOpacity.test.js index 772bce0f3..191a4d28b 100644 --- a/plugins/datasets/src/api/getOpacity.test.js +++ b/plugins/datasets/src/api/getOpacity.test.js @@ -1,12 +1,12 @@ import { getOpacity } from './getOpacity.js' import { datasetRegistry } from '../registry/datasetRegistry.js' -import { logger } from '../../../../../src/services/logger.js' +import { logger } from '../../../../src/services/logger.js' jest.mock('../registry/datasetRegistry.js', () => ({ datasetRegistry: { getDataset: jest.fn() } })) -jest.mock('../../../../../src/services/logger.js') +jest.mock('../../../../src/services/logger.js') describe('getOpacity', () => { beforeEach(() => { diff --git a/plugins/datasets/src/api/getStyle.js b/plugins/datasets/src/api/getStyle.js index 0a6cd1c94..6ecfc39af 100644 --- a/plugins/datasets/src/api/getStyle.js +++ b/plugins/datasets/src/api/getStyle.js @@ -1,4 +1,4 @@ -import { logger } from '../../../../../src/services/logger.js' +import { logger } from '../../../../src/services/logger.js' import { datasetRegistry } from '../registry/datasetRegistry.js' export const getStyle = ({ pluginState }, { datasetId, sublayerId } = {}) => { diff --git a/plugins/datasets/src/api/getStyle.test.js b/plugins/datasets/src/api/getStyle.test.js index 21702a531..6a40f8d7c 100644 --- a/plugins/datasets/src/api/getStyle.test.js +++ b/plugins/datasets/src/api/getStyle.test.js @@ -1,12 +1,12 @@ import { getStyle } from './getStyle.js' import { datasetRegistry } from '../registry/datasetRegistry.js' -import { logger } from '../../../../../src/services/logger.js' +import { logger } from '../../../../src/services/logger.js' jest.mock('../registry/datasetRegistry.js', () => ({ datasetRegistry: { getDataset: jest.fn() } })) -jest.mock('../../../../../src/services/logger.js') +jest.mock('../../../../src/services/logger.js') describe('getStyle', () => { beforeEach(() => { diff --git a/plugins/datasets/src/api/setData.js b/plugins/datasets/src/api/setData.js index cf0ee23bd..f5485fc87 100644 --- a/plugins/datasets/src/api/setData.js +++ b/plugins/datasets/src/api/setData.js @@ -1,4 +1,4 @@ -import { logger } from '../../../../../src/services/logger.js' +import { logger } from '../../../../src/services/logger.js' import { datasetRegistry } from '../registry/datasetRegistry.js' import { layerAdapter } from '../adapters/loadLayerAdapter.js' diff --git a/plugins/datasets/src/api/setData.test.js b/plugins/datasets/src/api/setData.test.js index 7805bc0a7..43ace4c02 100644 --- a/plugins/datasets/src/api/setData.test.js +++ b/plugins/datasets/src/api/setData.test.js @@ -1,6 +1,6 @@ import { setData } from './setData.js' import { datasetRegistry } from '../registry/datasetRegistry.js' -import { logger } from '../../../../../src/services/logger.js' +import { logger } from '../../../../src/services/logger.js' import { layerAdapter } from '../adapters/loadLayerAdapter.js' jest.mock('../adapters/loadLayerAdapter.js', () => ({ @@ -13,7 +13,7 @@ jest.mock('../registry/datasetRegistry.js', () => ({ datasetRegistry: { getDataset: jest.fn() } })) -jest.mock('../../../../../src/services/logger.js') +jest.mock('../../../../src/services/logger.js') describe('setData', () => { const geojson = { type: 'FeatureCollection', features: [] } diff --git a/plugins/datasets/src/api/setFeatureVisibility.js b/plugins/datasets/src/api/setFeatureVisibility.js index 1f3701ce4..7f7c09a35 100644 --- a/plugins/datasets/src/api/setFeatureVisibility.js +++ b/plugins/datasets/src/api/setFeatureVisibility.js @@ -1,4 +1,4 @@ -import { logger } from '../../../../../src/services/logger.js' +import { logger } from '../../../../src/services/logger.js' import { datasetRegistry } from '../registry/datasetRegistry.js' export const setFeatureVisibility = ({ pluginState: { dispatch } }, visible, featureIds, { datasetId = null } = {}) => { diff --git a/plugins/datasets/src/api/setFeatureVisibility.test.js b/plugins/datasets/src/api/setFeatureVisibility.test.js index a30b9fb88..4c7b910d4 100644 --- a/plugins/datasets/src/api/setFeatureVisibility.test.js +++ b/plugins/datasets/src/api/setFeatureVisibility.test.js @@ -1,12 +1,12 @@ import { setFeatureVisibility } from './setFeatureVisibility.js' import { datasetRegistry } from '../registry/datasetRegistry.js' -import { logger } from '../../../../../src/services/logger.js' +import { logger } from '../../../../src/services/logger.js' jest.mock('../registry/datasetRegistry.js', () => ({ datasetRegistry: { getDataset: jest.fn() } })) -jest.mock('../../../../../src/services/logger.js') +jest.mock('../../../../src/services/logger.js') describe('setFeatureVisibility', () => { const dispatch = jest.fn() diff --git a/plugins/datasets/src/api/setGlobals.js b/plugins/datasets/src/api/setGlobals.js index 10195266d..99c16d90b 100644 --- a/plugins/datasets/src/api/setGlobals.js +++ b/plugins/datasets/src/api/setGlobals.js @@ -1,4 +1,4 @@ -import { logger } from '../../../../../src/services/logger.js' +import { logger } from '../../../../src/services/logger.js' export const setGlobals = ({ pluginState: { dispatch } }, values) => { const { opacityMode } = values const payload = {} diff --git a/plugins/datasets/src/api/setGlobals.test.js b/plugins/datasets/src/api/setGlobals.test.js index 6a107d229..f4bfcfa82 100644 --- a/plugins/datasets/src/api/setGlobals.test.js +++ b/plugins/datasets/src/api/setGlobals.test.js @@ -1,7 +1,7 @@ import { setGlobals } from './setGlobals.js' -import { logger } from '../../../../../src/services/logger.js' +import { logger } from '../../../../src/services/logger.js' -jest.mock('../../../../../src/services/logger.js') +jest.mock('../../../../src/services/logger.js') describe('setGlobals', () => { const dispatch = jest.fn() diff --git a/plugins/datasets/src/components/Key/KeyItem.jsx b/plugins/datasets/src/components/Key/KeyItem.jsx index b47a1ccc5..439aed354 100644 --- a/plugins/datasets/src/components/Key/KeyItem.jsx +++ b/plugins/datasets/src/components/Key/KeyItem.jsx @@ -1,4 +1,4 @@ -import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle.js' +import { getValueForStyle } from '../../../../../src/utils/getValueForStyle.js' import { KeySvg } from './KeySvg.jsx' export const KeyItem = ({ registryDataset, symbolRegistry, patternRegistry, mapStyle }) => { diff --git a/plugins/datasets/src/components/Key/KeySvg.test.jsx b/plugins/datasets/src/components/Key/KeySvg.test.jsx index 494e9c4f2..4020fd161 100644 --- a/plugins/datasets/src/components/Key/KeySvg.test.jsx +++ b/plugins/datasets/src/components/Key/KeySvg.test.jsx @@ -1,8 +1,8 @@ import { render } from '@testing-library/react' import { KeySvg } from './KeySvg' -import { symbolRegistry } from '../../../../../../src/services/symbolRegistry.js' -import { patternRegistry } from '../../../../../../src/services/patternRegistry.js' +import { symbolRegistry } from '../../../../../src/services/symbolRegistry.js' +import { patternRegistry } from '../../../../../src/services/patternRegistry.js' const getSymbolDef = jest.spyOn(symbolRegistry, 'getSymbolDef') diff --git a/plugins/datasets/src/components/Key/KeySvgLine.jsx b/plugins/datasets/src/components/Key/KeySvgLine.jsx index 13fb3d87c..7d5b165ad 100644 --- a/plugins/datasets/src/components/Key/KeySvgLine.jsx +++ b/plugins/datasets/src/components/Key/KeySvgLine.jsx @@ -1,5 +1,5 @@ import { svgProps, SVG_SIZE, SVG_CENTER } from './svgProperties.js' -import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle.js' +import { getValueForStyle } from '../../../../../src/utils/getValueForStyle.js' export const KeySvgLine = ({ mapStyle, registryDataset }) => { const { style } = registryDataset diff --git a/plugins/datasets/src/components/Key/KeySvgLine.test.jsx b/plugins/datasets/src/components/Key/KeySvgLine.test.jsx index 866aa3300..4a596b80a 100644 --- a/plugins/datasets/src/components/Key/KeySvgLine.test.jsx +++ b/plugins/datasets/src/components/Key/KeySvgLine.test.jsx @@ -1,9 +1,9 @@ import { render } from '@testing-library/react' import { KeySvgLine } from './KeySvgLine' -import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle' +import { getValueForStyle } from '../../../../../src/utils/getValueForStyle' -jest.mock('../../../../../../src/utils/getValueForStyle', () => ({ +jest.mock('../../../../../src/utils/getValueForStyle', () => ({ getValueForStyle: jest.fn((value) => value) })) diff --git a/plugins/datasets/src/components/Key/KeySvgPattern.test.jsx b/plugins/datasets/src/components/Key/KeySvgPattern.test.jsx index c806168a5..209d9953f 100644 --- a/plugins/datasets/src/components/Key/KeySvgPattern.test.jsx +++ b/plugins/datasets/src/components/Key/KeySvgPattern.test.jsx @@ -1,6 +1,6 @@ import { render } from '@testing-library/react' import { KeySvgPattern } from './KeySvgPattern' -import { patternRegistry } from '../../../../../../src/services/patternRegistry.js' +import { patternRegistry } from '../../../../../src/services/patternRegistry.js' const getKeyPatternPaths = jest.spyOn(patternRegistry, 'getKeyPatternPaths') const defaultProps = { diff --git a/plugins/datasets/src/components/Key/KeySvgRect.jsx b/plugins/datasets/src/components/Key/KeySvgRect.jsx index 86f233309..ac0bab836 100644 --- a/plugins/datasets/src/components/Key/KeySvgRect.jsx +++ b/plugins/datasets/src/components/Key/KeySvgRect.jsx @@ -1,4 +1,4 @@ -import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle.js' +import { getValueForStyle } from '../../../../../src/utils/getValueForStyle.js' import { svgProps, SVG_SIZE } from './svgProperties.js' export const KeySvgRect = ({ mapStyle, registryDataset }) => { diff --git a/plugins/datasets/src/components/Key/KeySvgRect.test.jsx b/plugins/datasets/src/components/Key/KeySvgRect.test.jsx index 0365d8069..30da5bc72 100644 --- a/plugins/datasets/src/components/Key/KeySvgRect.test.jsx +++ b/plugins/datasets/src/components/Key/KeySvgRect.test.jsx @@ -1,9 +1,9 @@ import { render } from '@testing-library/react' import { KeySvgRect } from './KeySvgRect' -import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle' +import { getValueForStyle } from '../../../../../src/utils/getValueForStyle' -jest.mock('../../../../../../src/utils/getValueForStyle', () => ({ +jest.mock('../../../../../src/utils/getValueForStyle', () => ({ getValueForStyle: jest.fn((value) => value) })) diff --git a/plugins/datasets/src/components/Key/KeySvgSymbol.jsx b/plugins/datasets/src/components/Key/KeySvgSymbol.jsx index e2b92f246..7e21a026e 100644 --- a/plugins/datasets/src/components/Key/KeySvgSymbol.jsx +++ b/plugins/datasets/src/components/Key/KeySvgSymbol.jsx @@ -1,4 +1,4 @@ -import { getSymbolStyleColors, getSymbolViewBox } from '../../../../../../src/utils/symbolUtils.js' +import { getSymbolStyleColors, getSymbolViewBox } from '../../../../../src/utils/symbolUtils.js' import { svgSymbolProps } from './svgProperties.js' export const KeySvgSymbol = ({ symbolRegistry, registryDataset, mapStyle, symbolDef }) => { diff --git a/plugins/datasets/src/components/Key/KeySvgSymbol.test.jsx b/plugins/datasets/src/components/Key/KeySvgSymbol.test.jsx index 4436c78f5..920fd3e28 100644 --- a/plugins/datasets/src/components/Key/KeySvgSymbol.test.jsx +++ b/plugins/datasets/src/components/Key/KeySvgSymbol.test.jsx @@ -1,9 +1,9 @@ import { render } from '@testing-library/react' import { KeySvgSymbol } from './KeySvgSymbol' -import { getSymbolStyleColors, getSymbolViewBox } from '../../../../../../src/utils/symbolUtils.js' +import { getSymbolStyleColors, getSymbolViewBox } from '../../../../../src/utils/symbolUtils.js' -jest.mock('../../../../../../src/utils/symbolUtils.js', () => ({ +jest.mock('../../../../../src/utils/symbolUtils.js', () => ({ getSymbolStyleColors: jest.fn(() => ({ foreground: '#000', background: '#fff' })), getSymbolViewBox: jest.fn(() => '0 0 38 38') })) diff --git a/plugins/datasets/src/initialise/DatasetsInit.jsx b/plugins/datasets/src/initialise/DatasetsInit.jsx index 81f88be46..46df27c23 100755 --- a/plugins/datasets/src/initialise/DatasetsInit.jsx +++ b/plugins/datasets/src/initialise/DatasetsInit.jsx @@ -1,6 +1,6 @@ // src/plugins/datasets/datasetsInit.jsx import { useEffect, useRef } from 'react' -import { EVENTS } from '../../../../../src/config/events.js' +import { EVENTS } from '../../../../src/config/events.js' import { initialiseDatasets } from './initialiseDatasets.js' import { datasetRegistry } from '../registry/datasetRegistry.js' import { setMenuState } from '../registry/isVisibleWhen.js' diff --git a/plugins/datasets/src/reducers/pluginState.js b/plugins/datasets/src/reducers/pluginState.js index 33f894a79..8603ce1e4 100755 --- a/plugins/datasets/src/reducers/pluginState.js +++ b/plugins/datasets/src/reducers/pluginState.js @@ -1,7 +1,7 @@ import { addDatasetToMenu, removeDatasetsFromMenu } from './datasetsToMenu.js' import { buildMenuState } from './menuStateReducer.js' import { mappedDatasetsReducer } from './mappedDatasetsReducer.js' -import { logger } from '../../../../../src/services/logger.js' +import { logger } from '../../../../src/services/logger.js' const initialState = { globals: { diff --git a/plugins/datasets/src/reducers/pluginState.test.js b/plugins/datasets/src/reducers/pluginState.test.js index c0d927530..82854fb52 100644 --- a/plugins/datasets/src/reducers/pluginState.test.js +++ b/plugins/datasets/src/reducers/pluginState.test.js @@ -1,7 +1,7 @@ import { initialState, actions } from './pluginState.js' -import { logger } from '../../../../../src/services/logger.js' +import { logger } from '../../../../src/services/logger.js' -jest.mock('../../../../../src/services/logger.js') +jest.mock('../../../../src/services/logger.js') // Helper: build a minimal plugin state with some mapped datasets const makeState = (overrides = {}) => ({ diff --git a/rollup.esm.mjs b/rollup.esm.mjs index f9cfef484..10505f1a4 100644 --- a/rollup.esm.mjs +++ b/rollup.esm.mjs @@ -177,11 +177,13 @@ const createESMConfig = (entryPath, outDir, isCore = false, manualChunks = null, ...(isCore ? [removeFullCssPlugin(cssDir)] : []), // Only runs when ANALYZE=1 is set; writes stats to dist/stats/.html - ...(process.env.ANALYZE ? [visualizer({ + ...(process.env.ANALYZE +? [visualizer({ filename: path.resolve(__dirname, 'dist/stats', `${outDir.replace(/\//g, '-')}.html`), open: false, gzipSize: true - })] : []) + })] +: []) ], output: { @@ -262,8 +264,8 @@ const ALL_BUILDS = [ manualChunks: (id) => { if (id.includes('/manifest')) return 'im-interact-plugin' } }, { - entryPath: './plugins/beta/datasets/src/index.js', - outDir: 'plugins/beta/datasets/dist/esm', + entryPath: './plugins/datasets/src/index.js', + outDir: 'plugins/datasets/dist/esm', manualChunks: (id) => { if (id.includes('/manifest')) return 'im-datasets-plugin' if (id.includes('maplibreLayerAdapter')) return 'im-datasets-ml-adapter' diff --git a/webpack.umd.mjs b/webpack.umd.mjs index 3704729f2..b67609617 100755 --- a/webpack.umd.mjs +++ b/webpack.umd.mjs @@ -144,7 +144,7 @@ const ALL_BUILDS = [ { entryPath: './plugins/beta/use-location/src/index.js', libraryPath: 'useLocationPlugin', outDir: 'plugins/beta/use-location/dist/umd' }, { entryPath: './plugins/search/src/index.js', libraryPath: 'searchPlugin', outDir: 'plugins/search/dist/umd' }, { entryPath: './plugins/interact/src/index.js', libraryPath: 'interactPlugin', outDir: 'plugins/interact/dist/umd' }, - { entryPath: './plugins/beta/datasets/src/index.js', libraryPath: 'datasetsPlugin', outDir: 'plugins/beta/datasets/dist/umd', cssOutDir: 'plugins/beta/datasets/dist' }, + { entryPath: './plugins/datasets/src/index.js', libraryPath: 'datasetsPlugin', outDir: 'plugins/datasets/dist/umd', cssOutDir: 'plugins/datasets/dist' }, { entryPath: './plugins/beta/map-styles/src/index.js', libraryPath: 'mapStylesPlugin', outDir: 'plugins/beta/map-styles/dist/umd' }, { entryPath: './plugins/beta/draw-ml/src/index.js', libraryPath: 'drawMLPlugin', outDir: 'plugins/beta/draw-ml/dist/umd' }, { entryPath: './plugins/beta/frame/src/index.js', libraryPath: 'framePlugin', outDir: 'plugins/beta/frame/dist/umd' } From 8de1161e47b7b47e31ea7b2a4846cdc930ac8343 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Tue, 14 Jul 2026 13:02:26 +0100 Subject: [PATCH 40/53] IM-401 addressed some Sonar Issues --- .../src/adapters/esri/esriLayerAdapter.js | 10 ++++---- .../adapters/maplibre/maplibreLayerAdapter.js | 6 +++-- plugins/datasets/src/api/getStyle.js | 2 +- plugins/datasets/src/api/setData.js | 2 +- plugins/datasets/src/components/Key/Key.jsx | 1 - .../components/LayersMenu/LayersMenuRadio.jsx | 2 +- .../LayersMenu/LayersRadioGroupWrapper.jsx | 7 +++--- .../src/initialise/initialiseDatasets.js | 1 - .../src/reducers/__data__/demoDatasets.js | 23 +++++++++---------- .../src/registry/datasetDefinitionCache.js | 4 ++-- .../datasets/src/registry/datasetRegistry.js | 2 +- plugins/datasets/src/utils/bbox.js | 8 +++---- 12 files changed, 33 insertions(+), 35 deletions(-) diff --git a/plugins/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js index 3cc304b5e..6be8f18b7 100644 --- a/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js @@ -6,11 +6,10 @@ import { EsriDataset } from './registry/esriDataset.js' import { logger } from '../../../../../src/services/logger.js' export default class EsriLayerAdapter extends LayerAdapter { - constructor (mapProvider, symbolRegistry, patternRegistry) { + constructor (mapProvider) { super() this._mapProvider = mapProvider this._map = mapProvider.map - // TODO: Implement symbolRegistry and patternRegistry usage in the adapter // _vectorTileLayers is a map of datasetId to VectorTileLayer instances // it includes stand alone vectorTileLayers and vectorTileLayers that are part of a groupLayer @@ -33,7 +32,7 @@ export default class EsriLayerAdapter extends LayerAdapter { async init () { const topLevelDatasets = datasetRegistry.topLevelDatasets() // ensure the datasets are added in order - for await (const registryDataset of topLevelDatasets) { + for (const registryDataset of topLevelDatasets) { await this._addLayers(registryDataset) } @@ -111,7 +110,7 @@ export default class EsriLayerAdapter extends LayerAdapter { } // If the group layer has no more sublayers, we need to also remove the group layer from the map - if (groupLayer && groupLayer.layers.length === 0) { + if (groupLayer?.layers.length === 0) { this._map.remove(groupLayer) delete this._groupLayers[esriGroupId] } @@ -127,7 +126,8 @@ export default class EsriLayerAdapter extends LayerAdapter { this._applyStyleLayerVisibility(registryDataset, vectorTileLayer) // Don't apply the visibility change to the parent, since the parent may have other sublayers that are visible return - } else if (visible) { + } + if (visible) { // No need to apply style layer visibility for datasets that are hidden registryDataset.sublayers.forEach(sublayer => this._applyStyleLayerVisibility(sublayer, vectorTileLayer)) } diff --git a/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js b/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js index dc214ad45..d33011fca 100644 --- a/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +++ b/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js @@ -125,7 +125,9 @@ export default class MaplibreLayerAdapter extends LayerAdapter { if (imageId) { this._map.setLayoutProperty(symbolLayerId, 'icon-image', imageId) } - } else if (fillLayerId && this._map.getLayer(fillLayerId)) { + return + } + if (fillLayerId && this._map.getLayer(fillLayerId)) { const imageId = this._patternRegistry.getPatternImageId(registryDataset.style, mapStyle.id, this._pixelRatio) if (imageId) { this._map.setPaintProperty(fillLayerId, 'fill-pattern', imageId) @@ -163,7 +165,7 @@ export default class MaplibreLayerAdapter extends LayerAdapter { // Remove source if no other dataset is using it const sourceIsShared = datasetRegistry.topLevelDatasets() - .filter(registryDataset => registryDataset.id !== datasetId && registryDataset.sourceId === sourceId) + .filter(dataset => dataset.id !== datasetId && dataset.sourceId === sourceId) .length > 0 if (!sourceIsShared && this._map.getSource(sourceId)) { diff --git a/plugins/datasets/src/api/getStyle.js b/plugins/datasets/src/api/getStyle.js index 6ecfc39af..0f26d3401 100644 --- a/plugins/datasets/src/api/getStyle.js +++ b/plugins/datasets/src/api/getStyle.js @@ -1,7 +1,7 @@ import { logger } from '../../../../src/services/logger.js' import { datasetRegistry } from '../registry/datasetRegistry.js' -export const getStyle = ({ pluginState }, { datasetId, sublayerId } = {}) => { +export const getStyle = ({ _pluginState }, { datasetId, sublayerId } = {}) => { datasetId = sublayerId ? `${datasetId}-${sublayerId}` : datasetId const registryDataset = datasetRegistry.getDataset(datasetId) if (!registryDataset) { diff --git a/plugins/datasets/src/api/setData.js b/plugins/datasets/src/api/setData.js index f5485fc87..a63f4f640 100644 --- a/plugins/datasets/src/api/setData.js +++ b/plugins/datasets/src/api/setData.js @@ -2,7 +2,7 @@ import { logger } from '../../../../src/services/logger.js' import { datasetRegistry } from '../registry/datasetRegistry.js' import { layerAdapter } from '../adapters/loadLayerAdapter.js' -export const setData = ({ pluginState }, geojson, { datasetId }) => { +export const setData = ({ _pluginState }, geojson, { datasetId }) => { const registryDataset = datasetRegistry.getDataset(datasetId) if (!registryDataset) { logger.warn(`setData: Dataset with id ${datasetId} not found`) diff --git a/plugins/datasets/src/components/Key/Key.jsx b/plugins/datasets/src/components/Key/Key.jsx index 40d9d3ff6..2b3582d5f 100755 --- a/plugins/datasets/src/components/Key/Key.jsx +++ b/plugins/datasets/src/components/Key/Key.jsx @@ -7,7 +7,6 @@ import { datasetRegistry } from '../../registry/datasetRegistry.js' export const Key = ({ pluginConfig: { noKeyItemText }, mapState: { mapStyle }, - pluginState: { mappedDatasets }, services: { symbolRegistry, patternRegistry } }) => { const { items: keyGroups, hasGroups } = datasetRegistry.keyItems() diff --git a/plugins/datasets/src/components/LayersMenu/LayersMenuRadio.jsx b/plugins/datasets/src/components/LayersMenu/LayersMenuRadio.jsx index 2b74090ea..0ffefb89c 100644 --- a/plugins/datasets/src/components/LayersMenu/LayersMenuRadio.jsx +++ b/plugins/datasets/src/components/LayersMenu/LayersMenuRadio.jsx @@ -1,6 +1,6 @@ import { isVisibleWhen } from '../../registry/isVisibleWhen.js' -export const LayersMenuRadio = ({ menuState, menuGroupItem, checked, name, onChange }) => { +export const LayersMenuRadio = ({ menuGroupItem, name, checked, onChange }) => { const itemClass = 'im-c-datasets-layers__item govuk-radios govuk-radios--small"' const { visibleWhen } = menuGroupItem const visible = visibleWhen ? isVisibleWhen(visibleWhen) : true diff --git a/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx b/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx index 493d51430..9f12b17b6 100644 --- a/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +++ b/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React from 'react' import { isVisibleWhen } from '../../registry/isVisibleWhen.js' import { LayersMenuRadio } from './LayersMenuRadio.jsx' @@ -10,9 +10,8 @@ export const LayersRadioGroupWrapper = ({ pluginState, menuGroup }) => { } const { menuState, dispatch } = pluginState - const [value, setValue] = useState(menuState[id]) + const value = menuState[id] const handleChange = (event) => { - setValue(event.target.value) dispatch({ type: 'UPDATE_MENU_STATE', payload: { [id]: event.target.value } }) } @@ -23,7 +22,7 @@ export const LayersRadioGroupWrapper = ({ pluginState, menuGroup }) => { {menuGroup.label} -
+
{items.map((menuGroupItem) => { - const existingDefinition = this.idToDefinitionMap.get(id) - this.definitionToInstanceMap.delete(existingDefinition) + const definition = this.idToDefinitionMap.get(id) + this.definitionToInstanceMap.delete(definition) this.idToDefinitionMap.delete(id) }) } diff --git a/plugins/datasets/src/registry/datasetRegistry.js b/plugins/datasets/src/registry/datasetRegistry.js index aebdc2d4f..8b9a2e88f 100644 --- a/plugins/datasets/src/registry/datasetRegistry.js +++ b/plugins/datasets/src/registry/datasetRegistry.js @@ -25,7 +25,7 @@ const datasetRegistry = { // createDataset defaults to a generic dataset factory function, but can be overridden by calling // attachCreateDataset, which allows the layer adapter to provide its own createDataset function, - attachCreateDataset (createDataset) { this._createDataset = createDataset }, + attachCreateDataset (newCreateDatasetFunction) { this._createDataset = newCreateDatasetFunction }, _createDataset: (datasetDefinition) => createDataset(datasetDefinition), attachMapStyle (mapStyle) { diff --git a/plugins/datasets/src/utils/bbox.js b/plugins/datasets/src/utils/bbox.js index bcb0d55e7..7fa337638 100755 --- a/plugins/datasets/src/utils/bbox.js +++ b/plugins/datasets/src/utils/bbox.js @@ -103,10 +103,10 @@ export const getGeometryBbox = (geometry) => { case 'GeometryCollection': geometry.geometries.forEach(g => { const b = getGeometryBbox(g) - minX = Math.min(minX, b[0]) - minY = Math.min(minY, b[1]) - maxX = Math.max(maxX, b[2]) - maxY = Math.max(maxY, b[3]) + minX = Math.min(minX, b[0]) // west + minY = Math.min(minY, b[1]) // south + maxX = Math.max(maxX, b[2]) // east + maxY = Math.max(maxY, b[3]) // NOSONAR north }) break default: From dda7db2fa9d45b6c61ac7cf4ee6b31e0bdc51407 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Tue, 14 Jul 2026 13:41:48 +0100 Subject: [PATCH 41/53] IM-401 more sonar fixes --- .../src/reducers/__data__/demoDatasets.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/plugins/datasets/src/reducers/__data__/demoDatasets.js b/plugins/datasets/src/reducers/__data__/demoDatasets.js index aeddec4d4..64cf922f8 100644 --- a/plugins/datasets/src/reducers/__data__/demoDatasets.js +++ b/plugins/datasets/src/reducers/__data__/demoDatasets.js @@ -3,16 +3,16 @@ const pointData = { features: [{ type: 'Feature', properties: { category: 'prehistoric' }, - geometry: { coordinates: [-2.4558622, 54.5617135], type: 'Point' } + geometry: { coordinates: [-2.4558622, 54.5617135], type: 'Point' } // NOSONAR }, { type: 'Feature', properties: { category: 'roman' }, - geometry: { coordinates: [-2.439823, 54.5525437], type: 'Point' } + geometry: { coordinates: [-2.439823, 54.5525437], type: 'Point' } // NOSONAR }, { type: 'Feature', properties: { category: 'medieval' }, - geometry: { coordinates: [-2.4481939, 54.5575261], type: 'Point' } + geometry: { coordinates: [-2.4481939, 54.5575261], type: 'Point' } // NOSONAR }] } export const datasets = [ @@ -25,7 +25,7 @@ export const datasets = [ transformRequest: (url) => url + 'TRANSFORMED', // Required maxFeatures: 50000 // Optional: evict distant features when exceeded }, - hiddenFeatures: [42], + hiddenFeatures: [42], // NOSONAR query: {}, maxFeatures: 50000, // Optional: evict distant features when exceeded minZoom: 10, @@ -219,14 +219,8 @@ export const expectedDatasetsMenuConfig = [ } ] -const landCovers = datasets[0] -const existingFields = datasets[1] -const historicMonuments = datasets[2] -const hedgeControl = datasets[3] -const landCoversMenuItem = expectedDatasetsMenuConfig[0] -const existingFieldsMenuItem = expectedDatasetsMenuConfig[1] -const historicMonumentsMenuItem = expectedDatasetsMenuConfig[2] -const hedgeControlMenuItem = expectedDatasetsMenuConfig[3] +const [landCovers, existingFields, historicMonuments, hedgeControl] = datasets +const [landCoversMenuItem, existingFieldsMenuItem, historicMonumentsMenuItem, hedgeControlMenuItem] = expectedDatasetsMenuConfig const testGroupLabel = 'Test group' export const datasetsWithGroups = [ From 8e2ec3e053a9cd70f23e75df4208e0ba0255a0d3 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Tue, 14 Jul 2026 16:49:10 +0100 Subject: [PATCH 42/53] IM-399 fixed umd build issue with isVisibleWhen setMenuState --- plugins/datasets/src/registry/isVisibleWhen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/datasets/src/registry/isVisibleWhen.js b/plugins/datasets/src/registry/isVisibleWhen.js index b2fd1cb97..f9d32ece0 100644 --- a/plugins/datasets/src/registry/isVisibleWhen.js +++ b/plugins/datasets/src/registry/isVisibleWhen.js @@ -1,7 +1,7 @@ import { datasetRegistry } from './datasetRegistry.js' let _menuState = {} -export const setMenuState = (menuState) => (_menuState = menuState) +export const setMenuState = (menuState) => { _menuState = menuState } const _isVisibleWhenMenuCheck = (menuVisibleWhen) => { for (const [key, valueArray] of Object.entries(menuVisibleWhen)) { From f83badfd39edf74067a89f1f3b24d249d0e4ff1e Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Tue, 14 Jul 2026 16:54:37 +0100 Subject: [PATCH 43/53] IM-399 featureLayers working - but no styles --- demo/js/esri-datasets.js | 66 +++++++++++++++++-- demo/js/ml-datasets.js | 18 ++--- .../src/adapters/esri/esriLayerAdapter.js | 28 +++++++- plugins/datasets/src/registry/dataset.js | 1 + 4 files changed, 96 insertions(+), 17 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 54e3535be..3de2d1d95 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -9,6 +9,8 @@ import { transformGeocodeRequest, transformVtsRequest3857, setupEsriConfig } fro const nonFloodZoneLight = '#2b8cbe' const nonFloodZoneDark = '#7fcdbb' +const white = '#ffffff' +const darkTeal = '#12393d' const COLOURS = { // floodExtents: { default: nonFloodZoneLight, dark: nonFloodZoneDark }, @@ -36,8 +38,6 @@ const nonFloodZoneDepthBandsLight = [COLOURS.depthOver2300.default, COLOURS.dept // GREENS dark tones > 2300 to < 150 const nonFloodZoneDepthBandsDark = [COLOURS.depthOver2300.dark, COLOURS.depth2300.dark, COLOURS.depth1200.dark, COLOURS.depth900.dark, COLOURS.depth600.dark, COLOURS.depth300.dark, COLOURS.depth150.dark] - - const datasetFloodZonesCC = { id: 'floodzonescc', label: 'Flood Zones Climate Change', @@ -326,8 +326,60 @@ const surfaceWaterDepthAllDataset = { ] } +const datasetMainRivers = { + id: 'mainrivers', + label: 'Main Rivers', + groupLabel: 'Map features', + type: 'FeatureService', + tiles: 'https://services1.arcgis.com/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Statutory_Main_River_Map/FeatureServer', + showInKey: true, + showInMenu: true, + sourceLayer: 'Statutory_Main_River_Map', + visible: false, + style: { + stroke: { outdoor: darkTeal, dark: white }, + strokeWidth: 3 + } +} + +const datasetWaterStorageAreas = { + id: 'waterstorage', + label: 'Water Storage', + groupLabel: 'Map features', + type: 'FeatureService', + tiles: 'https://services1.arcgis.com/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Storage_Areas_NON_PRODUCTION/FeatureServer', + showInKey: true, + showInMenu: true, + sourceLayer: 'Flood_Storage_Areas', + visible: false, + style: { + stroke: { outdoor: darkTeal, dark: white }, + strokeWidth: 1, + fillPattern: 'diagonal-cross-hatch', + fillPatternForegroundColor: { outdoor: darkTeal, dark: white }, + fillPatternBackgroundColor: 'transparent' + } +} + +const datasetFloodDefences = { + id: 'flooddefence', + label: 'Flood Defence', + groupLabel: 'Map features', + type: 'FeatureService', + tiles: 'https://services1.arcgis.com/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Defences_NON_PRODUCTION/FeatureServer', + showInKey: true, + showInMenu: true, + sourceLayer: 'Defences', + visible: false, + style: { + stroke: { outdoor: '#f47738', dark: '#f47738' }, + strokeWidth: 3 + } +} + const datasets = [ - datasetFloodZonesCC, datasetFloodZones, surfaceWaterDataset, surfaceWaterDepthAllDataset + datasetFloodZonesCC, datasetFloodZones, surfaceWaterDataset, surfaceWaterDepthAllDataset, + datasetWaterStorageAreas, datasetFloodDefences, datasetMainRivers ] const menu = [ @@ -391,14 +443,14 @@ const menu = [ ] }, { id: 'features', - label: 'Map features', + groupLabel: 'Map features', urlKey: 'features', type: 'checkbox', visibleWhen: true, items: [ - { id: 'water-storage', label: 'Water storage', checked: false }, - { id: 'flood-defence', label: 'Flood defence', checked: false }, - { id: 'main-rivers', label: 'Main rivers', checked: false }, + { id: 'waterstorage', label: 'Water storage' }, + { id: 'flooddefence', label: 'Flood defence' }, + { id: 'mainrivers', label: 'Main rivers' }, ] } ] diff --git a/demo/js/ml-datasets.js b/demo/js/ml-datasets.js index 378f54039..ec3d79034 100644 --- a/demo/js/ml-datasets.js +++ b/demo/js/ml-datasets.js @@ -467,15 +467,15 @@ const testSetData = () => { } interactiveMap.on('datasets:ready', function () { - testGetters() - testInvalidApiCalls() - testFeatureVisibility() - testSetOpacity() - testSetStyle() - testVisibility() - testGlobalVisibility() - testRemoveAndAddDataset() - testSetData() + // testGetters() + // testInvalidApiCalls() + // testFeatureVisibility() + // testSetOpacity() + // testSetStyle() + // testVisibility() + // testGlobalVisibility() + // testRemoveAndAddDataset() + // testSetData() }) // Ref to the selected features diff --git a/plugins/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js index 6be8f18b7..58cc0e469 100644 --- a/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js @@ -1,4 +1,5 @@ import VectorTileLayer from '@arcgis/core/layers/VectorTileLayer.js' +import FeatureLayer from '@arcgis/core/layers/FeatureLayer.js' import GroupLayer from '@arcgis/core/layers/GroupLayer.js' import { LayerAdapter } from '../layerAdapter.js' import { datasetRegistry } from '../../registry/datasetRegistry.js' @@ -61,8 +62,30 @@ export default class EsriLayerAdapter extends LayerAdapter { return this._groupLayers[esriGroupId] } + async _addFeatureLayers (registryDataset) { + const featureLayer = new FeatureLayer({ + id: registryDataset.id, + url: registryDataset.tiles, + opacity: 1, + visible: false + }) + this._vectorTileLayers[registryDataset.id] = featureLayer + this._vectorTileOpacityLayers[registryDataset.id] = featureLayer + try { + this._map.add(featureLayer) + return featureLayer.when() + } catch (error) { + logger.error(`Error adding FeatureLayer for dataset ${registryDataset.id}:`, error) + } + } + async _addLayers (registryDataset) { - const { esriGroupId } = registryDataset + const { type, esriGroupId } = registryDataset + + if (type === 'FeatureService') { + return this._addFeatureLayers(registryDataset) + } + const vectorTileParent = esriGroupId ? this._addGroupLayer(esriGroupId) : this._map const vectorTileLayer = new VectorTileLayer({ id: registryDataset.id, @@ -121,6 +144,9 @@ export default class EsriLayerAdapter extends LayerAdapter { // if this is a top level dataset, we need to apply the visibility to the vectorTileLayer/ groupLayer itself const { id, isSublayer, visible, parentId } = registryDataset const vectorTileLayer = this._vectorTileLayers[isSublayer ? parentId : id] + if (!vectorTileLayer) { + return + } if (isSublayer) { this._applyStyleLayerVisibility(registryDataset, vectorTileLayer) diff --git a/plugins/datasets/src/registry/dataset.js b/plugins/datasets/src/registry/dataset.js index 9d14997d9..77237ea20 100644 --- a/plugins/datasets/src/registry/dataset.js +++ b/plugins/datasets/src/registry/dataset.js @@ -23,6 +23,7 @@ export class Dataset { get parentId () { return this._datasetDefinition.parentId } get minZoom () { return this._datasetDefinition.minZoom || this.parent?.minZoom } get maxZoom () { return this._datasetDefinition.maxZoom || this.parent?.maxZoom } + get type () { return this._datasetDefinition.type || this.parent?.type } get showInKey () { const own = this._datasetDefinition.showInKey From c32915db05d6b73ec48f9155e1295dc09f472869 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Tue, 14 Jul 2026 20:56:54 +0100 Subject: [PATCH 44/53] IM-399 featureLayers styles working --- demo/js/esri-datasets.js | 30 +++++++++- .../src/adapters/esri/esriLayerAdapter.js | 46 +++++++++------ .../adapters/esri/esriLayerAdapter.test.js | 58 +++++++++---------- .../src/adapters/esri/registry/esriDataset.js | 19 ++++++ 4 files changed, 104 insertions(+), 49 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 3de2d1d95..60c5b1ffe 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -337,6 +337,14 @@ const datasetMainRivers = { sourceLayer: 'Statutory_Main_River_Map', visible: false, style: { + renderer: { + type: 'simple', + symbol: { + type: 'simple-line', + width: '3px', + color: { outdoor: darkTeal, dark: white }, + } + }, stroke: { outdoor: darkTeal, dark: white }, strokeWidth: 3 } @@ -353,6 +361,18 @@ const datasetWaterStorageAreas = { sourceLayer: 'Flood_Storage_Areas', visible: false, style: { + renderer: { + type: 'simple', + symbol: { + type: 'simple-fill', + style: 'diagonal-cross', + color: { outdoor: darkTeal, dark: white }, + outline: { + color: { outdoor: darkTeal, dark: white }, + width: 1 + } + } + }, stroke: { outdoor: darkTeal, dark: white }, strokeWidth: 1, fillPattern: 'diagonal-cross-hatch', @@ -372,7 +392,15 @@ const datasetFloodDefences = { sourceLayer: 'Defences', visible: false, style: { - stroke: { outdoor: '#f47738', dark: '#f47738' }, + renderer: { + type: 'simple', + symbol: { + type: 'simple-line', + width: '3px', + color: '#f47738', + } + }, + stroke: '#f47738', strokeWidth: 3 } } diff --git a/plugins/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js index 58cc0e469..ab80862b8 100644 --- a/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js @@ -12,15 +12,15 @@ export default class EsriLayerAdapter extends LayerAdapter { this._mapProvider = mapProvider this._map = mapProvider.map - // _vectorTileLayers is a map of datasetId to VectorTileLayer instances + // _mapVisibilityLayers is a map of datasetId to VectorTileLayer or FeatureLayer instances // it includes stand alone vectorTileLayers and vectorTileLayers that are part of a groupLayer // but does not include group layers themselves, which are tracked in _groupLayers - this._vectorTileLayers = {} + this._mapVisibilityLayers = {} - // _vectorTileOpacityLayers is a map of datasetId to VectorTileLayer/GroupLayer where opacity is applied - // it includes stand alone vectorTileLayers and groupLayers that contain vectorTileLayers + // _mapOpacityLayers is a map of datasetId to mapLayers where opacity is applied + // it includes featureLayers, vectorTileLayers and groupLayers // but does not include vectorTileLayers that are part of a groupLayer - this._vectorTileOpacityLayers = {} + this._mapOpacityLayers = {} // _groupLayers is a map of esriGroupId to GroupLayer this._groupLayers = {} @@ -66,11 +66,12 @@ export default class EsriLayerAdapter extends LayerAdapter { const featureLayer = new FeatureLayer({ id: registryDataset.id, url: registryDataset.tiles, + renderer: registryDataset.renderer, opacity: 1, visible: false }) - this._vectorTileLayers[registryDataset.id] = featureLayer - this._vectorTileOpacityLayers[registryDataset.id] = featureLayer + this._mapVisibilityLayers[registryDataset.id] = featureLayer + this._mapOpacityLayers[registryDataset.id] = featureLayer try { this._map.add(featureLayer) return featureLayer.when() @@ -93,8 +94,8 @@ export default class EsriLayerAdapter extends LayerAdapter { opacity: 1, visible: false }) - this._vectorTileLayers[registryDataset.id] = vectorTileLayer - this._vectorTileOpacityLayers[registryDataset.id] = esriGroupId ? vectorTileParent : vectorTileLayer + this._mapVisibilityLayers[registryDataset.id] = vectorTileLayer + this._mapOpacityLayers[registryDataset.id] = esriGroupId ? vectorTileParent : vectorTileLayer vectorTileParent.add(vectorTileLayer) return vectorTileLayer.when() } @@ -107,7 +108,7 @@ export default class EsriLayerAdapter extends LayerAdapter { } await this._addLayers(registryDataset) const { parentId } = registryDataset - const vectorTileLayer = this._vectorTileLayers[parentId || datasetId] + const vectorTileLayer = this._mapVisibilityLayers[parentId || datasetId] this.applyDatasetOpacity(datasetId) this._applyStyleLayerPaintProperties(registryDataset, vectorTileLayer) this.applyDatasetVisibility(datasetId) @@ -119,7 +120,7 @@ export default class EsriLayerAdapter extends LayerAdapter { return } const { esriGroupId } = registryDataset - const vectorTileLayer = this._vectorTileLayers[datasetId] + const vectorTileLayer = this._mapVisibilityLayers[datasetId] // If the dataset is part of a group layer, we need to remove it from the group layer const groupLayer = esriGroupId ? this._groupLayers[esriGroupId] : null const vectorTileParent = groupLayer || this._map @@ -128,8 +129,8 @@ export default class EsriLayerAdapter extends LayerAdapter { // Remove the vectorTileLayer from the map or group layer vectorTileParent.remove(vectorTileLayer) // And remove the vectorTileLayer from the adapter's internal state - delete this._vectorTileLayers[datasetId] - delete this._vectorTileOpacityLayers[datasetId] + delete this._mapVisibilityLayers[datasetId] + delete this._mapOpacityLayers[datasetId] } // If the group layer has no more sublayers, we need to also remove the group layer from the map @@ -143,7 +144,7 @@ export default class EsriLayerAdapter extends LayerAdapter { // if this is a sublayer, we need to apply the visibility to the vectorTileLayers style sheet // if this is a top level dataset, we need to apply the visibility to the vectorTileLayer/ groupLayer itself const { id, isSublayer, visible, parentId } = registryDataset - const vectorTileLayer = this._vectorTileLayers[isSublayer ? parentId : id] + const vectorTileLayer = this._mapVisibilityLayers[isSublayer ? parentId : id] if (!vectorTileLayer) { return } @@ -172,7 +173,7 @@ export default class EsriLayerAdapter extends LayerAdapter { } async applyDatasetOpacity (datasetId) { - const vectorTileLayer = this._vectorTileOpacityLayers[datasetId] + const vectorTileLayer = this._mapOpacityLayers[datasetId] const registryDataset = datasetRegistry.getDataset(datasetId) if (vectorTileLayer && registryDataset) { vectorTileLayer.opacity = registryDataset.opacity @@ -180,7 +181,7 @@ export default class EsriLayerAdapter extends LayerAdapter { } async applyGlobalOpacity () { - Object.entries(this._vectorTileOpacityLayers).forEach(([datasetId, vectorTileLayer]) => { + Object.entries(this._mapOpacityLayers).forEach(([datasetId, vectorTileLayer]) => { const registryDataset = datasetRegistry.getDataset(datasetId) if (registryDataset) { vectorTileLayer.opacity = registryDataset.opacity @@ -211,9 +212,16 @@ export default class EsriLayerAdapter extends LayerAdapter { async onMapStyleChange () { datasetRegistry.forEach(registryDataset => { const { id, isSublayer, parent } = registryDataset - const vectorTileLayer = this._vectorTileLayers[isSublayer ? parent.id : id] - this._applyStyleLayerVisibility(registryDataset, vectorTileLayer) - this._applyStyleLayerPaintProperties(registryDataset, vectorTileLayer) + + // mapLayer could be a VectorTileLayer or a FeatureLayer, depending on the dataset type + const mapLayer = this._mapVisibilityLayers[isSublayer ? parent.id : id] + if (registryDataset.type === 'FeatureService') { + // FeatureLayers don't have style layers, so we don't need to apply style layer visibility or paint properties + mapLayer.renderer = registryDataset.renderer + } else { + this._applyStyleLayerVisibility(registryDataset, mapLayer) + this._applyStyleLayerPaintProperties(registryDataset, mapLayer) + } }) // TODO - handle dynamic sources } diff --git a/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js b/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js index 9194185d8..172ef854a 100644 --- a/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +++ b/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js @@ -71,38 +71,38 @@ describe('esriLayerAdapter', () => { describe('addDataset', () => { it('copes and returns when the dataset is not in the registry', async () => { await adapter.addDataset('unknown') - expect(adapter._vectorTileLayers.unknown).toBeUndefined() + expect(adapter._mapVisibilityLayers.unknown).toBeUndefined() }) it('adds a standalone dataset to the map and populates internal state', async () => { await adapter.addDataset('esri-standalone') - expect(adapter._vectorTileLayers['esri-standalone']).toBeDefined() - expect(adapter._vectorTileOpacityLayers['esri-standalone']).toBeDefined() + expect(adapter._mapVisibilityLayers['esri-standalone']).toBeDefined() + expect(adapter._mapOpacityLayers['esri-standalone']).toBeDefined() }) it('applies opacity and visibility after adding layers', async () => { await adapter.addDataset('esri-standalone') - const vtl = adapter._vectorTileLayers['esri-standalone'] + const vtl = adapter._mapVisibilityLayers['esri-standalone'] expect(vtl.visible).toBe(true) - expect(adapter._vectorTileOpacityLayers['esri-standalone'].opacity) + expect(adapter._mapOpacityLayers['esri-standalone'].opacity) .toBe(datasetRegistry.getDataset('esri-standalone').opacity) }) it('applies paint properties for datasets with esriStyleLayerId', async () => { await adapter.addDataset('esri-standalone') - const vtl = adapter._vectorTileLayers['esri-standalone'] + const vtl = adapter._mapVisibilityLayers['esri-standalone'] expect(vtl.setPaintProperties).toHaveBeenCalledWith('standalone-style', expect.any(Object)) }) it('does not apply paint properties for server-style datasets', async () => { await adapter.addDataset('esri-server') - const vtl = adapter._vectorTileLayers['esri-server'] + const vtl = adapter._mapVisibilityLayers['esri-server'] expect(vtl.setPaintProperties).not.toHaveBeenCalled() }) it('creates a group layer when adding a grouped dataset', async () => { await adapter.addDataset('esri-grouped') - expect(adapter._vectorTileLayers['esri-grouped']).toBeDefined() + expect(adapter._mapVisibilityLayers['esri-grouped']).toBeDefined() expect(adapter._groupLayers['my-group']).toBeDefined() }) }) @@ -122,36 +122,36 @@ describe('esriLayerAdapter', () => { it('removes a standalone vectorTileLayer from the map and clears internal state', async () => { await adapter._addLayers(datasetRegistry.getDataset('esri-standalone')) - const vtl = adapter._vectorTileLayers['esri-standalone'] + const vtl = adapter._mapVisibilityLayers['esri-standalone'] await adapter.removeDataset('esri-standalone') expect(map.remove).toHaveBeenCalledWith(vtl) - expect(adapter._vectorTileLayers['esri-standalone']).toBeUndefined() - expect(adapter._vectorTileOpacityLayers['esri-standalone']).toBeUndefined() + expect(adapter._mapVisibilityLayers['esri-standalone']).toBeUndefined() + expect(adapter._mapOpacityLayers['esri-standalone']).toBeUndefined() }) it('removes the vectorTileLayer from its group layer but keeps the group when other layers remain', async () => { await adapter._addLayers(datasetRegistry.getDataset('flood-zones-cc')) await adapter._addLayers(datasetRegistry.getDataset('flood-zones')) - const vtl = adapter._vectorTileLayers['flood-zones-cc'] + const vtl = adapter._mapVisibilityLayers['flood-zones-cc'] const groupLayer = adapter._groupLayers['flood-zones-group'] await adapter.removeDataset('flood-zones-cc') expect(groupLayer.remove).toHaveBeenCalledWith(vtl) expect(map.remove).not.toHaveBeenCalledWith(groupLayer) expect(adapter._groupLayers['flood-zones-group']).toBeDefined() - expect(adapter._vectorTileLayers['flood-zones-cc']).toBeUndefined() - expect(adapter._vectorTileOpacityLayers['flood-zones-cc']).toBeUndefined() + expect(adapter._mapVisibilityLayers['flood-zones-cc']).toBeUndefined() + expect(adapter._mapOpacityLayers['flood-zones-cc']).toBeUndefined() }) it('removes the group layer from the map when its last vectorTileLayer is removed', async () => { await adapter._addLayers(datasetRegistry.getDataset('esri-grouped')) - const vtl = adapter._vectorTileLayers['esri-grouped'] + const vtl = adapter._mapVisibilityLayers['esri-grouped'] const groupLayer = adapter._groupLayers['my-group'] await adapter.removeDataset('esri-grouped') expect(groupLayer.remove).toHaveBeenCalledWith(vtl) expect(map.remove).toHaveBeenCalledWith(groupLayer) expect(adapter._groupLayers['my-group']).toBeUndefined() - expect(adapter._vectorTileLayers['esri-grouped']).toBeUndefined() - expect(adapter._vectorTileOpacityLayers['esri-grouped']).toBeUndefined() + expect(adapter._mapVisibilityLayers['esri-grouped']).toBeUndefined() + expect(adapter._mapOpacityLayers['esri-grouped']).toBeUndefined() }) }) @@ -165,7 +165,7 @@ describe('esriLayerAdapter', () => { it('applies visibility for a known dataset', async () => { await adapter.applyDatasetVisibility('esri-standalone') - expect(adapter._vectorTileLayers['esri-standalone'].visible).toBe(true) + expect(adapter._mapVisibilityLayers['esri-standalone'].visible).toBe(true) }) it('does nothing for an unknown dataset', async () => { @@ -186,7 +186,7 @@ describe('esriLayerAdapter', () => { it('sets opacity on the opacity layer for a known dataset', async () => { await adapter._addLayers(datasetRegistry.getDataset('esri-standalone')) await adapter.applyDatasetOpacity('esri-standalone') - expect(adapter._vectorTileOpacityLayers['esri-standalone'].opacity) + expect(adapter._mapOpacityLayers['esri-standalone'].opacity) .toBe(datasetRegistry.getDataset('esri-standalone').opacity) }) @@ -206,14 +206,14 @@ describe('esriLayerAdapter', () => { it('sets opacity on all opacity layers', async () => { await adapter._addLayers(datasetRegistry.getDataset('esri-standalone')) await adapter.applyGlobalOpacity() - expect(adapter._vectorTileOpacityLayers['esri-standalone'].opacity) + expect(adapter._mapOpacityLayers['esri-standalone'].opacity) .toBe(datasetRegistry.getDataset('esri-standalone').opacity) }) it('skips entries whose dataset is not in the registry', async () => { - adapter._vectorTileOpacityLayers['ghost-id'] = { opacity: 99 } + adapter._mapOpacityLayers['ghost-id'] = { opacity: 99 } await expect(adapter.applyGlobalOpacity()).resolves.not.toThrow() - expect(adapter._vectorTileOpacityLayers['ghost-id'].opacity).toBe(99) + expect(adapter._mapOpacityLayers['ghost-id'].opacity).toBe(99) }) }) @@ -227,19 +227,19 @@ describe('esriLayerAdapter', () => { it('calls setStyleLayerVisibility for datasets with esriStyleLayerId', async () => { await adapter.onMapStyleChange() - expect(adapter._vectorTileLayers['esri-standalone'].setStyleLayerVisibility) + expect(adapter._mapVisibilityLayers['esri-standalone'].setStyleLayerVisibility) .toHaveBeenCalledWith('standalone-style', expect.any(String)) }) it('calls setPaintProperties for datasets not using server style', async () => { await adapter.onMapStyleChange() - expect(adapter._vectorTileLayers['esri-standalone'].setPaintProperties) + expect(adapter._mapVisibilityLayers['esri-standalone'].setPaintProperties) .toHaveBeenCalledWith('standalone-style', expect.any(Object)) }) it('does not call setPaintProperties when useServerStyle is true', async () => { await adapter.onMapStyleChange() - expect(adapter._vectorTileLayers['esri-server'].setPaintProperties).not.toHaveBeenCalled() + expect(adapter._mapVisibilityLayers['esri-server'].setPaintProperties).not.toHaveBeenCalled() }) }) @@ -251,14 +251,14 @@ describe('esriLayerAdapter', () => { }) it('adds VectorTileLayers for all top-level datasets', async () => { - expect(adapter._vectorTileLayers['flood-zones-cc']).toBeDefined() - expect(adapter._vectorTileLayers['flood-zones']).toBeDefined() + expect(adapter._mapVisibilityLayers['flood-zones-cc']).toBeDefined() + expect(adapter._mapVisibilityLayers['flood-zones']).toBeDefined() }) it('applies dataset visibility after adding layers', async () => { expect(adapter._groupLayers['flood-zones-group'].visible).toBe(true) - expect(adapter._vectorTileLayers['flood-zones-cc'].visible).toBe(true) - expect(adapter._vectorTileLayers['flood-zones'].visible).toBe(false) + expect(adapter._mapVisibilityLayers['flood-zones-cc'].visible).toBe(true) + expect(adapter._mapVisibilityLayers['flood-zones'].visible).toBe(false) }) it('adds GroupLayers for datasets with esriGroupId', async () => { diff --git a/plugins/datasets/src/adapters/esri/registry/esriDataset.js b/plugins/datasets/src/adapters/esri/registry/esriDataset.js index 322b4318f..9a7f2be81 100644 --- a/plugins/datasets/src/adapters/esri/registry/esriDataset.js +++ b/plugins/datasets/src/adapters/esri/registry/esriDataset.js @@ -24,4 +24,23 @@ export class EsriDataset extends Dataset { get useServerStyle () { return Boolean(this._datasetDefinition.esriUseServerStyle) } + + get renderer () { + if (this.type !== 'FeatureService') { + return undefined + } + const rendererDefinition = this._datasetDefinition.style?.renderer || this.parent?.renderer + if (!rendererDefinition) { + return undefined + } + const { mapStyle } = datasetRegistry + const renderer = JSON.parse(JSON.stringify(rendererDefinition)) + if (renderer.symbol?.color) { + renderer.symbol.color = getValueForStyle(renderer.symbol.color, mapStyle.id) + } + if (renderer.symbol?.outline?.color) { + renderer.symbol.outline.color = getValueForStyle(renderer.symbol.outline.color, mapStyle.id) + } + return renderer + } } From a9a02484bd29bf88e54a7a58700d27956495bd66 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 17 Jul 2026 08:34:05 +0100 Subject: [PATCH 45/53] IM-399 ignore __data__ for coverage --- jest.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/jest.config.mjs b/jest.config.mjs index 02f1cd4dd..871620da9 100755 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -22,6 +22,7 @@ export default { testPathIgnorePatterns: ['/src/test-utils.js'], coveragePathIgnorePatterns: [ '/__mocks__/', + '/__data__/', '/src/index.umd.js', '/stylelint.config.js', '/coverage', From a334730fdccf9a336e8c0f67b91e52942dac864e Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 17 Jul 2026 08:48:51 +0100 Subject: [PATCH 46/53] IM-399 Sonar issue handled --- jest.config.mjs | 2 +- plugins/datasets/src/adapters/esri/esriLayerAdapter.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/jest.config.mjs b/jest.config.mjs index 871620da9..c2981305b 100755 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -28,7 +28,7 @@ export default { '/coverage', '/demo', '/src/test-utils.js', - '/plugins/datasets/', + // '/plugins/datasets/', '/providers/beta/', '/plugins/beta/draw-es', '/plugins/beta/draw-ml', diff --git a/plugins/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js index ab80862b8..76341baaa 100644 --- a/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js @@ -78,6 +78,7 @@ export default class EsriLayerAdapter extends LayerAdapter { } catch (error) { logger.error(`Error adding FeatureLayer for dataset ${registryDataset.id}:`, error) } + return Promise.resolve() // Return a resolved promise to avoid unhandled promise rejection } async _addLayers (registryDataset) { From 2033b5949fc2ec4cdb0d05de26af0e4b7627deab Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 17 Jul 2026 11:14:18 +0100 Subject: [PATCH 47/53] IM-411 key visibility fix for menu based implementation --- plugins/datasets/src/adapters/esri/esriLayerAdapter.js | 2 +- plugins/datasets/src/initialise/DatasetsInit.jsx | 5 ++++- plugins/datasets/src/registry/dataset.js | 4 ++++ plugins/datasets/src/registry/datasetRegistry.js | 7 +++++-- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/plugins/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js index 76341baaa..3a1dd8afa 100644 --- a/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js @@ -78,7 +78,7 @@ export default class EsriLayerAdapter extends LayerAdapter { } catch (error) { logger.error(`Error adding FeatureLayer for dataset ${registryDataset.id}:`, error) } - return Promise.resolve() // Return a resolved promise to avoid unhandled promise rejection + return null } async _addLayers (registryDataset) { diff --git a/plugins/datasets/src/initialise/DatasetsInit.jsx b/plugins/datasets/src/initialise/DatasetsInit.jsx index 46df27c23..36e740e68 100755 --- a/plugins/datasets/src/initialise/DatasetsInit.jsx +++ b/plugins/datasets/src/initialise/DatasetsInit.jsx @@ -50,7 +50,10 @@ export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, m initDatasets() }, [isBaseMapReady, appState.mode]) - useEffect(() => setMenuState(pluginState.menuState), [pluginState.menuState]) + useEffect(() => { + setMenuState(pluginState.menuState) + datasetRegistry.invalidateKeyItems() + }, [pluginState.menuState]) useEffect(() => datasetRegistry.attach(pluginState.mappedDatasets, pluginState.orderedDatasets), [pluginState.mappedDatasets, pluginState.orderedDatasets]) diff --git a/plugins/datasets/src/registry/dataset.js b/plugins/datasets/src/registry/dataset.js index 77237ea20..770e65390 100644 --- a/plugins/datasets/src/registry/dataset.js +++ b/plugins/datasets/src/registry/dataset.js @@ -105,6 +105,10 @@ export class Dataset { return visible ? 'visible' : 'none' } + get keyVisibility () { + return this.visibility === 'visible' && this.showInKey + } + get symbolAnchor () { if (this.style?.symbolAnchor) { return this.style.symbolAnchor diff --git a/plugins/datasets/src/registry/datasetRegistry.js b/plugins/datasets/src/registry/datasetRegistry.js index 8b9a2e88f..c952e0428 100644 --- a/plugins/datasets/src/registry/datasetRegistry.js +++ b/plugins/datasets/src/registry/datasetRegistry.js @@ -83,6 +83,9 @@ const datasetRegistry = { }, { patternConfigs: [], symbolConfigs: [] }) }, + invalidateKeyItems () { + this._lastKeyItemsDatasets = null + }, _lastKeyItems: {}, keyItems () { if (this.datasets === this._lastKeyItemsDatasets) { @@ -93,11 +96,11 @@ const datasetRegistry = { const seenGroups = new Set() let hasGroups = false this.forEachDataset((dataset) => { - if (!(dataset.showInKey && dataset.visible)) { + if (!(dataset.showInKey && dataset.keyVisibility)) { return } if (dataset.hasSublayers) { - const sublayers = dataset.sublayers.filter(sublayer => sublayer.visible) + const sublayers = dataset.sublayers.filter(sublayer => sublayer.keyVisibility) if (sublayers.length) { hasGroups = true items.push({ type: 'sublayers', dataset, sublayers }) From 11d7318af95c269a0d6975ae84e68ee84e7e4a90 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 17 Jul 2026 11:14:57 +0100 Subject: [PATCH 48/53] IM-411 fixed surface water layers (wrong way round) --- demo/js/esri-datasets.js | 50 ++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 60c5b1ffe..b87969c0a 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -159,6 +159,9 @@ const surfaceWaterDataset = { tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Surface_Water_Spatial_Planning_1_in_1000_Depths_NON_PRODUCTION/VectorTileServer`, showInKey: true, sourceLayer: 'Surface Water Spatial Planning 1 in 1000 Depths', + style: { + fill: { outdoor: nonFloodZoneLight, dark: nonFloodZoneDark }, + }, visibleWhen: { menu: { dataset: ['surfacewater'], @@ -178,9 +181,6 @@ const surfaceWaterDataset = { aep: ['low'], depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200', 'depth2300', 'depthOver2300'] } - }, - style: { - fill: { outdoor: nonFloodZoneDepthBandsLight[0], dark: nonFloodZoneDepthBandsDark[0] }, } }, { @@ -193,9 +193,6 @@ const surfaceWaterDataset = { aep: ['low'], depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200', 'depth2300'] } - }, - style: { - fill: { outdoor: nonFloodZoneDepthBandsLight[1], dark: nonFloodZoneDepthBandsDark[1] }, } }, { @@ -208,9 +205,6 @@ const surfaceWaterDataset = { aep: ['low'], depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200'] } - }, - style: { - fill: { outdoor: nonFloodZoneDepthBandsLight[2], dark: nonFloodZoneDepthBandsDark[2] }, } }, { @@ -223,9 +217,6 @@ const surfaceWaterDataset = { aep: ['low'], depth: ['depth150', 'depth300', 'depth600', 'depth900'] } - }, - style: { - fill: { outdoor: nonFloodZoneDepthBandsLight[3], dark: nonFloodZoneDepthBandsDark[3] }, } }, { @@ -238,9 +229,6 @@ const surfaceWaterDataset = { aep: ['low'], depth: ['depth150', 'depth300', 'depth600'] } - }, - style: { - fill: { outdoor: nonFloodZoneDepthBandsLight[4], dark: nonFloodZoneDepthBandsDark[4] }, } }, { @@ -253,9 +241,6 @@ const surfaceWaterDataset = { aep: ['low'], depth: ['depth150', 'depth300'] } - }, - style: { - fill: { outdoor: nonFloodZoneDepthBandsLight[5], dark: nonFloodZoneDepthBandsDark[5] }, } }, { @@ -268,9 +253,6 @@ const surfaceWaterDataset = { aep: ['low'], depth: ['depth150'] } - }, - style: { - fill: { outdoor: nonFloodZoneDepthBandsLight[6], dark: nonFloodZoneDepthBandsDark[6] }, } }, ] @@ -283,9 +265,6 @@ const surfaceWaterDepthAllDataset = { tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Surface_Water_Spatial_Planning_1_in_1000_Depths_NON_PRODUCTION/VectorTileServer`, showInKey: true, sourceLayer: 'Surface Water Spatial Planning 1 in 1000 Depths', - style: { - fill: { outdoor: nonFloodZoneLight, dark: nonFloodZoneDark }, - }, visibleWhen: { menu: { dataset: ['surfacewater'], @@ -298,31 +277,52 @@ const surfaceWaterDepthAllDataset = { { id: 'depthOver2300', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/>2300mm/1', + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[0], dark: nonFloodZoneDepthBandsDark[0] }, + } }, { id: 'depth2300', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/1200-2300mm/1', + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[1], dark: nonFloodZoneDepthBandsDark[1] }, + } }, { id: 'depth1200', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/900-1200mm/1', + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[2], dark: nonFloodZoneDepthBandsDark[2] }, + } }, { id: 'depth900', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/600-900mm/1', + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[3], dark: nonFloodZoneDepthBandsDark[3] }, + } }, { id: 'depth600', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/300-600mm/1', + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[4], dark: nonFloodZoneDepthBandsDark[4] }, + } }, { id: 'depth300', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/150-300mm/1', + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[5], dark: nonFloodZoneDepthBandsDark[5] }, + } }, { id: 'depth150', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/<150mm/1', - } + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[6], dark: nonFloodZoneDepthBandsDark[6] }, + } + }, ] } From 5bd912cc77c5f9c6f3091b6f72e364088b1a635a Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 17 Jul 2026 12:05:40 +0100 Subject: [PATCH 49/53] IM-411 added a SW key only dataset --- demo/js/esri-datasets.js | 73 ++++++++++++++++++- .../src/adapters/esri/esriLayerAdapter.js | 5 +- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index b87969c0a..d69f048c6 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -172,8 +172,8 @@ const surfaceWaterDataset = { sublayers: [ { id: 'depthOver2300', - label: 'Depth over 2300mm', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/>2300mm/1', + showInKey: false, visibleWhen: { menu: { dataset: ['surfacewater'], @@ -186,6 +186,7 @@ const surfaceWaterDataset = { { id: 'depth2300', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/1200-2300mm/1', + showInKey: false, visibleWhen: { menu: { dataset: ['surfacewater'], @@ -198,6 +199,7 @@ const surfaceWaterDataset = { { id: 'depth1200', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/900-1200mm/1', + showInKey: false, visibleWhen: { menu: { dataset: ['surfacewater'], @@ -210,6 +212,7 @@ const surfaceWaterDataset = { { id: 'depth900', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/600-900mm/1', + showInKey: false, visibleWhen: { menu: { dataset: ['surfacewater'], @@ -222,6 +225,7 @@ const surfaceWaterDataset = { { id: 'depth600', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/300-600mm/1', + showInKey: false, visibleWhen: { menu: { dataset: ['surfacewater'], @@ -234,6 +238,7 @@ const surfaceWaterDataset = { { id: 'depth300', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/150-300mm/1', + showInKey: false, visibleWhen: { menu: { dataset: ['surfacewater'], @@ -246,6 +251,7 @@ const surfaceWaterDataset = { { id: 'depth150', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/<150mm/1', + showInKey: false, visibleWhen: { menu: { dataset: ['surfacewater'], @@ -277,6 +283,7 @@ const surfaceWaterDepthAllDataset = { { id: 'depthOver2300', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/>2300mm/1', + label: 'Extent over 2300mm', style: { fill: { outdoor: nonFloodZoneDepthBandsLight[0], dark: nonFloodZoneDepthBandsDark[0] }, } @@ -284,6 +291,7 @@ const surfaceWaterDepthAllDataset = { { id: 'depth2300', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/1200-2300mm/1', + label: 'Extent over 1200mm', style: { fill: { outdoor: nonFloodZoneDepthBandsLight[1], dark: nonFloodZoneDepthBandsDark[1] }, } @@ -291,6 +299,7 @@ const surfaceWaterDepthAllDataset = { { id: 'depth1200', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/900-1200mm/1', + label: 'Extent over 900mm', style: { fill: { outdoor: nonFloodZoneDepthBandsLight[2], dark: nonFloodZoneDepthBandsDark[2] }, } @@ -298,6 +307,7 @@ const surfaceWaterDepthAllDataset = { { id: 'depth900', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/600-900mm/1', + label: 'Extent over 600mm', style: { fill: { outdoor: nonFloodZoneDepthBandsLight[3], dark: nonFloodZoneDepthBandsDark[3] }, } @@ -305,6 +315,7 @@ const surfaceWaterDepthAllDataset = { { id: 'depth600', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/300-600mm/1', + label: 'Extent over 300mm', style: { fill: { outdoor: nonFloodZoneDepthBandsLight[4], dark: nonFloodZoneDepthBandsDark[4] }, } @@ -312,6 +323,7 @@ const surfaceWaterDepthAllDataset = { { id: 'depth300', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/150-300mm/1', + label: 'Extent over 150mm', style: { fill: { outdoor: nonFloodZoneDepthBandsLight[5], dark: nonFloodZoneDepthBandsDark[5] }, } @@ -319,6 +331,7 @@ const surfaceWaterDepthAllDataset = { { id: 'depth150', esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/<150mm/1', + label: 'Extent up to 150mm', style: { fill: { outdoor: nonFloodZoneDepthBandsLight[6], dark: nonFloodZoneDepthBandsDark[6] }, } @@ -326,6 +339,61 @@ const surfaceWaterDepthAllDataset = { ] } +const surfaceWaterExtentsKey = { + id: 'surfacewater-extents-key', + label: 'Surface Water', + groupLabel: 'Datasets', + showInKey: true, + style: { + stroke: { outdoor: nonFloodZoneLight, dark: nonFloodZoneDark }, + fill: { outdoor: nonFloodZoneLight, dark: nonFloodZoneDark }, + }, + sublayers: [ + { + id: 'key-150', + label: 'Full extend of flooding', + showInKey: true, + visibleWhen: { menu: { dataset: ['surfacewater'], depth: ['depth150'] } } + }, + { + id: 'key-300', + label: 'Extent over 150mm', + showInKey: true, + visibleWhen: { menu: { dataset: ['surfacewater'], depth: ['depth300'] } } + }, + { + id: 'key-600', + label: 'Extent over 300mm', + showInKey: true, + visibleWhen: { menu: { dataset: ['surfacewater'], depth: ['depth600'] } } + }, + { + id: 'key-900', + label: 'Extent over 600mm', + showInKey: true, + visibleWhen: { menu: { dataset: ['surfacewater'], depth: ['depth900'] } } + }, + { + id: 'key-1200', + label: 'Extent over 900mm', + showInKey: true, + visibleWhen: { menu: { dataset: ['surfacewater'], depth: ['depth1200'] } } + }, + { + id: 'key-2300', + label: 'Extent over 1200mm', + showInKey: true, + visibleWhen: { menu: { dataset: ['surfacewater'], depth: ['depth2300'] } } + }, + { + id: 'key-over-2300', + label: 'Extent over 2300mm', + showInKey: true, + visibleWhen: { menu: { dataset: ['surfacewater'], depth: ['depthOver2300'] } } + } + ] +} + const datasetMainRivers = { id: 'mainrivers', label: 'Main Rivers', @@ -406,7 +474,8 @@ const datasetFloodDefences = { } const datasets = [ - datasetFloodZonesCC, datasetFloodZones, surfaceWaterDataset, surfaceWaterDepthAllDataset, + datasetFloodZonesCC, datasetFloodZones, + surfaceWaterDataset, surfaceWaterDepthAllDataset, surfaceWaterExtentsKey, datasetWaterStorageAreas, datasetFloodDefences, datasetMainRivers ] diff --git a/plugins/datasets/src/adapters/esri/esriLayerAdapter.js b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js index 3a1dd8afa..8f0b6038b 100644 --- a/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/datasets/src/adapters/esri/esriLayerAdapter.js @@ -82,7 +82,10 @@ export default class EsriLayerAdapter extends LayerAdapter { } async _addLayers (registryDataset) { - const { type, esriGroupId } = registryDataset + const { type, esriGroupId, tiles } = registryDataset + if (!tiles) { + return + } if (type === 'FeatureService') { return this._addFeatureLayers(registryDataset) From 72bc2bcda1f1c58a1b81cf3c7e18bddafd861b37 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 17 Jul 2026 12:21:26 +0100 Subject: [PATCH 50/53] IM-411 removed need for showInMenu when menu is passed in config --- demo/js/esri-datasets.js | 10 ---------- plugins/datasets/src/manifest.js | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index d69f048c6..278e6adb7 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -45,7 +45,6 @@ const datasetFloodZonesCC = { esriGroupId: 'floodzones-group', tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_CCP1_NON_PRODUCTION/VectorTileServer`, showInKey: true, - showInMenu: true, visible: true, sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea CCP1', sublayers: [ @@ -54,7 +53,6 @@ const datasetFloodZonesCC = { label: 'Climate change (2070 to 2125)', esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Flood Zones plus climate change/1', showInKey: true, - showInMenu: false, visibleWhen: { menu: { dataset: ['floodzones'], timeframe: ['climatechange'] @@ -69,7 +67,6 @@ const datasetFloodZonesCC = { id: 'data-unavailable', label: 'Climate change data unavailable', showInKey: true, - showInMenu: false, visibleWhen: { menu: { dataset: ['floodzones'], timeframe: ['climatechange'] @@ -84,7 +81,6 @@ const datasetFloodZonesCC = { { id: 'data-unavailable-outline', showInKey: false, - showInMenu: false, visibleWhen: { menu: { dataset: ['floodzones'], timeframe: ['climatechange'] } }, @@ -102,7 +98,6 @@ const datasetFloodZonesCC = { esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/1', esriUseServerStyle: true, showInKey: false, - showInMenu: false, }, { id: 'data-unavailable-dark', @@ -113,7 +108,6 @@ const datasetFloodZonesCC = { esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea CCP1/Unavailable/2', esriUseServerStyle: true, showInKey: false, - showInMenu: false, } ] } @@ -125,7 +119,6 @@ const datasetFloodZones = { esriGroupId: 'floodzones-group', tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Zones_2_and_3_Rivers_and_Sea_NON_PRODUCTION/VectorTileServer`, showInKey: true, - // showInMenu: true, sourceLayer: 'Flood Zones 2 and 3 Rivers and Sea', visibleWhen: { menu: { dataset: ['floodzones'] } @@ -401,7 +394,6 @@ const datasetMainRivers = { type: 'FeatureService', tiles: 'https://services1.arcgis.com/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Statutory_Main_River_Map/FeatureServer', showInKey: true, - showInMenu: true, sourceLayer: 'Statutory_Main_River_Map', visible: false, style: { @@ -425,7 +417,6 @@ const datasetWaterStorageAreas = { type: 'FeatureService', tiles: 'https://services1.arcgis.com/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Flood_Storage_Areas_NON_PRODUCTION/FeatureServer', showInKey: true, - showInMenu: true, sourceLayer: 'Flood_Storage_Areas', visible: false, style: { @@ -456,7 +447,6 @@ const datasetFloodDefences = { type: 'FeatureService', tiles: 'https://services1.arcgis.com/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Defences_NON_PRODUCTION/FeatureServer', showInKey: true, - showInMenu: true, sourceLayer: 'Defences', visible: false, style: { diff --git a/plugins/datasets/src/manifest.js b/plugins/datasets/src/manifest.js index 04f6105b4..91041469a 100755 --- a/plugins/datasets/src/manifest.js +++ b/plugins/datasets/src/manifest.js @@ -67,7 +67,7 @@ export const manifest = { label: 'Layers', panelId: 'datasetsLayers', iconId: 'layers', - excludeWhen: ({ pluginConfig }) => !pluginConfig.datasets.some(l => + excludeWhen: ({ pluginConfig }) => !pluginConfig.menu && !pluginConfig.datasets.some(l => l.showInMenu || l.sublayers?.some(r => r.showInMenu) ), mobile: { From c45a7fb1c42c856cd74ea3ada516bd74a6757bdb Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 17 Jul 2026 13:08:33 +0100 Subject: [PATCH 51/53] IM-411 added surfaceWaterDatasetGenerator to demo --- demo/js/esri-datasets.js | 177 +++++++++++++++------------------------ 1 file changed, 66 insertions(+), 111 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 278e6adb7..7d3c5c1c3 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -145,118 +145,66 @@ const datasetFloodZones = { ] } -const surfaceWaterDataset = { - id: 'surfacewater', - label: 'Surface Water', - groupLabel: 'Datasets', - tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Surface_Water_Spatial_Planning_1_in_1000_Depths_NON_PRODUCTION/VectorTileServer`, - showInKey: true, - sourceLayer: 'Surface Water Spatial Planning 1 in 1000 Depths', - style: { - fill: { outdoor: nonFloodZoneLight, dark: nonFloodZoneDark }, - }, - visibleWhen: { - menu: { - dataset: ['surfacewater'], - timeframe: ['presentday'], - aep: ['low'], - } - }, - sublayers: [ - { - id: 'depthOver2300', - esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/>2300mm/1', - showInKey: false, - visibleWhen: { - menu: { - dataset: ['surfacewater'], - timeframe: ['presentday'], - aep: ['low'], - depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200', 'depth2300', 'depthOver2300'] - } - } - }, - { - id: 'depth2300', - esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/1200-2300mm/1', - showInKey: false, - visibleWhen: { - menu: { - dataset: ['surfacewater'], - timeframe: ['presentday'], - aep: ['low'], - depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200', 'depth2300'] - } - } - }, - { - id: 'depth1200', - esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/900-1200mm/1', - showInKey: false, - visibleWhen: { - menu: { - dataset: ['surfacewater'], - timeframe: ['presentday'], - aep: ['low'], - depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200'] - } - } - }, - { - id: 'depth900', - esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/600-900mm/1', - showInKey: false, - visibleWhen: { - menu: { - dataset: ['surfacewater'], - timeframe: ['presentday'], - aep: ['low'], - depth: ['depth150', 'depth300', 'depth600', 'depth900'] - } - } - }, - { - id: 'depth600', - esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/300-600mm/1', - showInKey: false, - visibleWhen: { - menu: { - dataset: ['surfacewater'], - timeframe: ['presentday'], - aep: ['low'], - depth: ['depth150', 'depth300', 'depth600'] - } - } - }, - { - id: 'depth300', - esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/150-300mm/1', - showInKey: false, - visibleWhen: { - menu: { - dataset: ['surfacewater'], - timeframe: ['presentday'], - aep: ['low'], - depth: ['depth150', 'depth300'] - } - } - }, - { - id: 'depth150', - esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/<150mm/1', - showInKey: false, - visibleWhen: { - menu: { - dataset: ['surfacewater'], - timeframe: ['presentday'], - aep: ['low'], - depth: ['depth150'] - } - } - }, - ] +const surfaceWaterDatasetGenerator = ({id, tileName, sourceLayer, timeframe, aep}) => { + const visibleWhenMenu = { dataset: ['surfacewater'], timeframe, aep } + const extentsDataset = { + id, + label: 'Surface Water', + groupLabel: 'Datasets', + tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/${tileName}/VectorTileServer`, + showInKey: true, + sourceLayer, + style: { fill: { outdoor: nonFloodZoneLight, dark: nonFloodZoneDark }, }, + visibleWhen: { menu: visibleWhenMenu }, + sublayers: [ + { + id: 'depthOver2300', + esriStyleLayerId: `${sourceLayer}/>2300mm/1`, + showInKey: false, + visibleWhen: { menu: {...visibleWhenMenu, depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200', 'depth2300', 'depthOver2300'] } }, + }, + { + id: 'depth2300', + esriStyleLayerId: `${sourceLayer}/1200-2300mm/1`, + showInKey: false, + visibleWhen: { menu: {...visibleWhenMenu, depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200', 'depth2300'] } }, + }, + { + id: 'depth1200', + esriStyleLayerId: `${sourceLayer}/900-1200mm/1`, + showInKey: false, + visibleWhen: { menu: {...visibleWhenMenu, depth: ['depth150', 'depth300', 'depth600', 'depth900', 'depth1200'] } }, + }, + { + id: 'depth900', + esriStyleLayerId: `${sourceLayer}/600-900mm/1`, + showInKey: false, + visibleWhen: { menu: {...visibleWhenMenu, depth: ['depth150', 'depth300', 'depth600', 'depth900'] } }, + }, + { + id: 'depth600', + esriStyleLayerId: `${sourceLayer}/300-600mm/1`, + showInKey: false, + visibleWhen: { menu: {...visibleWhenMenu, depth: ['depth150', 'depth300', 'depth600'] } }, + }, + { + id: 'depth300', + esriStyleLayerId: `${sourceLayer}/150-300mm/1`, + showInKey: false, + visibleWhen: { menu: {...visibleWhenMenu, depth: ['depth150', 'depth300'] } }, + }, + { + id: 'depth150', + esriStyleLayerId: `${sourceLayer}/<150mm/1`, + showInKey: false, + visibleWhen: { menu: {...visibleWhenMenu, depth: ['depth150'] } }, + }, + ] + } + return [extentsDataset] } + const surfaceWaterDepthAllDataset = { id: 'surfacewaterDepthAll', label: 'Surface Water Depth All', @@ -465,7 +413,14 @@ const datasetFloodDefences = { const datasets = [ datasetFloodZonesCC, datasetFloodZones, - surfaceWaterDataset, surfaceWaterDepthAllDataset, surfaceWaterExtentsKey, + ...surfaceWaterDatasetGenerator({ + id: 'surfacewater-present-day-low', + tileName: 'Surface_Water_Spatial_Planning_1_in_1000_Depths_NON_PRODUCTION', + sourceLayer: 'Surface Water Spatial Planning 1 in 1000 Depths', + timeframe: ['presentday'], + aep: ['low'], + }) + , surfaceWaterDepthAllDataset, surfaceWaterExtentsKey, datasetWaterStorageAreas, datasetFloodDefences, datasetMainRivers ] From 0fac68c1ad4fae257928381c7968cad5aa5378e1 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 17 Jul 2026 13:25:17 +0100 Subject: [PATCH 52/53] IM-411 shows All Surface Water Layers --- demo/js/esri-datasets.js | 123 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 115 insertions(+), 8 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 7d3c5c1c3..41a4cc3dd 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -148,7 +148,7 @@ const datasetFloodZones = { const surfaceWaterDatasetGenerator = ({id, tileName, sourceLayer, timeframe, aep}) => { const visibleWhenMenu = { dataset: ['surfacewater'], timeframe, aep } const extentsDataset = { - id, + id: `${id}-extents`, label: 'Surface Water', groupLabel: 'Datasets', tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/${tileName}/VectorTileServer`, @@ -201,7 +201,75 @@ const surfaceWaterDatasetGenerator = ({id, tileName, sourceLayer, timeframe, aep }, ] } - return [extentsDataset] + + const depthDataset = { + id: `${id}-depths`, + label: 'Surface Water Depth All', + groupLabel: 'Datasets', + tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/${tileName}/VectorTileServer`, + showInKey: true, + sourceLayer, + visibleWhen: { menu: {...visibleWhenMenu, depth: ['depthAll'] } }, + sublayers: [ + { + id: 'depthOver2300', + esriStyleLayerId: `${sourceLayer}/>2300mm/1`, + label: 'Extent over 2300mm', + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[0], dark: nonFloodZoneDepthBandsDark[0] }, + } + }, + { + id: 'depth2300', + esriStyleLayerId: `${sourceLayer}/1200-2300mm/1`, + label: 'Extent over 1200mm', + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[1], dark: nonFloodZoneDepthBandsDark[1] }, + } + }, + { + id: 'depth1200', + esriStyleLayerId: `${sourceLayer}/900-1200mm/1`, + label: 'Extent over 900mm', + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[2], dark: nonFloodZoneDepthBandsDark[2] }, + } + }, + { + id: 'depth900', + esriStyleLayerId: `${sourceLayer}/600-900mm/1`, + label: 'Extent over 600mm', + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[3], dark: nonFloodZoneDepthBandsDark[3] }, + } + }, + { + id: 'depth600', + esriStyleLayerId: `${sourceLayer}/300-600mm/1`, + label: 'Extent over 300mm', + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[4], dark: nonFloodZoneDepthBandsDark[4] }, + } + }, + { + id: 'depth300', + esriStyleLayerId: `${sourceLayer}/150-300mm/1`, + label: 'Extent over 150mm', + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[5], dark: nonFloodZoneDepthBandsDark[5] }, + } + }, + { + id: 'depth150', + esriStyleLayerId: `${sourceLayer}/<150mm/1`, + label: 'Extent up to 150mm', + style: { + fill: { outdoor: nonFloodZoneDepthBandsLight[6], dark: nonFloodZoneDepthBandsDark[6] }, + } + }, + ] + } + return [extentsDataset, depthDataset] } @@ -412,15 +480,54 @@ const datasetFloodDefences = { } const datasets = [ - datasetFloodZonesCC, datasetFloodZones, + datasetFloodZonesCC, + datasetFloodZones, + surfaceWaterExtentsKey, + // Surface Water Present Day ...surfaceWaterDatasetGenerator({ - id: 'surfacewater-present-day-low', - tileName: 'Surface_Water_Spatial_Planning_1_in_1000_Depths_NON_PRODUCTION', - sourceLayer: 'Surface Water Spatial Planning 1 in 1000 Depths', + id: 'surfacewater-presentday-low', + tileName: 'Surface_Water_Spatial_Planning_1_in_1000_Depths_NON_PRODUCTION', + sourceLayer: 'Surface Water Spatial Planning 1 in 1000 Depths', timeframe: ['presentday'], aep: ['low'], - }) - , surfaceWaterDepthAllDataset, surfaceWaterExtentsKey, + }), + ...surfaceWaterDatasetGenerator({ + id: 'surfacewater-presentday-medium', + tileName: 'Surface_Water_Spatial_Planning_1_in_100_Depths_NON_PRODUCTION', + sourceLayer: 'Surface Water Spatial Planning 1 in 100 Depths', + timeframe: ['presentday'], + aep: ['medium'], + }), + ...surfaceWaterDatasetGenerator({ + id: 'surfacewater-presentday-high', + tileName: 'Surface_Water_Spatial_Planning_1_in_30_Depths_NON_PRODUCTION', + sourceLayer: 'Surface Water Spatial Planning 1 in 30 Depths', + timeframe: ['presentday'], + aep: ['high'], + }), + // Surface Water Climate Change + ...surfaceWaterDatasetGenerator({ + id: 'surfacewater-climatechange-low', + tileName: 'Surface_Water_Spatial_Planning_1_in_1000_CCP1_Depths_NON_PRODUCTION', + sourceLayer: 'Surface Water Spatial Planning 1 in 1000 CCP1 Depths', + timeframe: ['climatechange'], + aep: ['low'], + }), + ...surfaceWaterDatasetGenerator({ + id: 'surfacewater-climatechange-medium', + tileName: 'Surface_Water_Spatial_Planning_1_in_100_CCP1_Depths_NON_PRODUCTION', + sourceLayer: 'Surface Water Spatial Planning 1 in 100 CCP1 Depths', + timeframe: ['climatechange'], + aep: ['medium'], + }), + ...surfaceWaterDatasetGenerator({ + id: 'surfacewater-climatechange-high', + tileName: 'Surface_Water_Spatial_Planning_1_in_30_CCP1_Depths_NON_PRODUCTION', + sourceLayer: 'Surface Water Spatial Planning 1 in 30 CCP1 Depths', + timeframe: ['climatechange'], + aep: ['high'], + }), + datasetWaterStorageAreas, datasetFloodDefences, datasetMainRivers ] From 0382a8368a349ca6d5e170aee5eeaa4626760b9b Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 17 Jul 2026 15:33:22 +0100 Subject: [PATCH 53/53] IM-411 esri demo has manifest to fix label to left --- demo/js/esri-datasets.js | 89 ++++-------------------- plugins/datasets/src/registry/dataset.js | 1 + 2 files changed, 14 insertions(+), 76 deletions(-) diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 41a4cc3dd..4e2517826 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -272,82 +272,6 @@ const surfaceWaterDatasetGenerator = ({id, tileName, sourceLayer, timeframe, aep return [extentsDataset, depthDataset] } - -const surfaceWaterDepthAllDataset = { - id: 'surfacewaterDepthAll', - label: 'Surface Water Depth All', - groupLabel: 'Datasets', - tiles: `https://tiles.arcgis.com/tiles/JZM7qJpmv7vJ0Hzx/arcgis/rest/services/Surface_Water_Spatial_Planning_1_in_1000_Depths_NON_PRODUCTION/VectorTileServer`, - showInKey: true, - sourceLayer: 'Surface Water Spatial Planning 1 in 1000 Depths', - visibleWhen: { - menu: { - dataset: ['surfacewater'], - timeframe: ['presentday'], - aep: ['low'], - depth: ['depthAll'] - }, - }, - sublayers: [ - { - id: 'depthOver2300', - esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/>2300mm/1', - label: 'Extent over 2300mm', - style: { - fill: { outdoor: nonFloodZoneDepthBandsLight[0], dark: nonFloodZoneDepthBandsDark[0] }, - } - }, - { - id: 'depth2300', - esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/1200-2300mm/1', - label: 'Extent over 1200mm', - style: { - fill: { outdoor: nonFloodZoneDepthBandsLight[1], dark: nonFloodZoneDepthBandsDark[1] }, - } - }, - { - id: 'depth1200', - esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/900-1200mm/1', - label: 'Extent over 900mm', - style: { - fill: { outdoor: nonFloodZoneDepthBandsLight[2], dark: nonFloodZoneDepthBandsDark[2] }, - } - }, - { - id: 'depth900', - esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/600-900mm/1', - label: 'Extent over 600mm', - style: { - fill: { outdoor: nonFloodZoneDepthBandsLight[3], dark: nonFloodZoneDepthBandsDark[3] }, - } - }, - { - id: 'depth600', - esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/300-600mm/1', - label: 'Extent over 300mm', - style: { - fill: { outdoor: nonFloodZoneDepthBandsLight[4], dark: nonFloodZoneDepthBandsDark[4] }, - } - }, - { - id: 'depth300', - esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/150-300mm/1', - label: 'Extent over 150mm', - style: { - fill: { outdoor: nonFloodZoneDepthBandsLight[5], dark: nonFloodZoneDepthBandsDark[5] }, - } - }, - { - id: 'depth150', - esriStyleLayerId: 'Surface Water Spatial Planning 1 in 1000 Depths/<150mm/1', - label: 'Extent up to 150mm', - style: { - fill: { outdoor: nonFloodZoneDepthBandsLight[6], dark: nonFloodZoneDepthBandsDark[6] }, - } - }, - ] -} - const surfaceWaterExtentsKey = { id: 'surfacewater-extents-key', label: 'Surface Water', @@ -605,6 +529,19 @@ const menu = [ ] const datasetsPlugin = createDatasetsPlugin({ + manifest: { + panels: [{ + id: 'datasetsLayers', + desktop: { open: true, slot: 'side', width: '280px', dismissible: false}, + tablet: { slot: 'side', width: '280px', modal: true } + }], + buttons: [ + { + id: 'datasetsLayers', + excludeWhen: ({ appState }) => (appState?.breakpoint === 'desktop'), + } + ] + }, globals: { opacityMode: 'global', // 'dataset', 'global' or 'multiply' opacity: 0.75, diff --git a/plugins/datasets/src/registry/dataset.js b/plugins/datasets/src/registry/dataset.js index 770e65390..cb1e178f2 100644 --- a/plugins/datasets/src/registry/dataset.js +++ b/plugins/datasets/src/registry/dataset.js @@ -97,6 +97,7 @@ export class Dataset { } get esriStyleLayerId () { return this._datasetDefinition.esriStyleLayerId } + get visibility () { const { visible, visibleWhen } = this if (visible && visibleWhen) {