diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 7591a512..a8200c8a 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -8,20 +8,81 @@ 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', + 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: '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, + // 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', - showInKey: true, - showInMenu: false, + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 2/1', + showInMenu: true, style: { fill: { outdoor: '#1d70b8', dark: '#7fcdbb' }, stroke: 'none' @@ -30,9 +91,8 @@ 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, + esriStyleLayerId: 'Flood Zones 2 and 3 Rivers and Sea/Flood Zone 3/1', + showInMenu: true, style: { fill: { outdoor: '#003078', dark: '#e5f5e0' }, stroke: 'none' @@ -51,8 +111,6 @@ const datasetsPlugin = createDatasetsPlugin({ datasets }) - - const interactiveMap = new InteractiveMap('map', { behaviour: 'mapOnly', mapProvider: esriProvider({ setupConfig: setupEsriConfig }), @@ -79,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 94526656..05e36b01 100644 --- a/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/esri/esriLayerAdapter.js @@ -1,69 +1,197 @@ 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' export default class EsriLayerAdapter { constructor (mapProvider, symbolRegistry, patternRegistry) { 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 + // 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) { + return new EsriDataset(datasetDefinition) } async init (mapStyle) { - console.log('EsriLayerAdapter init', mapStyle) - datasetRegistry.forEachDataset(registryDataset => this._addLayers(registryDataset, mapStyle)) + const topLevelDatasets = datasetRegistry.topLevelDatasets() + // ensure the datasets are added in order + for await (const registryDataset of topLevelDatasets) { + await this._addLayers(registryDataset, 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] } - _addLayers (registryDataset, mapStyle) { + 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: registryDataset.visible + visible: false }) - 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) } - async applyDatasetVisibility (...args) { - console.log('ESRI: applyDatasetVisibility', 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 + const { id, isSublayer, visible, parentId } = registryDataset + 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)) + } + vectorTileLayer.visible = visible } - async applyGlobalVisibility (...args) { - console.log('ESRI: applyGlobalVisibility', args) + async applyDatasetVisibility (datasetId) { + const registryDataset = datasetRegistry.getDataset(datasetId) + if (registryDataset) { + this._applyRegistryDatasetVisibility(registryDataset) + } } - async applyDatasetOpacity (...args) { - console.log('ESRI: applyDatasetOpacity', args) + async applyGlobalVisibility () { + datasetRegistry.forEachDataset(registryDataset => this._applyRegistryDatasetVisibility(registryDataset)) } - async applyGlobalOpacity (...args) { - console.log('ESRI: applyGlobalOpacity', args) + async applyDatasetOpacity (datasetId) { + const vectorTileLayer = this._vectorTileOpacityLayers[datasetId] + const registryDataset = datasetRegistry.getDataset(datasetId) + if (vectorTileLayer && registryDataset) { + vectorTileLayer.opacity = registryDataset.opacity + } + } + + async applyGlobalOpacity () { + 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 (...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, esriStyleLayerId, 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)) + } + } + }) + // 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 new file mode 100644 index 00000000..e3466716 --- /dev/null +++ b/plugins/beta/datasets/src/adapters/esri/registry/esriDataset.js @@ -0,0 +1,27 @@ +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 + } + + get esriGroupId () { + if (this._datasetDefinition.esriGroupId === undefined) { + return this.parent?.esriGroupId + } + return this._datasetDefinition.esriGroupId + } + + get useServerStyle () { + return Boolean(this._datasetDefinition.esriUseServerStyle) + } +} diff --git a/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.js b/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.js index 0bd1e77d..a274fe37 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/components/Key/KeyItem.jsx b/plugins/beta/datasets/src/components/Key/KeyItem.jsx index f931a7dc..b47a1ccc 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 (