diff --git a/demo/js/ml-datasets.js b/demo/js/ml-datasets.js index f120c221..635c0384 100644 --- a/demo/js/ml-datasets.js +++ b/demo/js/ml-datasets.js @@ -154,6 +154,7 @@ const landCoversDataset = { label: 'Permanent grassland 2', filter: ['in', ['get', 'dominant_land_cover'], ['literal', ['130', '131']]], // 'dominant_land_cover = "130"' showInMenu: true, + visible: false, style: { stroke: { outdoor: '#00897B', dark: '#ffffff' }, fillPattern: 'diagonal-cross-hatch', @@ -299,7 +300,11 @@ const hedgeControlDataset = { const datasetsPlugin = createDatasetsPlugin({ layerAdapter: maplibreLayerAdapter, - // Example: Dynamic bbox-based fetching (uncomment to test) + globals: { + opacityMode: 'multiply', // 'dataset', 'global' or 'multiply' + opacity: 0.75, + visible: true + }, datasets: [ landCoversDataset, existingFieldsDataset, @@ -368,13 +373,16 @@ const testVisibility = () => { setTimeout(() => datasetsPlugin.setDatasetVisibility(true, { datasetId: 'land-covers' }), 4000) // now reshow show landcovers-130-131 setTimeout(() => datasetsPlugin.setDatasetVisibility(true, { datasetId: 'land-covers', sublayerId: '130-131' }), 5000) + + // TODO + // setTimeout(() => datasetsPlugin.setDatasetVisibility(false, { rememberOriginalValues: false }), 5000) } const testGlobalVisibility = () => { setTimeout(() => datasetsPlugin.setDatasetVisibility(false), 1000) - setTimeout(() => datasetsPlugin.setDatasetVisibility(true), 5000) - setTimeout(() => datasetsPlugin.setDatasetVisibility(true, { datasetId: 'hedge-control' }), 500) - setTimeout(() => datasetsPlugin.setStyle({ stroke: { outdoor: '#0000ff' }, }, { datasetId: 'hedge-control' }), 2000) + setTimeout(() => datasetsPlugin.setDatasetVisibility(true), 2000) + // setTimeout(() => datasetsPlugin.setDatasetVisibility(true, { datasetId: 'hedge-control' }), 500) + // setTimeout(() => datasetsPlugin.setStyle({ stroke: { outdoor: '#0000ff' }, }, { datasetId: 'hedge-control' }), 2000) } const testFeatureVisibility = () => { @@ -385,13 +393,18 @@ const testFeatureVisibility = () => { setTimeout(() => datasetsPlugin.setFeatureVisibility(false, [16], { datasetId: 'land-covers-permanent-grassland-2', idProperty: null }), 2000) } -const testOpacity = () => { - setTimeout(() => datasetsPlugin.setOpacity(0.5, { datasetId: 'land-covers', sublayerId: '130-131' }), 500) - setTimeout(() => datasetsPlugin.setOpacity(0, { datasetId: 'land-covers' }), 500) - setTimeout(() => datasetsPlugin.setOpacity(0.8, { datasetId: 'land-covers', sublayerId: '130-131' }), 1000) - setTimeout(() => datasetsPlugin.setOpacity(0.3, { datasetId: 'land-covers', sublayerId: '130-131' }), 1500) - setTimeout(() => datasetsPlugin.setOpacity(0.97, { datasetId: 'land-covers' }), 2000) - // setTimeout(() => datasetsPlugin.setOpacity(1, { datasetId: 'land-covers', sublayerId: '130-131' }), 4000) +const testSetOpacity = () => { + setTimeout(() => datasetsPlugin.setOpacity(0.8, { datasetId: 'land-covers' }), 500) + setTimeout(() => datasetsPlugin.setOpacity(0.2, { datasetId: 'land-covers', sublayerId: '130-131' }), 2000) + // setTimeout(() => datasetsPlugin.setOpacity(0.8, { datasetId: 'land-covers', sublayerId: '130-131' }), 2000) + // setTimeout(() => datasetsPlugin.setOpacity(0.3, { datasetId: 'land-covers', sublayerId: '130-131' }), 2500) + setTimeout(() => datasetsPlugin.setOpacity(0.97, { datasetId: 'land-covers' }), 4000) + // setTimeout(() => datasetsPlugin.setOpacity(1, { datasetId: 'land-covers', sublayerId: '130-131' }), 6000) + + setTimeout(() => datasetsPlugin.setOpacity(0), 8000) + setTimeout(() => datasetsPlugin.setOpacity(1), 10000) + // TODO: + // setTimeout(() => datasetsPlugin.setGlobal({ opacityMode: 'multiply' }), 2000) } const testSetStyle = () => { @@ -459,15 +472,15 @@ const testSetData = () => { } interactiveMap.on('datasets:ready', function () { - testGetters() - testInvalidApiCalls() - testFeatureVisibility() - testOpacity() - testSetStyle() - testVisibility() - testGlobalVisibility() - testRemoveAndAddDataset() - testSetData() + // testGetters() + // testInvalidApiCalls() + // testFeatureVisibility() + testSetOpacity() + // testSetStyle() + // testVisibility() + // testGlobalVisibility() + // testRemoveAndAddDataset() + // testSetData() }) // Ref to the selected features diff --git a/plugins/beta/datasets/src/DatasetsInit.jsx b/plugins/beta/datasets/src/DatasetsInit.jsx index 49507058..cc9df73f 100755 --- a/plugins/beta/datasets/src/DatasetsInit.jsx +++ b/plugins/beta/datasets/src/DatasetsInit.jsx @@ -3,6 +3,7 @@ import { useEffect, useRef } from 'react' import { EVENTS } from '../../../../src/config/events.js' import { createDatasets } from './datasets.js' import { datasetRegistry } from './registry/datasetRegistry.js' +import { attachGlobalState } from './registry/globalDataset.js' const useLayerAdapterActions = (methodName, dispatch, pluginState, dependencies) => useEffect(() => { @@ -76,9 +77,15 @@ export function DatasetsInit ({ pluginConfig, pluginState, appState, mapState, m useEffect(() => { datasetRegistry.attach(datasetsRef.current, pluginState.orderedDatasets) }, [pluginState.mappedDatasets, pluginState.orderedDatasets]) + + useEffect(() => { + attachGlobalState(pluginState.globals) + }, [pluginState.globals]) + useLayerAdapterActions('applyStyle', dispatch, pluginState, [pluginState.layerAdapterActions.applyStyle]) useLayerAdapterActions('applyDatasetVisibility', dispatch, pluginState, [pluginState.layerAdapterActions.applyDatasetVisibility]) - useLayerAdapterActions('setOpacity', dispatch, pluginState, [pluginState.layerAdapterActions.setOpacity]) + useLayerAdapterActions('applyDatasetOpacity', dispatch, pluginState, [pluginState.layerAdapterActions.applyDatasetOpacity]) + useLayerAdapterActions('applyGlobalOpacity', dispatch, pluginState, [pluginState.layerAdapterActions.applyGlobalOpacity]) useLayerAdapterActions('addDataset', dispatch, pluginState, [pluginState.layerAdapterActions.addDataset]) useLayerAdapterActions('applyFeatureFilter', dispatch, pluginState, [pluginState.layerAdapterActions.applyFeatureFilter]) diff --git a/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.js b/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.js index e9c45597..e52726c3 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.js +++ b/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.js @@ -48,24 +48,34 @@ export class MapLibreDataset extends Dataset { return [this.symbolLayerId, this.fillLayerId, this.strokeLayerId].filter(Boolean) } - getLayersWithFilters () { + getLayersWithValue (valueName, condition = false) { const response = [] - if (this.hasHiddenFeatures) { + if (condition === false || this[condition]) { const layerIds = [this.symbolLayerId, this.fillLayerId, this.strokeLayerId].filter(Boolean) - const { filter } = this - response.push({ layerIds, filter }) + if (layerIds.length) { + const value = this[valueName] + response.push({ layerIds, [valueName]: value }) + } } if (this.hasSublayers) { this.sublayers.forEach((sublayer) => { - if (sublayer.hasHiddenFeatures) { - response.push(sublayer.getLayersWithFilters()[0]) + if (condition === false || sublayer[condition]) { + response.push(sublayer.getLayersWithValue(valueName)[0]) } }) } return response } + getLayersWithOpacity () { + return this.getLayersWithValue('opacity') + } + + getLayersWithFilters () { + return this.getLayersWithValue('filter', 'hasHiddenFeatures') + } + get sourceId () { if (this.isSublayer) { return this.parent.sourceId } if (this.hasDynamicGeoJSON) { return this.dynamicGeoJSON.sourceId } diff --git a/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.test.js b/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.test.js index aae9a381..5b6b75b8 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.test.js +++ b/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.test.js @@ -104,6 +104,37 @@ describe('MapLibreDataset', () => { }) }) + describe('getLayersWithOpacity', () => { + it('returns layerIds and opacity for a sublayer with no sublayers', () => { + const dataset = datasetRegistry.getDataset('existing-fields') + expect(dataset.getLayersWithOpacity()).toEqual([{ layerIds: ['existing-fields', 'existing-fields-stroke'], opacity: 1 }]) + }) + + it('returns layerIds and opacity for a sublayer', () => { + const dataset = datasetRegistry.getDataset('land-covers-130-131') + const result = dataset.getLayersWithOpacity() + expect(result).toEqual([{ layerIds: ['land-covers-130-131', 'land-covers-130-131-stroke'], opacity: 1 }]) + }) + + it('returns layerIds and opacity for a sublayer with sublayers with specific opacity', () => { + const parentDef = { id: 'parent-ds', sublayerIds: ['parent-ds-sub'] } + const subDef = { id: 'parent-ds-sub', parentId: 'parent-ds', style: { stroke: '#ff0000', fill: '#00ff00', opacity: 0.75 } } + datasetRegistry.attach({ 'parent-ds': parentDef, 'parent-ds-sub': subDef }) + const dataset = datasetRegistry.getDataset('parent-ds') + const result = dataset.getLayersWithOpacity() + expect(result).toEqual([{ layerIds: ['parent-ds-sub', 'parent-ds-sub-stroke'], opacity: 0.75 }]) + }) + + it('returns layerIds and opacity for all sublayers', () => { + const dataset = datasetRegistry.getDataset('historic-monuments') + expect(dataset.getLayersWithOpacity()).toEqual([ + { layerIds: ['historic-monuments-prehistoric'], opacity: 1 }, + { layerIds: ['historic-monuments-roman'], opacity: 1 }, + { layerIds: ['historic-monuments-medieval'], opacity: 1 } + ]) + }) + }) + describe('fillLayerId, strokeLayerId, symbolLayerId — hasSublayers returns null', () => { it('fillLayerId returns null for a dataset with sublayers', () => { const dataset = datasetRegistry.getDataset('land-covers') @@ -129,8 +160,7 @@ describe('MapLibreDataset', () => { it('returns an entry with layerIds and filter when the dataset has hidden features', () => { const dataset = datasetRegistry.getDataset('land-covers-130-131') - const result = dataset.getLayersWithFilters() - expect(result).toEqual([{ + expect(dataset.getLayersWithFilters()).toEqual([{ layerIds: ['land-covers-130-131', 'land-covers-130-131-stroke'], filter: ['all', ['!', ['in', ['to-string', ['get', 'id']], ['literal', ['42']]]], @@ -140,12 +170,13 @@ describe('MapLibreDataset', () => { it('includes sublayer entries when a sublayer has hidden features', () => { const parentDef = { id: 'parent-ds', sublayerIds: ['parent-ds-sub'] } - const subDef = { id: 'parent-ds-sub', parentId: 'parent-ds', hiddenFeatures: [7], style: { stroke: '#ff0000' } } + const subDef = { id: 'parent-ds-sub', parentId: 'parent-ds', hiddenFeatures: [7], style: { stroke: '#ff0000', fill: '#00ff00' } } datasetRegistry.attach({ 'parent-ds': parentDef, 'parent-ds-sub': subDef }) const dataset = datasetRegistry.getDataset('parent-ds') - const result = dataset.getLayersWithFilters() - expect(result).toHaveLength(1) - expect(result[0].layerIds).toContain('parent-ds-sub') + expect(dataset.getLayersWithFilters()).toEqual([{ + layerIds: ['parent-ds-sub', 'parent-ds-sub-stroke'], + filter: ['!', ['in', ['to-string', ['id']], ['literal', ['7']]]] + }]) }) it('returns an empty array when sublayers exist but none have hidden features', () => { diff --git a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js index 25534c44..52083131 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js @@ -203,19 +203,32 @@ export default class MaplibreLayerAdapter { } /** - * Set opacity for all layers belonging to a dataset. - * Uses setPaintProperty directly — safe to call on every slider tick. + * Apply opacity for all layers with datasetId * @param {string} datasetId - * @param {number} opacity */ - setOpacity (datasetId, opacity) { - const style = this._map.getStyle() - if (!style?.layers) { - return + applyDatasetOpacity (datasetId) { + const registryDataset = datasetRegistry.getDataset(datasetId) + if (registryDataset) { + this._applyRegistryDatasetOpacity(registryDataset) } - style.layers - .filter(layer => layer.id === datasetId || layer.id.startsWith(`${datasetId}-`)) - .forEach(layer => this._setPaintOpacity(layer.id, opacity)) + } + + /** + * Apply opacity for all layers belonging to a registryDataset. + * Uses setPaintProperty directly — safe to call on every slider tick. + * @param {Object} registryDataset + */ + _applyRegistryDatasetOpacity (registryDataset) { + registryDataset.getLayersWithOpacity().forEach(({ layerIds, opacity }) => { + layerIds.forEach(layerId => this._setPaintOpacity(layerId, opacity)) + }) + } + + /** + * Apply opacity for all layers + */ + applyGlobalOpacity () { + datasetRegistry.forEachDataset(registryDataset => this._applyRegistryDatasetOpacity(registryDataset)) } /** diff --git a/plugins/beta/datasets/src/api/setOpacity.js b/plugins/beta/datasets/src/api/setOpacity.js index d7d851ee..32783543 100644 --- a/plugins/beta/datasets/src/api/setOpacity.js +++ b/plugins/beta/datasets/src/api/setOpacity.js @@ -1,7 +1,8 @@ -export const setOpacity = ({ pluginState: { dispatch } }, opacity, { datasetId, sublayerId }) => { - datasetId = sublayerId ? `${datasetId}-${sublayerId}` : datasetId - if (datasetId) { - dispatch({ type: 'SET_OPACITY', payload: { datasetId, opacity } }) +export const setOpacity = ({ pluginState: { dispatch } }, opacity, options = {}) => { + const { datasetId, sublayerId } = options + const fullId = sublayerId ? `${datasetId}-${sublayerId}` : datasetId + if (fullId) { + dispatch({ type: 'SET_OPACITY', payload: { datasetId: fullId, opacity } }) } else { // Global update dispatch({ type: 'SET_GLOBAL_OPACITY', payload: { opacity } }) diff --git a/plugins/beta/datasets/src/datasets.js b/plugins/beta/datasets/src/datasets.js index ac3df9c8..8d024a63 100644 --- a/plugins/beta/datasets/src/datasets.js +++ b/plugins/beta/datasets/src/datasets.js @@ -15,9 +15,12 @@ export const createDatasets = ({ eventBus }) => { const { datasets } = pluginConfig - const dynamicSources = new Map() + if (pluginConfig.globals) { + dispatch({ type: 'INITIALISE_GLOBAL_STATE', payload: pluginConfig.globals }) + } + // Initialise all datasets via the adapter, then set up dynamic sources const processedDatasets = datasets.map(d => applyDatasetDefaults(d, datasetDefaults)) const { mappedDatasets, orderedDatasets } = mappedDatasetsReducer({ datasets }) diff --git a/plugins/beta/datasets/src/reducer.js b/plugins/beta/datasets/src/reducer.js index 55013185..4c43e3df 100755 --- a/plugins/beta/datasets/src/reducer.js +++ b/plugins/beta/datasets/src/reducer.js @@ -6,11 +6,10 @@ const initialState = { globals: { visible: true, opacity: 1, - // overrideDatasetOpacity: - // 'local': registryDataset opacity is used instead if set; + opacityMode: 'dataset' + // 'dataset': registryDataset opacity is used instead if set; // 'global': registryDataset opacity is ignored - // 'multiply': registryDataset opacity is multiplied by global opacity - overrideDatasetOpacity: 'global' + // 'multiply': registryDataset opacity is multiplied by parent opacity and global opacity }, key: { items: [], @@ -20,7 +19,8 @@ const initialState = { layerAdapterActions: { applyStyle: [], applyDatasetVisibility: [], - setOpacity: [], + applyDatasetOpacity: [], + applyGlobalOpacity: [], addDataset: [], applyFeatureFilter: [] } @@ -34,6 +34,13 @@ const validateDatasetExists = (state, datasetId, prefix, suffix = 'not found') = return true } +const initialiseGlobalState = (state, payload) => { + return { + ...state, + globals: { ...state.globals, ...payload } + } +} + const setDatasets = (state, payload) => { const { datasets, mappedDatasets, orderedDatasets } = payload const menu = payload.menu || datasetsToMenu({ datasets }) @@ -164,18 +171,20 @@ const setOpacity = (state, payload) => { } const style = { ...state.mappedDatasets[datasetId].style, opacity } const dataset = { ...state.mappedDatasets[datasetId], style } - const setOpacity = [...state.layerAdapterActions.setOpacity, [datasetId, opacity]] + const applyDatasetOpacity = [...state.layerAdapterActions.applyDatasetOpacity, [datasetId]] return { ...state, - layerAdapterActions: { ...state.layerAdapterActions, setOpacity }, + layerAdapterActions: { ...state.layerAdapterActions, applyDatasetOpacity }, mappedDatasets: { ...state.mappedDatasets, [datasetId]: dataset } } } const setGlobalOpacity = (state, payload) => { const { opacity } = payload + const applyGlobalOpacity = [...state.layerAdapterActions.applyDatasetOpacity, []] return { ...state, + layerAdapterActions: { ...state.layerAdapterActions, applyGlobalOpacity }, globals: { ...state.globals, opacity } } } @@ -194,7 +203,8 @@ const actions = { HIDE_FEATURES: hideFeatures, SHOW_FEATURES: showFeatures, SET_LAYER_ADAPTER: setLayerAdapter, - SET_LAYER_ADAPTER_ACTIONS: setLayerAdapterActions + SET_LAYER_ADAPTER_ACTIONS: setLayerAdapterActions, + INITIALISE_GLOBAL_STATE: initialiseGlobalState } export { diff --git a/plugins/beta/datasets/src/registry/__mocks__/datasetRegistry.js b/plugins/beta/datasets/src/registry/__mocks__/datasetRegistry.js index be291755..fbb2717b 100644 --- a/plugins/beta/datasets/src/registry/__mocks__/datasetRegistry.js +++ b/plugins/beta/datasets/src/registry/__mocks__/datasetRegistry.js @@ -1,8 +1,15 @@ import { mappedDatasetsReducer } from '../../reducers/mappedDatasetsReducer.js' import { datasets as datasetDefinitions } from '../../reducers/__data__/demoDatasets.js' +import { attachGlobalState } from '../globalDataset.js' const { datasetRegistry } = jest.requireActual('../datasetRegistry.js') const { mappedDatasets, orderedDatasets } = mappedDatasetsReducer({ datasets: datasetDefinitions }) +const globalState = { + opacityMode: 'dataset', + opacity: 1, + visible: true +} + // By adding jest.mock('/datasetRegistry.js') // to a test file, any import of datasetRegistry from that file will get this // version with the demo datasets attached for testing. @@ -11,6 +18,7 @@ const { mappedDatasets, orderedDatasets } = mappedDatasetsReducer({ datasets: da // and attach it in the specific test beforeEach(() => { datasetRegistry.attach(mappedDatasets, orderedDatasets) + attachGlobalState(globalState) }) datasetRegistry.mockExtend = (extraDatasets) => datasetRegistry.attach( diff --git a/plugins/beta/datasets/src/registry/dataset.js b/plugins/beta/datasets/src/registry/dataset.js index 6cbe1109..e7f9a325 100644 --- a/plugins/beta/datasets/src/registry/dataset.js +++ b/plugins/beta/datasets/src/registry/dataset.js @@ -2,6 +2,7 @@ import { datasetRegistry } from './datasetRegistry.js' import { hasCustomVisualStyle } from '../defaults.js' import { hasPattern } from '../../../../../src/utils/patternUtils.js' import { DynamicGeoJson } from './dynamicGeoJson.js' +import { calculateOpacity } from './globalDataset.js' export class Dataset { constructor (dataset) { @@ -25,7 +26,9 @@ export class Dataset { get showInKey () { return this._datasetDefinition.showInKey || this.parent?.showInKey || false } get groupLabel () { return this._datasetDefinition.groupLabel } get opacity () { - return this.style.opacity === undefined ? 1 : this.style.opacity + const myOpacity = this.style?.opacity + const parentOpacity = this.parent?.style?.opacity + return calculateOpacity(myOpacity, parentOpacity) } get hasDynamicGeoJSON () { @@ -101,7 +104,16 @@ export class Dataset { get style () { const parentStyle = this.parent?.style if (parentStyle) { - return { ...parentStyle, ...this._datasetDefinition.style, symbolDescription: this.symbolDescription } + // Here - we must set the merge styles opacity to undefined before the specific child opacity + // so that we can correctly calculate opacity in the Dataset.opacity getter + // - otherwise if opacity mode multiply is set, + // any child with a parent opacity only would be multiplied by itself + return { + ...parentStyle, + opacity: undefined, + ...this._datasetDefinition.style, + symbolDescription: this.symbolDescription + } } return this._datasetDefinition.style || {} } diff --git a/plugins/beta/datasets/src/registry/dataset.test.js b/plugins/beta/datasets/src/registry/dataset.test.js index c176af5e..3a18b793 100644 --- a/plugins/beta/datasets/src/registry/dataset.test.js +++ b/plugins/beta/datasets/src/registry/dataset.test.js @@ -1,9 +1,15 @@ import { Dataset } from './dataset.js' import { datasetRegistry } from './datasetRegistry.js' +import { attachGlobalState } from './globalDataset.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('./datasetRegistry.js') +const globalState = { + opacityMode: 'dataset', + opacity: 1, + visible: true +} describe('Dataset class', () => { describe('isSublayer', () => { it('returns false for a top-level dataset', () => { @@ -248,19 +254,141 @@ describe('Dataset class', () => { }) describe('opacity', () => { - it('returns 1 when no opacity is set in style', () => { - const dataset = new Dataset({ style: { stroke: '#ff0000' } }) - expect(dataset.opacity).toBe(1) - }) - - it('returns the opacity value from style', () => { - const dataset = new Dataset({ style: { opacity: 0.5 } }) - expect(dataset.opacity).toBe(0.5) - }) - - it('returns 0 when opacity is explicitly set to 0', () => { - const dataset = new Dataset({ style: { opacity: 0 } }) - expect(dataset.opacity).toBe(0) + const noOpacity = { + id: 'noOpacity' + } + const noOpacityChild = { + id: 'noOpacityChild', + parentId: 'noOpacity' + } + const opacityChild4 = { + id: 'opacityChild4', + parentId: 'noOpacity', + style: { opacity: 0.4 } + } + const zeroOpacity = { + id: 'zeroOpacity', + style: { opacity: 0 } + } + const opacity8 = { + id: 'opacity8', + style: { opacity: 0.8 } + } + const opacity8Child = { + id: 'opacity8Child', + parentId: 'opacity8', + style: { opacity: 0.4 } + } + const opacity8ChildNoOpacity = { + id: 'opacity8ChildNoOpacity', + parentId: 'opacity8' + } + beforeEach(() => { + datasetRegistry.attach({ + zeroOpacity, + noOpacity, + noOpacityChild, + opacity8, + opacity8Child, + opacity8ChildNoOpacity, + opacityChild4 + }) + }) + + describe('with opacityMode set to "dataset"', () => { + it('returns global opacity when no opacity', () => { + expect(datasetRegistry.getDataset('noOpacity').opacity).toBe(1) + }) + + it('returns global opacity when no opacity is set in child or parent', () => { + expect(datasetRegistry.getDataset('noOpacityChild').opacity).toBe(1) + }) + + it('returns the specified opacity when opacity is set in child only', () => { + expect(datasetRegistry.getDataset('opacityChild4').opacity).toBe(0.4) + }) + + it('returns the specified opacity for a topLevel Dataset', () => { + expect(datasetRegistry.getDataset('opacity8').opacity).toBe(0.8) + }) + + it('returns the specified opacity when opacity is set in parent only', () => { + expect(datasetRegistry.getDataset('opacity8ChildNoOpacity').opacity).toBe(0.8) + }) + + it('returns the specified opacity for a child Dataset', () => { + expect(datasetRegistry.getDataset('opacity8Child').opacity).toBe(0.4) + }) + + it('returns 0 when opacity is explicitly set to 0', () => { + expect(datasetRegistry.getDataset('zeroOpacity').opacity).toBe(0) + }) + }) + + describe('with opacityMode set to "multiply"', () => { + beforeEach(() => { + attachGlobalState({ + ...globalState, + opacityMode: 'multiply', + opacity: 0.75 + }) + }) + + it('returns global opacity when no opacity', () => { + expect(datasetRegistry.getDataset('noOpacity').opacity).toBe(0.75) + }) + + it('returns global opacity when no opacity is set in child or parent', () => { + expect(datasetRegistry.getDataset('noOpacityChild').opacity).toBe(0.75) + }) + + it('returns the multiplied opacity when opacity is set in child only', () => { + expect(datasetRegistry.getDataset('opacityChild4').opacity).toBe(0.3) + }) + + it('returns the multiplied opacity for a topLevel Dataset', () => { + // 0.8 (dataset opacity) * 0.75 (global opacity) + expect(datasetRegistry.getDataset('opacity8').opacity).toBe(0.6) + }) + + it('returns the multiplied opacity for a child Dataset', () => { + // 0.4 (dataset opacity) * 0.75 (global opacity) * 0.8 (parent dataset opacity) + expect(datasetRegistry.getDataset('opacity8Child').opacity).toBe(0.24) + }) + + it('returns 0 when opacity is explicitly set to 0', () => { + expect(datasetRegistry.getDataset('zeroOpacity').opacity).toBe(0) + }) + }) + + describe('with opacityMode set to "global"', () => { + beforeEach(() => { + attachGlobalState({ + ...globalState, + opacityMode: 'global', + opacity: 0.6 + }) + }) + + it('returns the global opacity when no opacity', () => { + expect(datasetRegistry.getDataset('noOpacity').opacity).toBe(0.6) + }) + + it('returns the global opacity when no opacity is set in child or parent', () => { + expect(datasetRegistry.getDataset('noOpacityChild').opacity).toBe(0.6) + }) + + it('returns the global opacity for a topLevel Dataset', () => { + expect(datasetRegistry.getDataset('opacity8').opacity).toBe(0.6) + }) + + it('returns the global opacity for a child Dataset', () => { + expect(datasetRegistry.getDataset('opacity8Child').opacity).toBe(0.6) + }) + + it('returns global opacity when opacity is explicitly set to 0', () => { + expect(datasetRegistry.getDataset('zeroOpacity').opacity).toBe(0.6) + }) }) }) diff --git a/plugins/beta/datasets/src/registry/globalDataset.js b/plugins/beta/datasets/src/registry/globalDataset.js new file mode 100644 index 00000000..0be35341 --- /dev/null +++ b/plugins/beta/datasets/src/registry/globalDataset.js @@ -0,0 +1,34 @@ +let globalState = {} + +export const attachGlobalState = (globalStateRef) => { + globalState = globalStateRef +} + +// TODO - remove this once testing finishes +window.getGlobalState = () => Object.freeze(globalState) + +const calculateLocalOpacity = (opacityValue, parentOpacityValue) => { + if (opacityValue === undefined) { + if (parentOpacityValue === undefined) { + return globalState.opacity + } + return parentOpacityValue + } + return opacityValue +} + +const multiplyOpacities = (opacityValue = 1, parentOpacityValue = 1) => { + return Math.round((opacityValue * parentOpacityValue * globalState.opacity + Number.EPSILON) * 100) / 100 +} + +export const calculateOpacity = (opacityValue, parentOpacityValue) => { + switch (globalState.opacityMode) { + case 'global': + return globalState.opacity + case 'multiply': + return multiplyOpacities(opacityValue, parentOpacityValue) + case 'dataset': + default: + return calculateLocalOpacity(opacityValue, parentOpacityValue) + } +}