diff --git a/src/modules/Map/Map/Map.js b/src/modules/Map/Map/Map.js index a4d60154..35988382 100644 --- a/src/modules/Map/Map/Map.js +++ b/src/modules/Map/Map/Map.js @@ -2,6 +2,7 @@ import React from 'react'; import mapBoxGl from 'mapbox-gl'; import PropTypes from 'prop-types'; import 'mapbox-gl/dist/mapbox-gl.css'; +import { connectState } from '../../State/context'; import { updateCluster } from '../services/cluster'; @@ -11,6 +12,7 @@ import CaptureControl from './components/CaptureControl'; import DrawControl from './components/DrawControl'; import PrintControl from './components/PrintControl'; import HomeControl from './components/HomeControl'; +import PermalinkControl from './components/PermalinkControl'; import './Map.scss'; @@ -27,6 +29,7 @@ export const CONTROL_CAPTURE = 'CaptureControl'; export const CONTROL_DRAW = 'DrawControl'; export const CONTROL_PRINT = 'PrintControl'; export const CONTROL_HOME = 'HomeControl'; +export const CONTROL_PERMALINK = 'PermalinkControl'; export const DEFAULT_CONTROLS = [{ control: CONTROL_ATTRIBUTION, @@ -79,6 +82,7 @@ export class MapComponent extends React.Component { CONTROL_DRAW, CONTROL_PRINT, CONTROL_HOME, + CONTROL_PERMALINK, ]), PropTypes.shape({ onAdd: PropTypes.func, @@ -173,7 +177,7 @@ export class MapComponent extends React.Component { const { map: { flyTo } } = this.props; flyTo(flyToConfig); } - } + }; updateMapProperties = prevProps => { const { @@ -218,7 +222,7 @@ export class MapComponent extends React.Component { if (JSON.stringify(customStyle) !== JSON.stringify(prevProps.customStyle)) { this.replaceLayers(prevProps.customStyle); } - } + }; focusOnSearchResult = ({ center, bounds }) => { const { map } = this.props; @@ -231,7 +235,7 @@ export class MapComponent extends React.Component { if (center) { map.setCenter(center); } - } + }; onSearchResultClick = onResultClick => ({ result, ...rest }) => { const { map } = this.props; @@ -245,7 +249,7 @@ export class MapComponent extends React.Component { } else { this.focusOnSearchResult(result); } - } + }; async initMapProperties () { const { @@ -386,6 +390,15 @@ export class MapComponent extends React.Component { map.addControl(controlInstance, position); break; } + case CONTROL_PERMALINK: { + const controlInstance = new PermalinkControl({ + ...this.props, + ...params, + }); + this.controls.push(controlInstance); + map.addControl(controlInstance, position); + break; + } default: { const controlInstance = typeof control === 'string' ? new mapBoxGl[control]({ ...params }) @@ -403,4 +416,4 @@ export class MapComponent extends React.Component { } } -export default MapComponent; +export default connectState('initialState')(MapComponent); diff --git a/src/modules/Map/Map/Map.scss b/src/modules/Map/Map/Map.scss index 9e38255c..793c313c 100644 --- a/src/modules/Map/Map/Map.scss +++ b/src/modules/Map/Map/Map.scss @@ -43,7 +43,8 @@ } // Taken from .mapboxgl-ctrl-group > button -.print-button .mapboxgl-ctrl-icon { +.mapboxgl-ctrl-permalink .mapboxgl-ctrl-icon, +.mapboxgl-ctrl-print .mapboxgl-ctrl-icon { width: 30px; height: 30px; display: block; diff --git a/src/modules/Map/Map/Map.test.js b/src/modules/Map/Map/Map.test.js index fe7a46f4..af3961f7 100644 --- a/src/modules/Map/Map/Map.test.js +++ b/src/modules/Map/Map/Map.test.js @@ -599,6 +599,19 @@ describe('controls', () => { expect(map.addControl).toHaveBeenCalledWith(instance.controls[0], 'top-right'); }); + it('should update permalink control state', () => { + const instance = new Map({}); + instance.props = { + map, + controls: [{ + control: 'PermalinkControl', + position: 'top-right', + }], + }; + instance.resetControls(); + expect(map.addControl).toHaveBeenCalledWith(instance.controls[0], 'top-right'); + }); + it('should focus on search result', () => { const instance = new Map({ map }); diff --git a/src/modules/Map/Map/components/PermalinkControl/PermalinkControl.js b/src/modules/Map/Map/components/PermalinkControl/PermalinkControl.js new file mode 100644 index 00000000..2422d723 --- /dev/null +++ b/src/modules/Map/Map/components/PermalinkControl/PermalinkControl.js @@ -0,0 +1,90 @@ +import { + Button, + ControlGroup, + Icon, + Popover, + PopoverPosition, +} from '@blueprintjs/core'; +import PropTypes from 'prop-types'; +import { stringify } from 'query-string'; +import React from 'react'; +import translateMock from '../../../../../utils/translate'; +import { DEFAULT_OPTIONS } from '../../../../State/Hash/withHashState'; + +import AbstractMapControl from '../../../helpers/AbstractMapControl'; + + +export class PermalinkControl extends AbstractMapControl { + static containerClassName = 'mapboxgl-ctrl mapboxgl-ctrl-group mapboxgl-ctrl-permalink'; + + static propTypes = { + /** Function used to translate wording. Takes key and object of options as parameters */ + translate: PropTypes.func, + }; + + static defaultProps = { + translate: translateMock({ + 'terralego.map.permalink_control.button_label': 'Get permalink', + }), + }; + + state = {}; + + inputRef = React.createRef(); + + options = DEFAULT_OPTIONS; + + generateHashString = () => { + const { initialState } = this.props; + const [currentUrl] = window.location.href.split('#'); + const url = `${currentUrl}#${stringify(initialState, this.options)}`; + this.setState({ url }); + }; + + copyToCliboard = () => { + const { current: textInput } = this.inputRef; + textInput.setSelectionRange(0, textInput.value.length); + textInput.focus(); + document.execCommand('copy'); + this.setState({ copySuccess: true }); + setTimeout(() => this.setState({ copySuccess: false }), 2000); + }; + + render () { + const { translate } = this.props; + const { url, copySuccess } = this.state; + return ( + + + + target.setSelectionRange(0, value.length)} + value={url} + readOnly + size={80} + /> + + + + +`; diff --git a/src/modules/Map/Map/components/PermalinkControl/index.js b/src/modules/Map/Map/components/PermalinkControl/index.js new file mode 100644 index 00000000..e08716f3 --- /dev/null +++ b/src/modules/Map/Map/components/PermalinkControl/index.js @@ -0,0 +1,3 @@ +import PermalinkControl from './PermalinkControl'; + +export default PermalinkControl; diff --git a/src/modules/Map/Map/components/PrintControl/PrintControl.js b/src/modules/Map/Map/components/PrintControl/PrintControl.js index 344fe717..e589f268 100644 --- a/src/modules/Map/Map/components/PrintControl/PrintControl.js +++ b/src/modules/Map/Map/components/PrintControl/PrintControl.js @@ -21,7 +21,7 @@ const ORIENTATION_PORTRAIT = 'portrait'; const ORIENTATION_LANDSCAPE = 'landscape'; export class PrintControl extends AbstractMapControl { - static containerClassName = 'mapboxgl-ctrl mapboxgl-ctrl-group print-button'; + static containerClassName = 'mapboxgl-ctrl mapboxgl-ctrl-group mapboxgl-ctrl-print'; static propTypes = { translate: PropTypes.func, diff --git a/src/modules/Map/Map/hash.js b/src/modules/Map/Map/hash.js index a9f05db2..4677f4e6 100644 --- a/src/modules/Map/Map/hash.js +++ b/src/modules/Map/Map/hash.js @@ -9,7 +9,6 @@ import debounce from 'lodash.debounce'; */ class Hash { constructor (hashName) { - this._onHashChange.bind(this); this._hashName = hashName; // Mobile Safari doesn't allow updating the hash more than 100 times per 30 seconds. @@ -68,7 +67,7 @@ class Hash { return hash; } - _onHashChange () { + _onHashChange = () => { let loc = ''; if (this._hashName) { const params = new URLSearchParams(window.location.hash.slice(1)); diff --git a/src/modules/Map/Map/withMap.js b/src/modules/Map/Map/withMap.js index 3305189b..a60339c1 100644 --- a/src/modules/Map/Map/withMap.js +++ b/src/modules/Map/Map/withMap.js @@ -20,7 +20,6 @@ export const withMap = WrappedComponent => fitBounds: PropTypes.shape({ coordinates: PropTypes.arrayOf( PropTypes.array, - PropTypes.array, ), padding: PropTypes.shape({ top: PropTypes.number, @@ -30,7 +29,6 @@ export const withMap = WrappedComponent => }), offset: PropTypes.arrayOf( PropTypes.number, - PropTypes.number, ), }), onMapInit: PropTypes.func, diff --git a/src/modules/State/Hash/index.js b/src/modules/State/Hash/index.js new file mode 100644 index 00000000..fd583701 --- /dev/null +++ b/src/modules/State/Hash/index.js @@ -0,0 +1,4 @@ +import StateProvider from '../StateProvider'; +import { withHashState } from './withHashState'; + +export default withHashState()(StateProvider); diff --git a/src/modules/State/Hash/withHashState.js b/src/modules/State/Hash/withHashState.js new file mode 100644 index 00000000..c3e3b23e --- /dev/null +++ b/src/modules/State/Hash/withHashState.js @@ -0,0 +1,72 @@ +import PropTypes from 'prop-types'; +import { stringify, parse } from 'query-string'; +import React from 'react'; + + +export const DEFAULT_OPTIONS = { + encode: false, + arrayFormat: 'comma', + sort: false, + parseNumbers: true, + parseBooleans: true, +}; + +/** + * Decorator for getting an initialState from hash. + */ +export const withHashState = () => WrappedComponent => + class WithHashState extends React.Component { + static propTypes = { + listenHash: PropTypes.bool, + updateHash: PropTypes.bool, + }; + + static defaultProps = { + listenHash: true, + updateHash: true, + }; + + options = DEFAULT_OPTIONS; + + componentDidMount () { + const { listenHash } = this.props; + if (listenHash) { + window.addEventListener('hashchange', this.onHashChange, false); + } + } + + componentWillUnmount () { + this.isUnmount = true; + window.removeEventListener('hashchange', this.onHashChange, false); + } + + onHashChange = () => { + if (!this.isUnmount) { + this.forceUpdate(); + } + }; + + getCurrentHashString = state => `#${stringify(state, this.options)}`; + + updateHashString = state => { + const { updateHash } = this.props; + if (updateHash) { + window.history.replaceState(window.history.state, '', `#${stringify(state, this.options)}`); + } + }; + + render () { + const { listenHash, updateHash, ...props } = this.props; + const initialState = parse(window.location.hash, this.options); + return ( + + ); + } + }; + +export default withHashState; diff --git a/src/modules/State/Hash/withHashState.test.js b/src/modules/State/Hash/withHashState.test.js new file mode 100644 index 00000000..030facb9 --- /dev/null +++ b/src/modules/State/Hash/withHashState.test.js @@ -0,0 +1,61 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import withHashState from './withHashState'; + +const Component = jest.fn(() => null); +const ComponentWithHash = withHashState()(Component); + +it('should not set listeners', () => { + jest.spyOn(window, 'addEventListener'); + jest.spyOn(window.history, 'replaceState'); + + const wrapper = shallow( + , + ); + const instance = wrapper.instance(); + expect(window.addEventListener).not.toHaveBeenCalled(); + + instance.updateHashString({ foo: 'bar' }); + expect(window.history.replaceState).not.toHaveBeenCalled(); +}); + +it('should get correct parameters', () => { + jest.spyOn(window, 'addEventListener'); + jest.spyOn(window, 'removeEventListener'); + + window.location.hash = '#myparam=1&foo=bar&baz=false'; + + const wrapper = shallow( + , + ); + const listener = wrapper.instance().onHashChange; + expect(window.addEventListener).toHaveBeenCalledWith('hashchange', listener, false); + listener(); + + const { initialState } = wrapper.props(); + expect(initialState).toEqual({ + myparam: 1, + foo: 'bar', + baz: false, + }); + expect(wrapper.instance().getCurrentHashString(initialState)).toEqual(window.location.hash); + + wrapper.unmount(); + expect(window.removeEventListener).toHaveBeenCalledWith('hashchange', listener, false); + listener(); +}); + +it('should set correct parameters', () => { + const instance = shallow( + , + ).instance(); + + instance.updateHashString({ myparam: 1 }); + expect(window.location.hash).toEqual('#myparam=1'); + + instance.updateHashString({ myparam: 1, foo: 'bar' }); + expect(window.location.hash).toEqual('#myparam=1&foo=bar'); + + instance.updateHashString({ myparam: 0, baz: false }); + expect(window.location.hash).toEqual('#myparam=0&baz=false'); +}); diff --git a/src/modules/State/StateProvider.js b/src/modules/State/StateProvider.js new file mode 100644 index 00000000..05d210cc --- /dev/null +++ b/src/modules/State/StateProvider.js @@ -0,0 +1,40 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import context from './context'; + +const { Provider } = context; + +export class StateProvider extends React.Component { + static propTypes = { + onStateChange: PropTypes.func, + }; + + static defaultProps = { + onStateChange () {}, + }; + + componentWillMount () { + const { initialState } = this.props; + this.setState(initialState); + } + + setCurrentState = state => { + const { onStateChange } = this.props; + this.setState(state, () => onStateChange(this.state)); + }; + + render () { + const { children } = this.props; + return ( + + {children} + + ); + } +} + +export default StateProvider; diff --git a/src/modules/State/StateProvider.test.js b/src/modules/State/StateProvider.test.js new file mode 100644 index 00000000..fb67c20c --- /dev/null +++ b/src/modules/State/StateProvider.test.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import StateProvider from './StateProvider'; + + +it('should set state', () => { + const onStateChange = jest.fn(); + const wrapper = shallow( + , + ); + + expect(wrapper.state()).toEqual({ foo: 'bar' }); + + wrapper.instance().setCurrentState({ foo: 'baz' }); + expect(wrapper.state()).toEqual({ foo: 'baz' }); + expect(onStateChange).toHaveBeenCalled(); +}); diff --git a/src/modules/State/context.js b/src/modules/State/context.js new file mode 100644 index 00000000..ab96143e --- /dev/null +++ b/src/modules/State/context.js @@ -0,0 +1,7 @@ +import React from 'react'; +import connect from 'react-ctx-connect'; + +export const context = React.createContext(); +export const connectState = connect(context); + +export default context; diff --git a/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.js b/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.js index 0652af98..d1843700 100644 --- a/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.js +++ b/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { connectState } from '../../../State/context'; import context from './context'; import { @@ -14,8 +15,13 @@ export class LayersTreeProvider extends React.Component { static propTypes = { /** Callback executed everytime layersTreeState change. Takes layersTreeState as parameter */ onChange: PropTypes.func, + /** Initial state */ + initialState: PropTypes.shape({ + layers: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), // Active layer id(s) + table: PropTypes.string, // Layer if table is active + }), /** Initial layer tree state */ - initialState: PropTypes.instanceOf(Map), + initialLayersTreeState: PropTypes.instanceOf(Map), /** * Function called when a filter property of single or many type need to fetch values * Takes `layer` and `property` as parameters @@ -25,25 +31,28 @@ export class LayersTreeProvider extends React.Component { /** * Function called when a filter property of range type need to fetch min and max * Takes `layer` and `property` as parameters - * @return {min: Number, max: Number} + * @return {{}} Object of form {min: Number, max: Number} * */ fetchPropertyRange: PropTypes.func, translate: PropTypes.func, - } + setCurrentState: PropTypes.func, + }; static defaultProps = { onChange () {}, - initialState: new Map(), + initialState: {}, + initialLayersTreeState: new Map(), fetchPropertyValues () {}, fetchPropertyRange () {}, translate: translateMock({ 'visualizer.layerstree.group.selector': 'No layer found', }), - } + setCurrentState () {}, + }; constructor (props) { super(props); - const { initialState: layersTreeState } = this.props; + const { initialLayersTreeState: layersTreeState } = this.props; this.state = { layersTreeState }; } @@ -51,11 +60,11 @@ export class LayersTreeProvider extends React.Component { this.initLayersState(); } - componentDidUpdate ({ initialState: prevInitialState, layersTree: prevLayersTree }) { - const { initialState, layersTree } = this.props; + componentDidUpdate ({ initialLayersTreeState: prevLayersTreeState, layersTree: prevLayersTree }) { + const { initialLayersTreeState, layersTree } = this.props; - if (initialState !== prevInitialState) { - this.initLayersState(initialState); + if (initialLayersTreeState !== prevLayersTreeState) { + this.initLayersState(initialLayersTreeState); } if (layersTree !== prevLayersTree) { @@ -71,12 +80,12 @@ export class LayersTreeProvider extends React.Component { this.resetState(({ layersTreeState }) => ({ layersTreeState: setLayerStateAction(layer, newState, layersTreeState, reset), })); - } + }; getLayerState = ({ layer }) => { const { layersTreeState } = this.state; return layersTreeState.get(layer) || {}; - } + }; fetchPropertyValues = async (layer, property) => { const { fetchPropertyValues } = this.props; @@ -91,7 +100,7 @@ export class LayersTreeProvider extends React.Component { property.values = [...properties]; const { layersTreeState: newLayersTreeState } = this.state; this.resetState(new Map(newLayersTreeState)); - } + }; fetchPropertyRange = async (layer, property) => { const { fetchPropertyRange } = this.props; @@ -110,29 +119,44 @@ export class LayersTreeProvider extends React.Component { /* eslint-enable no-param-reassign */ const { layersTreeState: newLayersTreeState } = this.state; this.resetState(new Map(newLayersTreeState)); - } + }; - initLayersState = initialState => { + initLayersState = initialLayersTreeState => { this.resetState(({ layersTreeState }) => { - const { layersTree } = this.props; - const state = initialState || layersTreeState; + const { layersTree, initialState } = this.props; + const state = initialLayersTreeState || layersTreeState; if (!layersTree) return {}; return { layersTreeState: state.size ? state - : initLayersStateAction(layersTree), + : initLayersStateAction(layersTree, initialState), }; }); - } + }; resetState (state, callback = () => {}) { + const { setCurrentState } = this.props; + this.setState(state, () => { callback(); const { onChange } = this.props; const { layersTreeState } = this.state; + // Simplify the state from the map + const activeLayers = []; + let table = null; + layersTreeState && layersTreeState.forEach((layerState, { layers: [layerId] = [] }) => { + if (layerState.active) { + activeLayers.push(layerId); + } + if (layerState.table) { + table = layerId; + } + }); + setCurrentState({ layers: activeLayers, table: table || undefined }); + onChange(layersTreeState); }); } @@ -169,4 +193,4 @@ export class LayersTreeProvider extends React.Component { } } -export default LayersTreeProvider; +export default connectState('initialState', 'setCurrentState')(LayersTreeProvider); diff --git a/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.test.js b/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.test.js index 47a2ff68..d2b54bb0 100644 --- a/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.test.js +++ b/src/modules/Visualizer/LayersTree/LayersTreeProvider/LayersTreeProvider.test.js @@ -1,14 +1,12 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import LayersTreeProvider from './LayersTreeProvider'; +import { LayersTreeProvider } from './LayersTreeProvider'; import { connectLayersTree } from './context'; -import { setLayerStateAction, initLayersStateAction } from '../../services/layersTreeUtils'; +import * as layersTreeUtils from '../../services/layersTreeUtils'; -jest.mock('../../services/layersTreeUtils', () => ({ - initLayersStateAction: jest.fn(), - setLayerStateAction: jest.fn(), -})); +const initLayersStateAction = jest.spyOn(layersTreeUtils, 'initLayersStateAction').mockImplementation(jest.fn()); +const setLayerStateAction = jest.spyOn(layersTreeUtils, 'setLayerStateAction').mockImplementation(jest.fn()); beforeEach(() => { initLayersStateAction.mockClear(); @@ -40,12 +38,12 @@ it('should update', () => { expect(instance.resetState).not.toHaveBeenCalled(); expect(instance.initLayersState).not.toHaveBeenCalled(); - instance.props.initialState = {}; - instance.componentDidUpdate({ }); - expect(instance.initLayersState).toHaveBeenCalledWith(instance.props.initialState); + instance.props.initialLayersTreeState = {}; + instance.componentDidUpdate({}); + expect(instance.initLayersState).toHaveBeenCalledWith(instance.props.initialLayersTreeState); instance.props.layersTree = []; - instance.componentDidUpdate({ }); + instance.componentDidUpdate({}); expect(instance.initLayersState).toHaveBeenCalledWith(); }); @@ -139,6 +137,7 @@ it('should fetch property ranges', async () => { it('should init layers state', () => { const instance = new LayersTreeProvider({ initialState: new Map() }); + instance.props.getInitialState = jest.fn(); instance.resetState = jest.fn(); instance.initLayersState(); expect(instance.resetState).toHaveBeenCalled(); @@ -159,3 +158,124 @@ it('should init layers state', () => { }); expect(initLayersStateAction).toHaveBeenCalled(); }); + +it('should get layers state from hash', () => { + initLayersStateAction.mockRestore(); + + const layer1 = { layers: ['thatlayerid'] }; + const layer2 = { layers: ['t'] }; + const layersTreeState = new Map(); + + const initialState = { + layers: ['thatlayerid', 'b'], + }; + + const instance = new LayersTreeProvider({ + initialState, + layersTree: [layer1, layer2], + }); + + instance.resetState = jest.fn(stateFn => stateFn({ layersTreeState })); + instance.initLayersState(); + expect(instance.resetState.mock.calls[0][0]({ layersTreeState: new Map() })).toEqual({ + layersTreeState: new Map([ + [layer1, { + active: true, + opacity: 1, + }], + [layer2, { + active: false, + opacity: 1, + }], + ]), + }); + + instance.props.initialState = { + layers: 'thatlayerid', + }; + instance.initLayersState(); + expect(instance.resetState.mock.calls[1][0]({ layersTreeState: new Map() })).toEqual({ + layersTreeState: new Map([ + [layer1, { + active: true, + opacity: 1, + }], + [layer2, { + active: false, + opacity: 1, + }], + ]), + }); + + instance.props.initialState = { + layers: ['thatlayerid', 't'], + table: 'thatlayerid', + }; + instance.initLayersState(); + expect(instance.resetState.mock.calls[2][0]({ layersTreeState: new Map() })).toEqual({ + layersTreeState: new Map([ + [layer1, { + active: true, + opacity: 1, + table: true, + }], + [layer2, { + active: true, + opacity: 1, + }], + ]), + }); +}); + +it('should set layers state from hash', () => { + const setCurrentState = jest.fn(); + const layer1 = { layers: ['thatlayerid'] }; + const layer2 = { layers: ['t'] }; + const layer3 = {}; + const instance = new LayersTreeProvider({ + setCurrentState, + onChange: jest.fn(), + }); + + instance.state = { + layersTreeState: new Map([ + [layer1, { + active: true, + opacity: 1, + }], + [layer2, { + active: false, + opacity: 1, + }], + [layer3, {}], + ]), + }; + instance.setState = jest.fn( + (stateFn, callback) => callback(stateFn(instance.state)), + ); + instance.initLayersState(); + + expect(setCurrentState).toHaveBeenCalledWith({ + layers: ['thatlayerid'], + table: undefined, + }); + + instance.state = { + layersTreeState: new Map([ + [layer1, { + active: true, + opacity: 1, + }], + [layer2, { + active: true, + opacity: 1, + table: true, + }], + ]), + }; + instance.initLayersState(); + expect(setCurrentState).toHaveBeenCalledWith({ + layers: ['thatlayerid', 't'], + table: 't', + }); +}); diff --git a/src/modules/Visualizer/services/layersTreeUtils.js b/src/modules/Visualizer/services/layersTreeUtils.js index 8836e979..b2d49c5c 100644 --- a/src/modules/Visualizer/services/layersTreeUtils.js +++ b/src/modules/Visualizer/services/layersTreeUtils.js @@ -4,11 +4,20 @@ export const INITIAL_FILTERS = new Map(); export const isCluster = (source, layerId) => !!source.match(new RegExp(`^${layerId}-${PREFIX_SOURCE}-[0-9]+`)); -export function initLayersStateAction (layersTree) { +/** + * Returns a flattened map of layers state from a layers tree + * + * @param {object} layersTree The layers tree config object + * @param {string|string[]} layers Active layer(s) from hash + * @param {string} table Active table from hash (layer id) + * @return {Map} A reduced layer tree state + */ +export const initLayersStateAction = (layersTree, { layers, table } = {}) => { const layersTreeState = new Map(); + function reduceLayers (group, map) { return group.reduce((layersStateMap, layer) => { - const { initialState = {} } = layer; + const { initialState = {}, layers: [layerId] = [] } = layer; if (layer.group) { return reduceLayers(layer.layers, layersStateMap); } @@ -16,6 +25,15 @@ export function initLayersStateAction (layersTree) { ? 1 : initialState.opacity; + if (layers) { + initialState.active = ( + Array.isArray(layers) && layers.includes(layerId)) + || layers === layerId; + } + if (table && table === layerId) { + initialState.table = true; + } + layersStateMap.set(layer, { active: false, opacity: 1, @@ -24,8 +42,9 @@ export function initLayersStateAction (layersTree) { return layersStateMap; }, map); } + return reduceLayers(layersTree, layersTreeState); -} +}; export function setGroupLayerStateAction (layer, layerState, prevLayersTreeState) { const { layers } = layer;