diff --git a/docs/developer-guide/maps-configuration.md b/docs/developer-guide/maps-configuration.md
index 1c8653047c..ac1e9e8c84 100644
--- a/docs/developer-guide/maps-configuration.md
+++ b/docs/developer-guide/maps-configuration.md
@@ -1644,6 +1644,12 @@ Example:
"clippingPolygonFeatureId": "feature.id.01",
"clippingPolygonUnion": false
}
+ ],
+ "groups": [
+ {
+ "id": "group_01",
+ "visibility": true
+ }
]
}
],
@@ -1710,6 +1716,7 @@ View configuration object
| globeTranslucency.nearDistance | number | when `fadeByDistance` is true it indicates the minimum distance to apply translucency |
| globeTranslucency.farDistance | number | when `fadeByDistance` is true it indicates the maximum distance to apply translucency |
| layers | array | array of layer configuration overrides, default properties override `visibility` and `opacity` |
+| groups | array | array of group configuration overrides, default property overrides `visibility` |
Resource object configuration
diff --git a/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js b/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js
index 6e2e06c884..c50c89483c 100644
--- a/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js
+++ b/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js
@@ -95,12 +95,19 @@ function clip3DTiles(tileSet, options, map) {
});
}
-function ensureReady(tileSet, callback) {
+let pendingCallbacks = {};
+
+function ensureReady(layer, callback, eventKey) {
+ const tileSet = layer.getTileSet();
+ if (!tileSet && eventKey) {
+ pendingCallbacks[eventKey] = callback;
+ return;
+ }
if (tileSet.ready) {
- callback();
+ callback(tileSet);
} else {
tileSet.readyPromise.then(() => {
- callback();
+ callback(tileSet);
});
}
}
@@ -152,10 +159,13 @@ const createLayer = (options, map) => {
map.scene.primitives.remove(tileSet);
tileSet = undefined;
};
+ const layer = {
+ getTileSet: () => tileSet,
+ getResource: () => resource
+ };
return {
detached: true,
- getTileSet: () => tileSet,
- getResource: () => resource,
+ ...layer,
add: () => {
resource = new Cesium.Resource({
url: options.url,
@@ -175,7 +185,7 @@ const createLayer = (options, map) => {
map.scene.primitives.add(tileSet);
// assign the original mapstore id of the layer
tileSet.msId = options.id;
- ensureReady(tileSet, () => {
+ ensureReady(layer, () => {
updateModelMatrix(tileSet, options);
clip3DTiles(tileSet, options, map);
updateShading(tileSet, options, map);
@@ -184,6 +194,10 @@ const createLayer = (options, map) => {
if (style) {
tileSet.style = new Cesium.Cesium3DTileStyle(style);
}
+ Object.keys(pendingCallbacks).forEach((eventKey) => {
+ pendingCallbacks[eventKey](tileSet);
+ });
+ pendingCallbacks = {};
});
});
});
@@ -209,21 +223,20 @@ Layers.registerType('3dtiles', {
if (newOptions.forceProxy !== oldOptions.forceProxy) {
return createLayer(newOptions, map);
}
- const tileSet = layer?.getTileSet();
if (
(!isEqual(newOptions.clippingPolygon, oldOptions.clippingPolygon)
|| newOptions.clippingPolygonUnion !== oldOptions.clippingPolygonUnion
|| newOptions.clipOriginalGeometry !== oldOptions.clipOriginalGeometry)
- && tileSet) {
- ensureReady(tileSet, () => {
+ ) {
+ ensureReady(layer, (tileSet) => {
clip3DTiles(tileSet, newOptions, map);
- });
+ }, 'clip');
}
if ((
!isEqual(newOptions.style, oldOptions.style)
|| newOptions?.pointCloudShading?.attenuation !== oldOptions?.pointCloudShading?.attenuation
- ) && tileSet) {
- ensureReady(tileSet, () => {
+ )) {
+ ensureReady(layer, (tileSet) => {
getStyle(newOptions)
.then((style) => {
if (style && tileSet) {
@@ -231,17 +244,17 @@ Layers.registerType('3dtiles', {
tileSet.style = new Cesium.Cesium3DTileStyle(style);
}
});
- });
+ }, 'style');
}
- if (!isEqual(newOptions.pointCloudShading, oldOptions.pointCloudShading) && tileSet) {
- ensureReady(tileSet, () => {
+ if (!isEqual(newOptions.pointCloudShading, oldOptions.pointCloudShading)) {
+ ensureReady(layer, (tileSet) => {
updateShading(tileSet, newOptions, map);
- });
+ }, 'shading');
}
- if (tileSet && newOptions.heightOffset !== oldOptions.heightOffset) {
- ensureReady(tileSet, () => {
+ if (newOptions.heightOffset !== oldOptions.heightOffset) {
+ ensureReady(layer, (tileSet) => {
updateModelMatrix(tileSet, newOptions);
- });
+ }, 'matrix');
}
return null;
}
diff --git a/web/client/components/mapviews/MapViewSettings.jsx b/web/client/components/mapviews/MapViewSettings.jsx
index 2ada681d68..80a29eb697 100644
--- a/web/client/components/mapviews/MapViewSettings.jsx
+++ b/web/client/components/mapviews/MapViewSettings.jsx
@@ -31,6 +31,7 @@ function ViewSettings({
view,
api,
layers = [],
+ groups = [],
onChange,
onUpdateResource = () => { },
onCaptureView,
@@ -77,6 +78,26 @@ function ViewSettings({
});
}
+ function handleChangeGroup(groupId, options) {
+ const viewGroup = view?.groups?.find(vGroup => vGroup.id === groupId);
+ const viewGroups = viewGroup
+ ? (view?.groups || [])
+ .map((vGroup) => vGroup.id === groupId ? ({ ...viewGroup, ...options }) : vGroup)
+ : [...(view?.groups || []), { id: groupId, ...options }];
+ onChange({
+ ...view,
+ groups: viewGroups
+ });
+ }
+
+ function handleResetGroup(groupId) {
+ const viewGroups = view?.groups?.filter(vGroup => vGroup.id !== groupId);
+ onChange({
+ ...view,
+ groups: viewGroups
+ });
+ }
+
function updateLayerRequest({ layer, inverse = false, offset = 0 } = {}) {
return getResourceFromLayer({
layer,
@@ -108,11 +129,14 @@ function ViewSettings({
onChange={handleChange}
resources={resources}
layers={layers.filter(({ type }) => !(api?.options?.unsupportedLayers || []).includes(type))}
+ groups={groups}
vectorLayers={availableVectorLayers}
updateLayerRequest={updateLayerRequest}
locale={locale}
onChangeLayer={handleChangeLayer}
onResetLayer={handleResetLayer}
+ onChangeGroup={handleChangeGroup}
+ onResetGroup={handleResetGroup}
showClipGeometries={showClipGeometries}
onShowClipGeometries={onShowClipGeometries}
onCaptureView={onCaptureView}
diff --git a/web/client/components/mapviews/MapViewsSupport.jsx b/web/client/components/mapviews/MapViewsSupport.jsx
index f489588660..0ac177e87f 100644
--- a/web/client/components/mapviews/MapViewsSupport.jsx
+++ b/web/client/components/mapviews/MapViewsSupport.jsx
@@ -161,6 +161,7 @@ function MapViewsSupport({
selectedId,
defaultTitle = 'Map View',
layers,
+ groups,
locale,
resources: resourcesProp = [],
services,
@@ -597,6 +598,7 @@ function MapViewsSupport({
onChange={handleUpdateView}
onCaptureView={handleCaptureView}
layers={layers}
+ groups={groups}
locale={locale}
services={services}
selectedService={selectedService}
diff --git a/web/client/components/mapviews/settings/LayerOverridesNode.jsx b/web/client/components/mapviews/settings/LayerOverridesNode.jsx
deleted file mode 100644
index 3b48893efa..0000000000
--- a/web/client/components/mapviews/settings/LayerOverridesNode.jsx
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright 2022, GeoSolutions Sas.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-import React, { useState } from 'react';
-import {
- ButtonGroup,
- Glyphicon,
- Checkbox,
- FormGroup,
- ControlLabel,
- Alert
-} from 'react-bootstrap';
-import Select from 'react-select';
-import {clamp} from 'lodash';
-import FormControl from '../../misc/DebouncedFormControl';
-import { formatClippingFeatures } from '../../../utils/MapViewsUtils';
-import Message from '../../I18N/Message';
-import ButtonMS from '../../misc/Button';
-import tooltip from '../../misc/enhancers/tooltip';
-const Button = tooltip(ButtonMS);
-
-function LayerOverridesNode({
- layer = {},
- onChange,
- onReset,
- title,
- updateLayerRequest,
- vectorLayers,
- clippingFeatures,
- clippingLayerResource,
- initialExpanded
-}) {
-
- const [expanded, setExpanded] = useState(initialExpanded);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(false);
- const isVisibilitySupported = !['terrain'].includes(layer.type);
- const isOpacitySupported = !['3dtiles', 'terrain'].includes(layer.type);
- const isClippingSupported = !!['3dtiles', 'terrain'].includes(layer.type);
-
- function handleUpdateLayer({ layer: resourceLayer }) {
- setError(false);
- if (!resourceLayer) {
- return onChange({
- clippingLayerResourceId: undefined,
- clippingPolygonFeatureId: undefined,
- clippingPolygonUnion: undefined
- });
- }
- setLoading(true);
- return updateLayerRequest({ layer: resourceLayer })
- .then((clippingLayerResourceId) => {
- onChange({ clippingLayerResourceId });
- setLoading(false);
- })
- .catch(() => {
- setError(true);
- setLoading(false);
- });
- }
-
- const formattedClippingFeatures = formatClippingFeatures(clippingFeatures);
-
- return (
-
-
-
-
{title}
-
- {layer.changed && }
- {isVisibilitySupported && }
-
-
- {expanded &&
- {isOpacitySupported &&
-
- {
- const opacity = value && clamp(parseFloat(value), 0, 1);
- onChange({ opacity: opacity || 0 });
- }}
- />
- }
- {isClippingSupported &&
-
-
-
-
-
-
-
- onChange({ clippingPolygonUnion: !layer.clippingPolygonUnion })}>
-
-
-
- {loading &&
}
-
}
-
}
-
- );
-}
-
-export default LayerOverridesNode;
diff --git a/web/client/components/mapviews/settings/LayerOverridesNodeContent.jsx b/web/client/components/mapviews/settings/LayerOverridesNodeContent.jsx
new file mode 100644
index 0000000000..e71ca5e674
--- /dev/null
+++ b/web/client/components/mapviews/settings/LayerOverridesNodeContent.jsx
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2025, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React, { useState } from 'react';
+import {
+ Checkbox,
+ FormGroup,
+ ControlLabel,
+ Alert
+} from 'react-bootstrap';
+import Select from 'react-select';
+import { formatClippingFeatures } from '../../../utils/MapViewsUtils';
+import Message from '../../I18N/Message';
+import { getTitle } from '../../../utils/LayersUtils';
+import PropTypes from 'prop-types';
+
+/**
+ * LayerOverridesNodeContent render additional content inside the layer node in toc for map views
+ * @prop {object} node layer object
+ * @prop {object} config optional configuration available for the nodes
+ * @prop {function} onChange return the changes of this node
+ */
+function LayerOverridesNodeContent({
+ node = {},
+ config,
+ onChange
+}) {
+
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(false);
+
+ const isClippingSupported = !!['3dtiles', 'terrain'].includes(node.type);
+
+ if (!isClippingSupported) {
+ return null;
+ }
+
+ const {
+ updateLayerRequest,
+ vectorLayers,
+ resources,
+ locale
+ } = config?.mapViews || {};
+
+ const vectorLayersOptions = vectorLayers
+ ?.filter((layer) => {
+ if (layer.type === 'wfs') {
+ return true;
+ }
+ if (layer.type === 'vector') {
+ return !!layer?.features?.find(({ geometry }) => ['Polygon'].includes(geometry?.type));
+ }
+ return false;
+ })
+ .map((layer) => ({
+ label: getTitle(layer.title, locale) || layer.name || layer.id,
+ value: layer.id,
+ layer
+ }));
+
+ function handleUpdateLayer({ layer: resourceLayer }) {
+ setError(false);
+ if (!resourceLayer) {
+ return onChange({
+ clippingLayerResourceId: undefined,
+ clippingPolygonFeatureId: undefined,
+ clippingPolygonUnion: undefined
+ });
+ }
+ setLoading(true);
+ return updateLayerRequest({ layer: resourceLayer })
+ .then((clippingLayerResourceId) => {
+ onChange({ clippingLayerResourceId });
+ setLoading(false);
+ })
+ .catch(() => {
+ setError(true);
+ setLoading(false);
+ });
+ }
+
+ const _clippingLayerResource = resources?.find(({ id }) => id === node.clippingLayerResourceId)?.data;
+ const vectorLayer = vectorLayers?.find(({ id }) => id === _clippingLayerResource?.id);
+ const clippingFeatures = _clippingLayerResource?.collection?.features || vectorLayer?.features;
+ const clippingLayerResource = _clippingLayerResource
+ ? {
+ value: _clippingLayerResource?.id,
+ label: getTitle(vectorLayer?.title, locale) || vectorLayer?.name || vectorLayer?.id,
+ resource: _clippingLayerResource
+ } : undefined;
+
+ const formattedClippingFeatures = formatClippingFeatures(clippingFeatures);
+
+ return (
+
+
+
+
+
+
+
+
+ onChange({ clippingPolygonUnion: !node.clippingPolygonUnion })}>
+
+
+
+ {loading &&
}
+
+ );
+}
+
+LayerOverridesNodeContent.propTypes = {
+ node: PropTypes.object,
+ config: PropTypes.object,
+ onChange: PropTypes.func
+};
+
+export default LayerOverridesNodeContent;
diff --git a/web/client/components/mapviews/settings/LayersSection.jsx b/web/client/components/mapviews/settings/LayersSection.jsx
index 0f09c05d29..d944a203d7 100644
--- a/web/client/components/mapviews/settings/LayersSection.jsx
+++ b/web/client/components/mapviews/settings/LayersSection.jsx
@@ -6,52 +6,132 @@
* LICENSE file in the root directory of this source tree.
*/
-import React from 'react';
-import { FormGroup, Checkbox } from 'react-bootstrap';
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { FormControl as FormControlRB, FormGroup, InputGroup, Glyphicon, Checkbox, Button as ButtonRB, ButtonGroup } from 'react-bootstrap';
import Section from './Section';
-import { getTitle } from '../../../utils/LayersUtils';
-import { mergeViewLayers } from '../../../utils/MapViewsUtils';
-import LayerOverridesNode from './LayerOverridesNode';
+import { mergeViewLayers, mergeViewGroups, pickViewLayerProperties, pickViewGroupProperties } from '../../../utils/MapViewsUtils';
+import LayerOverridesNodeContent from './LayerOverridesNodeContent';
import Message from '../../I18N/Message';
+import TOC from '../../../plugins/TOC/components/TOC';
+import tooltip from '../../misc/enhancers/tooltip';
+import localizedProps from '../../misc/enhancers/localizedProps';
+import { NodeTypes } from '../../../utils/LayersUtils';
+import { getMessageById } from '../../../utils/LocaleUtils';
+const Button = tooltip(ButtonRB);
+const FormControl = localizedProps('placeholder')(FormControlRB);
+
+/**
+ * ResetLayerOverrides node tool to link and unlink groups and layers to TOC
+ * @prop {object} itemComponent default node tool component
+ * @prop {object} node layer object
+ * @prop {object} config optional configuration available for the nodes
+ * @prop {string} nodeType node type
+ * @prop {object} nodeTypes constant values for node types
+ * @prop {function} onChange return the changes of this node
+ */
+function ResetLayerOverrides({
+ itemComponent,
+ node,
+ config,
+ nodeType,
+ nodeTypes,
+ onChange
+}) {
+ const ItemComponent = itemComponent;
+ const { view } = config?.mapViews || {};
+ const changed = nodeType === nodeTypes.LAYER
+ ? !!view?.layers?.find(layer => layer.id === node.id)
+ : !!view?.groups?.find(group => group.id === node.id);
+ function handleClick() {
+ if (changed) {
+ onChange({ resetView: true });
+ } else {
+ onChange(nodeType === nodeTypes.LAYER
+ ? pickViewLayerProperties(node)
+ : pickViewGroupProperties(node));
+ }
+ }
+ return (
+
+ );
+}
+
+ResetLayerOverrides.propTypes = {
+ itemComponent: PropTypes.any,
+ node: PropTypes.object,
+ config: PropTypes.object,
+ nodeType: PropTypes.string,
+ nodeTypes: PropTypes.object,
+ onChange: PropTypes.func
+};
+/**
+ * LayersSection table of content for layers and groups inside a map view
+ * @prop {object} view view configuration
+ * @prop {object} expandedSections state of the expended section
+ * @prop {function} onExpandSection returns the new expanded state
+ * @prop {function} onChange returns changes on the view
+ * @prop {array} resources list of resources available for the views
+ * @prop {array} layers list of supported layers
+ * @prop {array} groups list of supported groups
+ * @prop {array} vectorLayers list of vector layers
+ * @prop {string} locale current locale
+ * @prop {function} onChangeLayer returns changes on a view layer
+ * @prop {function} onResetLayer requests a reset on the selected view layer
+ * @prop {function} onChangeGroup returns changes on a view group
+ * @prop {function} onResetGroup requests a reset on the selected view group
+ * @prop {boolean} showClipGeometries visibility state of clipping features
+ * @prop {function} onShowClipGeometries return the clipping checkbox state
+ * @prop {function} isTerrainAvailable if true shows the terrain options
+ * @prop {function} isClippingAvailable if true shows enable clipping options
+ */
function LayersSection({
view,
expandedSections = {},
onExpandSection,
onChange,
resources,
- layers,
+ layers = [],
+ groups = [],
vectorLayers,
updateLayerRequest,
locale,
onChangeLayer,
onResetLayer,
+ onChangeGroup,
+ onResetGroup,
showClipGeometries,
onShowClipGeometries,
isTerrainAvailable,
isClippingAvailable
-}) {
+}, { messages }) {
- const terrainClippingLayerResource = resources?.find(resource => resource.id === view?.terrain?.clippingLayerResourceId)?.data;
- const terrainVectorLayer = vectorLayers?.find(layer => layer.id === terrainClippingLayerResource?.id);
- const terrainClippingFeatures = terrainClippingLayerResource?.collection?.features || terrainVectorLayer?.features;
+ const [filterText, setFilterText] = useState('');
+ const [expandedNodes, setExpandedNodes] = useState([
+ ...groups.filter((group) => group.expanded).map(group => group.id),
+ ...layers.filter((layer) => layer.expanded).map(layer => layer.id)
+ ]);
const mergedLayers = mergeViewLayers(layers, view);
- const vectorLayersOptions = vectorLayers
- ?.filter((layer) => {
- if (layer.type === 'wfs') {
- return true;
- }
- if (layer.type === 'vector') {
- return !!layer?.features?.find(({ geometry }) => ['Polygon'].includes(geometry?.type));
- }
- return false;
- })
- .map((layer) => ({
- label: getTitle(layer.title, locale) || layer.name || layer.id,
- value: layer.id,
- layer
- }));
-
+ const mergedGroups = mergeViewGroups(groups, view);
+ const tocMapViewConfig = {
+ view,
+ updateLayerRequest,
+ vectorLayers,
+ resources,
+ locale
+ };
+ function applyExpandedProperty(nodes) {
+ return nodes.map(node => ({ ...node, expanded: expandedNodes.includes(node.id) }));
+ }
+ function areAllNodesUnlinked() {
+ return layers.every(layer => (view?.layers || []).some(vLayer => vLayer.id === layer.id))
+ && groups.every(group => (view?.groups || []).some(vGroup => vGroup.id === group.id));
+ }
return (
}
@@ -69,55 +149,150 @@ function LayersSection({
}
-
- {isTerrainAvailable && }
- layer={{
+
+
+ setFilterText(event?.target?.value)}
+ />
+ {filterText
+ ?
+
+
+ :
+
+ }
+
+
+
+
+
+
+ {isTerrainAvailable ? onChange({ terrain: { ...view?.terrain, ...newOptions }})}
- updateLayerRequest={updateLayerRequest}
- vectorLayers={vectorLayersOptions}
- clippingFeatures={terrainClippingFeatures}
- clippingLayerResource={terrainClippingLayerResource
- ? {
- value: terrainClippingLayerResource?.id,
- label: getTitle(terrainVectorLayer?.title, locale) || terrainVectorLayer?.name || terrainVectorLayer?.id,
- resource: terrainClippingLayerResource
- }
- : undefined}
- />}
- {mergedLayers?.length === 0
- ?
- : [ ...mergedLayers ].reverse().map((layer) => {
- const clippingLayerResource = resources?.find(({ id }) => id === layer.clippingLayerResourceId)?.data;
- const vectorLayer = vectorLayers?.find(({ id }) => id === clippingLayerResource?.id);
- const clippingFeatures = clippingLayerResource?.collection?.features || vectorLayer?.features;
- return (
- onChangeLayer(layer.id, newOptions)}
- onReset={() => onResetLayer(layer.id)}
- updateLayerRequest={updateLayerRequest}
- vectorLayers={vectorLayersOptions}
- clippingFeatures={clippingFeatures}
- clippingLayerResource={clippingLayerResource
- ? {
- value: clippingLayerResource?.id,
- label: getTitle(vectorLayer?.title, locale) || vectorLayer?.name || vectorLayer?.id,
- resource: clippingLayerResource
- } : undefined}
- />
- );
- })}
-
+ title: getMessageById(messages, 'mapViews.terrain')
+ }]
+ }}
+ nodeContentItems={[
+ { name: 'LayerOverridesNodeContent', Component: LayerOverridesNodeContent }
+ ]}
+ config={{
+ sortable: false,
+ hideOpacitySlider: true,
+ hideVisibilityButton: true,
+ layerOptions: {
+ hideFilter: true,
+ hideLegend: true
+ },
+ mapViews: tocMapViewConfig
+ }}
+ onChangeNode={(nodeId, nodeType, options) => {
+ if (nodeId === 'terrain' && nodeType === NodeTypes.LAYER) {
+ onChange({ terrain: { ...view?.terrain, ...options }});
+ }
+ }}
+ /> : null}
+ {
+ if (options.expanded !== undefined) {
+ return setExpandedNodes(
+ options.expanded
+ ? [...expandedNodes, nodeId]
+ : expandedNodes.filter(expandedNodeId => expandedNodeId !== nodeId));
+ }
+ if (options.resetView) {
+ return nodeType === NodeTypes.LAYER
+ ? onResetLayer(nodeId)
+ : onResetGroup(nodeId);
+ }
+ return nodeType === NodeTypes.LAYER
+ ? onChangeLayer(nodeId, options)
+ : onChangeGroup(nodeId, options);
+ }}
+ />
);
}
+LayersSection.propTypes = {
+ view: PropTypes.object,
+ expandedSections: PropTypes.object,
+ onExpandSection: PropTypes.func,
+ onChange: PropTypes.func,
+ resources: PropTypes.array,
+ layers: PropTypes.array,
+ groups: PropTypes.array,
+ vectorLayers: PropTypes.array,
+ updateLayerRequest: PropTypes.func,
+ locale: PropTypes.string,
+ onChangeLayer: PropTypes.func,
+ onResetLayer: PropTypes.func,
+ onChangeGroup: PropTypes.func,
+ onResetGroup: PropTypes.func,
+ showClipGeometries: PropTypes.bool,
+ onShowClipGeometries: PropTypes.func,
+ isTerrainAvailable: PropTypes.bool,
+ isClippingAvailable: PropTypes.bool
+};
+
+LayersSection.contextTypes = {
+ messages: PropTypes.object
+};
+
export default LayersSection;
diff --git a/web/client/components/mapviews/settings/Section.jsx b/web/client/components/mapviews/settings/Section.jsx
index f40718dec1..25df460d9b 100644
--- a/web/client/components/mapviews/settings/Section.jsx
+++ b/web/client/components/mapviews/settings/Section.jsx
@@ -29,7 +29,7 @@ function Section({
onClick={handleExpand}
style={{ borderRadius: '50%', marginRight: 4 }}
>
-
+
{title}
diff --git a/web/client/components/mapviews/settings/__tests__/LayerOverridesNode-test.jsx b/web/client/components/mapviews/settings/__tests__/LayerOverridesNode-test.jsx
deleted file mode 100644
index 9138afd124..0000000000
--- a/web/client/components/mapviews/settings/__tests__/LayerOverridesNode-test.jsx
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright 2022, GeoSolutions Sas.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-import React from 'react';
-import ReactDOM from 'react-dom';
-import LayerOverridesNode from '../LayerOverridesNode';
-import expect from 'expect';
-
-describe('LayerOverridesNode component', () => {
- beforeEach((done) => {
- document.body.innerHTML = '
';
- setTimeout(done);
- });
-
- afterEach((done) => {
- ReactDOM.unmountComponentAtNode(document.getElementById("container"));
- document.body.innerHTML = '';
- setTimeout(done);
- });
-
- it('should render with default', () => {
- ReactDOM.render(
, document.getElementById("container"));
- const layerNode = document.querySelector('.ms-map-views-layer-node');
- expect(layerNode).toBeTruthy();
- });
- it('should display three buttons for changed layers', () => {
- ReactDOM.render(
, document.getElementById("container"));
- const layerNode = document.querySelector('.ms-map-views-layer-node.changed');
- expect(layerNode).toBeTruthy();
- const buttonNodes = document.querySelectorAll('button');
- expect(buttonNodes.length).toBe(3);
- });
- it('should display clipping opacity option for layers', () => {
- ReactDOM.render(
, document.getElementById("container"));
- const layerNode = document.querySelector('.ms-map-views-layer-node');
- expect(layerNode).toBeTruthy();
- const buttonNodes = document.querySelectorAll('button');
- expect(buttonNodes.length).toBe(2);
- const inputNodes = document.querySelectorAll('input');
- expect(inputNodes.length).toBe(1);
- });
- it('should display clipping options for 3D tiles', () => {
- ReactDOM.render(
, document.getElementById("container"));
- const layerNode = document.querySelector('.ms-map-views-layer-node');
- expect(layerNode).toBeTruthy();
- const buttonNodes = document.querySelectorAll('button');
- expect(buttonNodes.length).toBe(2);
- const selectNodes = document.querySelectorAll('.Select');
- expect(selectNodes.length).toBe(2);
- const inputNodes = document.querySelectorAll('input');
- expect(inputNodes.length).toBe(2);
- expect([...inputNodes].map(node => node.getAttribute('type')))
- .toEqual([ null, 'checkbox' ]);
- });
-});
diff --git a/web/client/components/mapviews/settings/__tests__/LayerOverridesNodeContent-test.jsx b/web/client/components/mapviews/settings/__tests__/LayerOverridesNodeContent-test.jsx
new file mode 100644
index 0000000000..bd4fff441d
--- /dev/null
+++ b/web/client/components/mapviews/settings/__tests__/LayerOverridesNodeContent-test.jsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2025, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import LayerOverridesNodeContent from '../LayerOverridesNodeContent';
+import expect from 'expect';
+
+describe('LayerOverridesNodeContent component', () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '
';
+ setTimeout(done);
+ });
+
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ it('should not render with default', () => {
+ ReactDOM.render(
, document.getElementById("container"));
+ expect(document.querySelector('#container').children.length).toBe(0);
+ });
+ it('should display clipping options for 3D tiles', () => {
+ ReactDOM.render(
, document.getElementById("container"));
+ const layerNode = document.querySelector('.ms-map-views-layer-clipping');
+ expect(layerNode).toBeTruthy();
+ const selectNodes = document.querySelectorAll('.Select');
+ expect(selectNodes.length).toBe(2);
+ const inputNodes = document.querySelectorAll('input');
+ expect(inputNodes.length).toBe(2);
+ expect([...inputNodes].map(node => node.getAttribute('type')))
+ .toEqual([ null, 'checkbox' ]);
+ });
+});
diff --git a/web/client/components/mapviews/settings/__tests__/LayersSection-test.jsx b/web/client/components/mapviews/settings/__tests__/LayersSection-test.jsx
index 2262668745..eda86ea8a8 100644
--- a/web/client/components/mapviews/settings/__tests__/LayersSection-test.jsx
+++ b/web/client/components/mapviews/settings/__tests__/LayersSection-test.jsx
@@ -10,6 +10,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import LayersSection from '../LayersSection';
import expect from 'expect';
+import { Simulate } from 'react-dom/test-utils';
describe('LayersSection component', () => {
beforeEach((done) => {
@@ -44,8 +45,8 @@ describe('LayersSection component', () => {
const sectionNode = document.querySelector('.ms-map-views-section');
expect(sectionNode).toBeTruthy();
const inputNodes = document.querySelectorAll('input');
- expect(inputNodes.length).toBe(1);
- expect([...inputNodes].map(node => node.getAttribute('type'))).toEqual(['checkbox']);
+ expect(inputNodes.length).toBe(2);
+ expect(inputNodes[0].getAttribute('type')).toBe('checkbox');
});
it('should display terrain node', () => {
ReactDOM.render(
{
/>, document.getElementById("container"));
const sectionNode = document.querySelector('.ms-map-views-section');
expect(sectionNode).toBeTruthy();
- const layerNodes = [...document.querySelectorAll('.ms-map-views-layer-node')];
+ const layerNodes = [...document.querySelectorAll('.ms-node-layer')];
expect(layerNodes.length).toBe(1);
});
it('should display list of layers', () => {
@@ -96,13 +97,474 @@ describe('LayersSection component', () => {
/>, document.getElementById("container"));
const sectionNode = document.querySelector('.ms-map-views-section');
expect(sectionNode).toBeTruthy();
- const layerNodes = [...document.querySelectorAll('.ms-map-views-layer-node')];
+ const layerNodes = [...document.querySelectorAll('.ms-node-layer')];
expect(layerNodes.length).toBe(2);
- const layerButtonNodes = [...layerNodes[0].querySelectorAll('button')];
- expect(layerButtonNodes.length).toBe(2);
- const changedLayerNodes = [...document.querySelectorAll('.ms-map-views-layer-node.changed')];
- expect(changedLayerNodes.length).toBe(1);
- const changedLayerButtonNodes = [...changedLayerNodes[0].querySelectorAll('button')];
- expect(changedLayerButtonNodes.length).toBe(3);
+ expect([...layerNodes[0].querySelectorAll('button')]
+ .map(layerButtonNode => layerButtonNode.querySelector('.glyphicon').getAttribute('class')))
+ .toEqual([ 'glyphicon glyphicon-checkbox-off', 'glyphicon glyphicon-plug' ]);
+ expect([...layerNodes[1].querySelectorAll('button')]
+ .map(layerButtonNode => layerButtonNode.querySelector('.glyphicon').getAttribute('class')))
+ .toEqual([ 'glyphicon glyphicon-checkbox-on', 'glyphicon glyphicon-unplug' ]);
+ });
+ it('should display list of groups', () => {
+ ReactDOM.render(, document.getElementById("container"));
+ const groupsHeaders = document.querySelectorAll('.ms-node-group > .ms-node-header');
+ expect(groupsHeaders.length).toBe(2);
+ expect([...groupsHeaders].map(group =>
+ group.querySelector('.ms-node-title').innerText
+ )).toEqual(['Group 01', 'Group 02']);
+ expect(
+ [...groupsHeaders].map(group =>
+ [...group.querySelectorAll('button > .glyphicon')]
+ .map((glyph) => glyph.getAttribute('class'))
+ ))
+ .toEqual([
+ [ 'glyphicon glyphicon-bottom', 'glyphicon glyphicon-checkbox-on', 'glyphicon glyphicon-plug' ],
+ [ 'glyphicon glyphicon-bottom', 'glyphicon glyphicon-checkbox-off', 'glyphicon glyphicon-unplug' ]
+ ]);
+ });
+ it('should trigger onChangeLayer by clicking on the unlink button of layers', (done) => {
+ ReactDOM.render( {
+ try {
+ expect(id).toBe('layer.01');
+ expect(value).toEqual({ id: 'layer.01', visibility: false });
+ } catch (e) {
+ done(e);
+ }
+ done();
+ }}
+ expandedSections={{ layers: true }}
+ isClippingAvailable
+ />, document.getElementById("container"));
+ const layerHeaders = document.querySelectorAll('.ms-node-layer > .ms-node-header');
+ expect(layerHeaders.length).toBe(1);
+ const buttons = layerHeaders[0].querySelectorAll('button');
+ expect(buttons.length).toBe(2);
+ Simulate.click(buttons[1]);
+ });
+ it('should trigger onResetLayer by clicking on the link button of layers', (done) => {
+ ReactDOM.render( {
+ try {
+ expect(id).toBe('layer.01');
+ } catch (e) {
+ done(e);
+ }
+ done();
+ }}
+ expandedSections={{ layers: true }}
+ isClippingAvailable
+ />, document.getElementById("container"));
+ const layerHeaders = document.querySelectorAll('.ms-node-layer > .ms-node-header');
+ expect(layerHeaders.length).toBe(1);
+ const buttons = layerHeaders[0].querySelectorAll('button');
+ expect(buttons.length).toBe(2);
+ Simulate.click(buttons[1]);
+ });
+ it('should trigger onChangeGroup by clicking on the unlink button of groups', (done) => {
+ ReactDOM.render( {
+ try {
+ expect(id).toBe('group_01');
+ expect(value).toEqual({ id: 'group_01', visibility: true });
+ } catch (e) {
+ done(e);
+ }
+ done();
+ }}
+ expandedSections={{ layers: true }}
+ isClippingAvailable
+ />, document.getElementById("container"));
+ const groupHeaders = document.querySelectorAll('.ms-node-group > .ms-node-header');
+ expect(groupHeaders.length).toBe(1);
+ const buttons = groupHeaders[0].querySelectorAll('button');
+ expect(buttons.length).toBe(3);
+ Simulate.click(buttons[2]);
+ });
+ it('should trigger onResetGroup by clicking on the link button of groups', (done) => {
+ ReactDOM.render( {
+ try {
+ expect(id).toBe('group_01');
+ } catch (e) {
+ done(e);
+ }
+ done();
+ }}
+ expandedSections={{ layers: true }}
+ isClippingAvailable
+ />, document.getElementById("container"));
+ const groupHeaders = document.querySelectorAll('.ms-node-group > .ms-node-header');
+ expect(groupHeaders.length).toBe(1);
+ const buttons = groupHeaders[0].querySelectorAll('button');
+ expect(buttons.length).toBe(3);
+ Simulate.click(buttons[2]);
+ });
+
+ it('should show toolbar with search input and global link and unlink buttons', () => {
+ ReactDOM.render(, document.getElementById("container"));
+ const formGroup = document.querySelectorAll('.form-group');
+ expect(formGroup.length).toBe(2);
+ const input = formGroup[1].querySelector('input');
+ expect(input).toBeTruthy();
+ const buttons = formGroup[1].querySelectorAll('button');
+ expect(buttons.length).toBe(2);
+ });
+
+ it('should trigger onChange after clicking to link button', (done) => {
+ ReactDOM.render( {
+ try {
+ expect(value).toEqual({ groups: undefined, layers: undefined });
+ } catch (e) {
+ done(e);
+ }
+ done();
+ }}
+ expandedSections={{ layers: true }}
+ isClippingAvailable
+ />, document.getElementById("container"));
+ const formGroup = document.querySelectorAll('.form-group');
+ expect(formGroup.length).toBe(2);
+ const buttons = formGroup[1].querySelectorAll('button');
+ expect(buttons.length).toBe(2);
+ Simulate.click(buttons[0]);
+ });
+
+ it('should trigger onChange after clicking to unlink button', (done) => {
+ ReactDOM.render( {
+ try {
+ expect(value).toEqual({
+ groups: [ { id: 'group_01', visibility: false } ],
+ layers: [ { id: 'layer.01', visibility: false } ]
+ });
+ } catch (e) {
+ done(e);
+ }
+ done();
+ }}
+ expandedSections={{ layers: true }}
+ isClippingAvailable
+ />, document.getElementById("container"));
+ const formGroup = document.querySelectorAll('.form-group');
+ expect(formGroup.length).toBe(2);
+ const buttons = formGroup[1].querySelectorAll('button');
+ expect(buttons.length).toBe(2);
+ Simulate.click(buttons[1]);
});
});
diff --git a/web/client/epics/__tests__/mapviews-test.js b/web/client/epics/__tests__/mapviews-test.js
index 94e910ef0a..4717bf4bcf 100644
--- a/web/client/epics/__tests__/mapviews-test.js
+++ b/web/client/epics/__tests__/mapviews-test.js
@@ -90,6 +90,8 @@ describe('mapviews epics', () => {
UPDATE_ADDITIONAL_LAYER
]);
+ expect(actions[3].options.visibility).toBe(false);
+ expect(actions[4].options.visibility).toBe(true);
expect(actions[4].options.style).toBeTruthy();
expect(actions[4].options.style).toEqual({
format: 'geostyler',
@@ -114,9 +116,13 @@ describe('mapviews epics', () => {
}
}, {
layers: {
+ groups: [
+ { id: 'group_01', visibility: false },
+ { id: 'group_02', visibility: false }
+ ],
flat: [
- { id: 'layer.01', type: '3dtiles', visibility: true },
- { id: 'layer.02', type: 'vector', visibility: true, features: [feature] }
+ { id: 'layer.01', group: 'group_01', type: '3dtiles', visibility: true },
+ { id: 'layer.02', group: 'group_02', type: 'vector', visibility: true, features: [feature] }
]
},
maptype: {
@@ -160,6 +166,12 @@ describe('mapviews epics', () => {
clippingLayerResourceId: 'resource.01',
clippingPolygonFeatureId: 'feature.01'
}
+ ],
+ groups: [
+ {
+ id: 'group_02',
+ visibility: true
+ }
]
}
],
diff --git a/web/client/epics/mapviews.js b/web/client/epics/mapviews.js
index accf8bbe6f..e53a40f903 100644
--- a/web/client/epics/mapviews.js
+++ b/web/client/epics/mapviews.js
@@ -40,14 +40,15 @@ import {
} from '../selectors/mapviews';
import { BROWSE_DATA } from '../actions/layers';
import { CLOSE_FEATURE_GRID } from '../actions/featuregrid';
-import { layersSelector } from '../selectors/layers';
+import { layersSelector, rawGroupsSelector } from '../selectors/layers';
import {
isShallowEqualBy
} from '../utils/ReselectUtils';
import { getResourceFromLayer } from '../api/MapViews';
-import { MAP_VIEWS_LAYERS_OWNER, formatClippingFeatures } from '../utils/MapViewsUtils';
+import { MAP_VIEWS_LAYERS_OWNER, formatClippingFeatures, isViewLayerChanged, mergeViewGroups, mergeViewLayers } from '../utils/MapViewsUtils';
import { isCesium } from '../selectors/maptype';
+import { getDerivedLayersVisibility } from '../utils/LayersUtils';
const deepCompare = isShallowEqualBy();
@@ -117,22 +118,32 @@ export const updateMapViewsLayers = (action$, store) =>
const state = store.getState();
const previousView = getPreviousView(state);
const currentView = getSelectedMapView(state);
- const { layers = [], mask = {}, id: viewId } = currentView || {};
+ const { layers = [], groups = [], mask = {}, id: viewId } = currentView || {};
const shouldUpdate = !!(
action.type === VISUALIZATION_MODE_CHANGED
|| !deepCompare(previousView?.layers || [], layers)
|| !deepCompare(previousView?.mask || {}, mask)
+ || !deepCompare(previousView?.groups || [], groups)
);
if (!shouldUpdate) {
return Observable.of(
setPreviousView(currentView)
);
}
+ const mapLayers = layersSelector(state);
+ const mergedGroups = mergeViewGroups(
+ rawGroupsSelector(state),
+ currentView, true);
+ const mergedLayers = mergeViewLayers(mapLayers, currentView);
+ const updatedLayers = getDerivedLayersVisibility(mergedLayers, mergedGroups);
+ const changedLayers = updatedLayers.filter((uLayer) => {
+ const currentLayer = (mapLayers || []).find(layer => layer.id === uLayer.id);
+ return isViewLayerChanged(uLayer, currentLayer);
+ });
const resources = getMapViewsResources(state);
return updateResourcesObservable(currentView, store)
.switchMap((allResources) => {
const checkedResources = allResources.filter(({ error }) => !error);
- const mapLayers = layersSelector(state);
const updatedResources = checkedResources.filter(resource => resource.updated);
const maskLayerResource = isString(mask.resourceId) && checkedResources.find((resource) => resource.id === mask.resourceId);
return Observable.of(
@@ -144,8 +155,7 @@ export const updateMapViewsLayers = (action$, store) =>
] : []),
setPreviousView(currentView),
removeAdditionalLayer({ owner: MAP_VIEWS_LAYERS_OWNER }),
- ...layers
- .filter((layer) => !!mapLayers.find(mapLayer => mapLayer.id === layer.id))
+ ...changedLayers
.map((layer) => {
const clipPolygonLayerResource = isString(layer.clippingLayerResourceId) && checkedResources.find((resource) => resource.id === layer.clippingLayerResourceId);
const clippingPolygon = isString(layer.clippingPolygonFeatureId)
@@ -156,7 +166,7 @@ export const updateMapViewsLayers = (action$, store) =>
'override',
{
...layer,
- clippingPolygon
+ ...(clippingPolygon && { clippingPolygon })
}
);
}),
diff --git a/web/client/plugins/TOC/components/DefaultGroup.jsx b/web/client/plugins/TOC/components/DefaultGroup.jsx
index 5b4ba4a559..1b27b1a071 100644
--- a/web/client/plugins/TOC/components/DefaultGroup.jsx
+++ b/web/client/plugins/TOC/components/DefaultGroup.jsx
@@ -49,6 +49,7 @@ const DefaultGroupNode = ({
onChange,
nodeType,
nodeTypes,
+ config,
itemComponent: NodeTool
};
return (
diff --git a/web/client/plugins/TOC/components/DefaultLayer.jsx b/web/client/plugins/TOC/components/DefaultLayer.jsx
index aa04502634..2a6ddcddde 100644
--- a/web/client/plugins/TOC/components/DefaultLayer.jsx
+++ b/web/client/plugins/TOC/components/DefaultLayer.jsx
@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
-import React from 'react';
+import React, { useRef, useLayoutEffect, useState } from 'react';
import { castArray, find } from 'lodash';
import { Glyphicon } from 'react-bootstrap';
import { isInsideResolutionsLimits, getLayerTypeGlyph } from '../../../utils/LayersUtils';
@@ -44,6 +44,86 @@ const getLayerVisibilityWarningMessageId = (node, config = {}) => {
}
return '';
};
+
+const NodeLegend = ({
+ config,
+ node,
+ visible,
+ onChange
+}) => {
+
+ if (config?.layerOptions?.hideLegend) {
+ return null;
+ }
+ const layerType = node?.type;
+ if (['wfs', 'vector'].includes(layerType)) {
+ const hasStyle = node?.style?.format === 'geostyler' && node?.style?.body?.rules?.length > 0;
+ return hasStyle
+ ? (
+ <>
+
+ {visible ? : null}
+
+ >
+ )
+ : null;
+ }
+ if (layerType === 'wms') {
+ return (
+ <>
+
+ {visible ? : null}
+
+ >
+ );
+ }
+ if (layerType === 'arcgis') {
+ return (
+ <>
+
+ {visible ? : null}
+
+ >
+ );
+ }
+ return null;
+};
+
+const NodeContent = ({
+ error,
+ config,
+ node,
+ visible,
+ onChange,
+ items
+}) => {
+ if (error) {
+ return null;
+ }
+ const contentProps = {
+ config,
+ node,
+ onChange,
+ visible
+ };
+ return <>
+ {items.map(({ Component, name }) => {
+ return ();
+ })}
+
+ >;
+};
/**
* DefaultLayerNode renders internal part of the layer node
* @prop {string} node layer node properties
@@ -51,6 +131,7 @@ const getLayerVisibilityWarningMessageId = (node, config = {}) => {
* @prop {function} onChange return the changes of a specific node
* @prop {object} config optional configuration available for the nodes
* @prop {array} nodeToolItems list of node tool component to customize specific tool available on a node, expected structure [ { name, Component } ]
+ * @prop {array} nodeContentItems list of node content component to customize specific content available after expanding the node, expected structure [ { name, Component } ]
* @prop {function} onSelect return the current selected node on click event
* @prop {string} nodeType node type
* @prop {object} nodeTypes constant values for node types
@@ -67,6 +148,7 @@ const DefaultLayerNode = ({
sortHandler,
config = {},
nodeToolItems = [],
+ nodeContentItems = [],
onSelect,
nodeType,
nodeTypes,
@@ -76,68 +158,21 @@ const DefaultLayerNode = ({
nodeIcon
}) => {
- const getContent = () => {
-
- // currently the only content of the layer is the legend
- // so we hide it if not visible
- if (error || config?.layerOptions?.hideLegend) {
- return null;
- }
-
- const layerType = node?.type;
- if (['wfs', 'vector'].includes(layerType)) {
- const hasStyle = node?.style?.format === 'geostyler' && node?.style?.body?.rules?.length > 0;
- return hasStyle
- ? (
- <>
-
-
-
- >
- )
- : null;
- }
- if (layerType === 'wms') {
- return (
- <>
-
-
-
- >
- );
- }
- if (layerType === 'arcgis') {
- return (
- <>
-
-
-
- >
- );
- }
- return null;
- };
+ const contentNode = useRef();
+ const [hasContent, setHasContent] = useState(false);
+ useLayoutEffect(() => {
+ setHasContent(!!contentNode?.current?.children?.length);
+ }, [error, node, config]);
const forceExpanded = config?.expanded !== undefined;
const expanded = forceExpanded ? config?.expanded : node?.expanded;
- const content = getContent(error);
const componentProps = {
node,
onChange,
nodeType,
nodeTypes,
+ config,
itemComponent: NodeTool
};
@@ -160,7 +195,7 @@ const DefaultLayerNode = ({
<>
{sortHandler}
@@ -184,9 +219,16 @@ const DefaultLayerNode = ({
>
}
/>
- {expanded && content ? : null}
+
{
event.stopPropagation();
+ event.preventDefault();
if (!disabled) {
onChange({ expanded: !expanded });
}
diff --git a/web/client/plugins/TOC/components/LayersTree.jsx b/web/client/plugins/TOC/components/LayersTree.jsx
index d3597b722a..94dd104080 100644
--- a/web/client/plugins/TOC/components/LayersTree.jsx
+++ b/web/client/plugins/TOC/components/LayersTree.jsx
@@ -62,6 +62,7 @@ const loopGroupCondition = (groupNode, condition) => {
* @prop {string} theme layers tree theme, one of undefined or `legend`
* @prop {string} className additional class name for the layer tree
* @prop {array} nodeItems list of node component to customize specific nodes, expected structure [ { name, Component, selector } ]
+ * @prop {array} nodeContentItems list of node content component to customize specific content available after expanding the node, expected structure [ { name, Component } ]
* @prop {array} nodeToolItems list of node tool component to customize specific tool available on a node, expected structure [ { name, Component } ]
* @prop {object} singleDefaultGroup if true it hides the default group nodes
* @prop {string} noFilteredResultsMsgId message id for no result on filter
@@ -87,6 +88,7 @@ const LayersTree = ({
className,
nodeItems,
nodeToolItems,
+ nodeContentItems,
singleDefaultGroup = isSingleDefaultGroup(tree),
theme
}) => {
@@ -204,6 +206,7 @@ const LayersTree = ({
onSelect={onSelect}
nodeItems={nodeItems}
nodeToolItems={nodeToolItems}
+ nodeContentItems={nodeContentItems}
/>
);
})}
diff --git a/web/client/plugins/TOC/components/NodeTool.jsx b/web/client/plugins/TOC/components/NodeTool.jsx
index 9ba1c95b00..e307373d08 100644
--- a/web/client/plugins/TOC/components/NodeTool.jsx
+++ b/web/client/plugins/TOC/components/NodeTool.jsx
@@ -49,6 +49,7 @@ function NodeTool({
tooltip={tooltipProp}
onClick={(event) => {
event.stopPropagation();
+ event.preventDefault();
if (!disabled) {
onClick(event);
}
diff --git a/web/client/plugins/TOC/components/TOC.jsx b/web/client/plugins/TOC/components/TOC.jsx
index 6801d3f446..c53bc8d795 100644
--- a/web/client/plugins/TOC/components/TOC.jsx
+++ b/web/client/plugins/TOC/components/TOC.jsx
@@ -34,6 +34,7 @@ import {
* @prop {string} filterText filter to apply to layers title
* @prop {string} theme layers tree theme, one of undefined or `legend`
* @prop {string} className additional class name for the layer tree
+ * @prop {array} nodeContentItems list of node content component to customize specific content available after expanding the node, expected structure [ { name, Component } ]
* @prop {array} nodeItems list of node component to customize specific nodes, expected structure [ { name, Component, selector } ]
* @prop {array} nodeToolItems list of node tool component to customize specific tool available on a node, expected structure [ { name, Component } ]
* @prop {object} singleDefaultGroup if true it hides the default group nodes
@@ -74,6 +75,7 @@ export function ControlledTOC({
className,
nodeItems,
nodeToolItems,
+ nodeContentItems,
singleDefaultGroup,
theme
}) {
@@ -100,6 +102,7 @@ export function ControlledTOC({
config={config}
nodeItems={nodeItems}
nodeToolItems={nodeToolItems}
+ nodeContentItems={nodeContentItems}
singleDefaultGroup={singleDefaultGroup}
/>
);
@@ -108,6 +111,7 @@ export function ControlledTOC({
/**
* TOC component that supports map configuration
* @prop {object} map map configuration
+ * @prop {function} onChangeNode return the changed node configuration
* @prop {function} onChangeMap return the changed map configuration
* @prop {array} selectedNodes list of selected node identifiers
* @prop {function} onSelectNode return the current selected node on click event
@@ -115,6 +119,7 @@ export function ControlledTOC({
* @prop {string} className additional class name for the layer tree
* @prop {array} nodeItems list of node component to customize specific nodes, expected structure [ { name, Component, selector } ]
* @prop {array} nodeToolItems list of node tool component to customize specific tool available on a node, expected structure [ { name, Component } ]
+ * @prop {array} nodeContentItems list of node content component to customize specific content available after expanding the node, expected structure [ { name, Component } ]
* @prop {object} singleDefaultGroup if true it hides the default group nodes
* @prop {object} config optional configuration available for the nodes
* @prop {number} config.resolution map resolution
@@ -138,15 +143,18 @@ export function ControlledTOC({
*/
function TOC({
map = { layers: [], groups: [] },
+ onChangeNode = () => {},
onChangeMap = () => {},
selectedNodes = [],
onSelectNode = () => {},
config,
className,
nodeToolItems,
+ nodeContentItems,
singleDefaultGroup,
nodeItems,
- theme
+ theme,
+ filterText
}) {
const { layers } = splitMapAndLayers(map) || {};
const tree = denormalizeGroups(layers.flat || [], layers.groups || []).groups;
@@ -173,6 +181,7 @@ function TOC({
}
}
function handleUpdateNode(nodeId, nodeType, options) {
+ onChangeNode(nodeId, nodeType, options);
const updatedNode = changeNodeConfiguration({
groups: layers.groups,
layers: layers.flat
@@ -188,6 +197,7 @@ function TOC({
className={className}
theme={theme}
tree={tree}
+ filterText={filterText}
selectedNodes={selectedNodesIdsToObject(selectedNodes, layers.flat, tree)}
onSelectNode={onSelectNode}
onSort={handleOnSort}
@@ -206,6 +216,7 @@ function TOC({
}}
nodeItems={nodeItems}
nodeToolItems={nodeToolItems}
+ nodeContentItems={nodeContentItems}
singleDefaultGroup={singleDefaultGroup}
/>
);
diff --git a/web/client/plugins/TOC/components/VisibilityCheck.jsx b/web/client/plugins/TOC/components/VisibilityCheck.jsx
index 1281afef79..8005c2dafe 100644
--- a/web/client/plugins/TOC/components/VisibilityCheck.jsx
+++ b/web/client/plugins/TOC/components/VisibilityCheck.jsx
@@ -40,6 +40,7 @@ const VisibilityCheck = ({
className={`ms-visibility-check${value ? ' active' : ''}`}
onClick={(event) => {
event.stopPropagation();
+ event.preventDefault();
onChange(!value);
}}
onContextMenu={(event) => {
diff --git a/web/client/plugins/TOC/components/__tests__/DefaultLayer-test.jsx b/web/client/plugins/TOC/components/__tests__/DefaultLayer-test.jsx
index b5c9647e32..7aab1562cc 100644
--- a/web/client/plugins/TOC/components/__tests__/DefaultLayer-test.jsx
+++ b/web/client/plugins/TOC/components/__tests__/DefaultLayer-test.jsx
@@ -100,9 +100,27 @@ describe('test DefaultLayer module component', () => {
}}/>, document.getElementById("container"));
const expand = document.querySelector('.ms-node-expand');
expect(expand).toBeTruthy();
- expect(document.querySelector('.ms-node-layer ul')).toBeFalsy();
+ expect(document.querySelector('.ms-node-layer ul').style.display).toBe('none');
TestUtils.Simulate.click(expand);
- expect(document.querySelector('.ms-node-layer ul')).toBeTruthy();
+ });
+
+ it('should include custom content with nodeContentItems prop when expanding layer node', () => {
+ const l = {
+ id: 'layer00',
+ name: 'layer00',
+ title: 'Layer',
+ visibility: true,
+ type: 'wms',
+ expanded: true
+ };
+ function CustomContent() {
+ return ;
+ }
+ ReactDOM.render(, document.getElementById("container"));
+ const expand = document.querySelector('.ms-node-expand');
+ expect(expand).toBeTruthy();
+ expect(document.querySelector('.ms-node-layer ul').style.display).toBe('');
+ expect(document.querySelector('.ms-node-layer #custom-content')).toBeTruthy();
});
it('tests opacity tool', () => {
diff --git a/web/client/plugins/mapviews/MapViews.jsx b/web/client/plugins/mapviews/MapViews.jsx
index 624a7799f9..d7710054fe 100644
--- a/web/client/plugins/mapviews/MapViews.jsx
+++ b/web/client/plugins/mapviews/MapViews.jsx
@@ -19,7 +19,7 @@ import {
hideViews
} from '../../actions/mapviews';
import mapviews from '../../reducers/mapviews';
-import { layersSelector } from '../../selectors/layers';
+import { layersSelector, rawGroupsSelector } from '../../selectors/layers';
import { currentLocaleSelector } from '../../selectors/locale';
import epics from '../../epics/mapviews';
import {
@@ -33,6 +33,8 @@ import {
} from '../../selectors/mapviews';
import Loader from '../../components/misc/Loader';
import { MAP_VIEWS_CONFIG_KEY } from '../../utils/MapViewsUtils';
+import { flattenArrayOfObjects } from '../../utils/LayersUtils';
+import { isObject } from 'lodash';
const MapViewsSupport = lazy(() => import('../../components/mapviews/MapViewsSupport'));
@@ -92,8 +94,9 @@ const ConnectedMapViews = connect(
isMapViewsActive,
isMapViewsHidden,
isMapViewsInitialized,
- getMapViewsUpdateUUID
- ], (selectedId, views, layers, locale, mapConfig, resources, active, hide, initialized, updateUUID) => ({
+ getMapViewsUpdateUUID,
+ rawGroupsSelector
+ ], (selectedId, views, layers, locale, mapConfig, resources, active, hide, initialized, updateUUID, groups) => ({
selectedId,
views,
layers: layers.filter(({ group }) => group !== 'background'),
@@ -103,7 +106,8 @@ const ConnectedMapViews = connect(
active,
hide,
initialized,
- updateUUID
+ updateUUID,
+ groups: flattenArrayOfObjects(groups).filter(isObject)
})),
{
onSelectView: selectView,
diff --git a/web/client/selectors/__tests__/mapviews-test.js b/web/client/selectors/__tests__/mapviews-test.js
index 95f82bcd7d..a4f104bb8b 100644
--- a/web/client/selectors/__tests__/mapviews-test.js
+++ b/web/client/selectors/__tests__/mapviews-test.js
@@ -99,7 +99,48 @@ describe('mapviews selectors', () => {
]
}
};
- expect(getMapViews(state)).toBe(state.mapviews.views);
+ expect(getMapViews(state)).toEqual(state.mapviews.views);
+ });
+ it('getMapViews remove missing layers and groups', () => {
+ const state = {
+ layers: {
+ flat: [{ id: 'layer.01', group: 'group_01' }],
+ groups: [{ id: 'group_01' }]
+ },
+ mapviews: {
+ views: [
+ {
+ center: {
+ longitude: 8.936900,
+ latitude: 44.395224,
+ height: 0
+ },
+ cameraPosition: {
+ longitude: 8.939256,
+ latitude: 44.386982,
+ height: 655
+ },
+ id: 'view.1',
+ title: 'Map view',
+ description: '',
+ duration: 10,
+ flyTo: true,
+ zoom: 16,
+ bbox: [
+ 8.920925,
+ 44.390840,
+ 8.948118,
+ 44.405544
+ ],
+ layers: [{ id: 'layer.01' }, { id: 'layer.02' }],
+ groups: [{ id: 'group_01' }, { id: 'group_02' }]
+ }
+ ]
+ }
+ };
+ const newMapViews = getMapViews(state);
+ expect(newMapViews[0].layers).toEqual([{ id: 'layer.01' }]);
+ expect(newMapViews[0].groups).toEqual([{ id: 'group_01' }]);
});
it('getMapViewsResources', () => {
const state = {
@@ -205,7 +246,7 @@ describe('mapviews selectors', () => {
active: true
}
};
- expect(getSelectedMapView(state)).toBe(state.mapviews.views[0]);
+ expect(getSelectedMapView(state)).toEqual(state.mapviews.views[0]);
state = {
mapviews: {
...state.mapviews,
diff --git a/web/client/selectors/mapviews.js b/web/client/selectors/mapviews.js
index 133efd7e25..fd2c5b68e7 100644
--- a/web/client/selectors/mapviews.js
+++ b/web/client/selectors/mapviews.js
@@ -6,10 +6,30 @@
* LICENSE file in the root directory of this source tree.
*/
+import { DEFAULT_GROUP_ID } from "../utils/LayersUtils";
+import { layersSelector } from "./layers";
+
export const isMapViewsActive = state => !!state?.mapviews?.active;
export const isMapViewsHidden = state => !!state?.mapviews?.hide;
export const getSelectedMapViewId = state => !isMapViewsHidden(state) && isMapViewsActive(state) && state?.mapviews?.selectedId;
-export const getMapViews = state => state?.mapviews?.views;
+export const getMapViews = state => {
+ const layers = layersSelector(state);
+ const layersIds = layers.map(layer => layer.id);
+ const groupsIds = layers.map(layer => layer.group || DEFAULT_GROUP_ID);
+ return state?.mapviews?.views ? state.mapviews.views.map(view => {
+ const viewLayers = view?.layers;
+ const viewGroups = view?.groups;
+ return {
+ ...view,
+ ...(viewLayers && {
+ layers: viewLayers.filter(viewLayer => layersIds.includes(viewLayer.id))
+ }),
+ ...(viewGroups && {
+ groups: viewGroups.filter(viewGroup => groupsIds.includes(viewGroup.id))
+ })
+ };
+ }) : undefined;
+};
export const getMapViewsResources = state => state?.mapviews?.resources;
export const getResourceById = (state, id) => getMapViewsResources(state)?.find(resource => resource.id === id);
export const getPreviousView = state => state?.mapviews?.previousView;
diff --git a/web/client/themes/default/less/map-views.less b/web/client/themes/default/less/map-views.less
index 364858160f..24b9ecd8af 100644
--- a/web/client/themes/default/less/map-views.less
+++ b/web/client/themes/default/less/map-views.less
@@ -34,12 +34,6 @@
}
.border-color-var(@theme-vars[main-border-color]);
}
- .ms-map-views-layer-node {
- &.changed {
- .border-left-color-var(@theme-vars[success]);
- }
- .border-color-var(@theme-vars[main-border-color]);
- }
.ms-map-views-section,
.ms-map-views-layers-options-header {
.background-color-var(@theme-vars[main-bg]);
@@ -135,8 +129,11 @@
top: 0;
left: 40px;
width: 500px;
- .btn-default {
- border-color: transparent !important;
+ .ms-layers-tree {
+ padding: 0;
+ + .ms-layers-tree {
+ margin-top: 0.5rem;
+ }
}
.form-group.inline {
display: flex;
@@ -285,31 +282,6 @@
align-items: center;
justify-content: center;
}
- .ms-map-views-layer-node {
- border: 1px solid transparent;
- padding: 8px;
- margin-bottom: 4px;
- &.changed {
- border-left: 4px solid transparent;
- }
- .ms-map-views-layer-node-head {
- display: flex;
- align-items: center;
- .ms-map-views-layer-node-title {
- flex: 1;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- }
- }
- .ms-map-views-layer-node-body {
- padding-top: 16px;
- padding-left: 32px;
- }
- .ms-map-views-layer-clipping {
- position: relative;
- }
- }
.ms-map-views-layers-options-header {
position: sticky;
z-index: 2;
@@ -322,12 +294,6 @@
margin: 0;
}
}
- .ms-map-views-layers-options-body {
- width: 100%;
- padding: 0;
- margin: 0;
- list-style: none;
- }
.ms-map-views-section {
display: flex;
align-items: center;
diff --git a/web/client/translations/data.da-DK.json b/web/client/translations/data.da-DK.json
index bca47de03e..b30947ab5d 100644
--- a/web/client/translations/data.da-DK.json
+++ b/web/client/translations/data.da-DK.json
@@ -3409,7 +3409,12 @@
"showViewsGeometries": "Show views positions",
"addInitialView": "Click on the plus button to add a new view",
"removeView": "Remove view",
- "resetLayerOverrides": "Reset changes to align layer to default value",
+ "linkAllNodes": "Visibiliy and opacity properties of layers and groups will be connected with the main table of content",
+ "unlinkAllNodes": "Visibiliy and opacity properties of layers and groups will be disconneded from the main table of content",
+ "layersLinked": "Visibiliy and opacity properties are connected to the main table of content",
+ "layersUnlinked": "Visibiliy and opacity properties are disconnected from the main table of content",
+ "groupsLinked": "Visibiliy property is connected to the main table of content",
+ "groupsUnlinked": "Visibiliy property is disconnected from the main table of content",
"hideLayer": "Hide layer",
"showLayer": "Show layer",
"undoChanges": "Undo changes",
diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json
index df6b185b0e..5c61e664ab 100644
--- a/web/client/translations/data.de-DE.json
+++ b/web/client/translations/data.de-DE.json
@@ -3715,7 +3715,12 @@
"showViewsGeometries": "Views-Positionen anzeigen",
"addInitialView": "Klicken Sie auf die Plus-Schaltfläche, um eine neue Ansicht hinzuzufügen",
"removeView": "Ansicht entfernen",
- "resetLayerOverrides": "Änderungen zurücksetzen, um die Ebene auf den Standardwert auszurichten",
+ "linkAllNodes": "Sichtbarkeits- und Opazitätseigenschaften von Ebenen und Gruppen werden mit dem Hauptinhaltsverzeichnis verknüpft",
+ "unlinkAllNodes": "Sichtbarkeits- und Opazitätseigenschaften von Ebenen und Gruppen werden vom Hauptinhaltsverzeichnis getrennt",
+ "layersLinked": "Sichtbarkeits- und Opazitätseigenschaften sind mit dem Hauptinhaltsverzeichnis verknüpft",
+ "layersUnlinked": "Sichtbarkeits- und Opazitätseigenschaften sind vom Hauptinhaltsverzeichnis getrennt",
+ "groupsLinked": "Sichtbarkeitseigenschaft ist mit dem Hauptinhaltsverzeichnis verknüpft",
+ "groupsUnlinked": "Sichtbarkeitseigenschaft ist vom Hauptinhaltsverzeichnis getrennt",
"hideLayer": "Ebene ausblenden",
"showLayer": "Ebene anzeigen",
"undoChanges": "Änderungen rückgängig machen",
diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json
index d0d7e09dd9..02595d66d6 100644
--- a/web/client/translations/data.en-US.json
+++ b/web/client/translations/data.en-US.json
@@ -3688,7 +3688,12 @@
"showViewsGeometries": "Show views positions",
"addInitialView": "Click on the plus button to add a new view",
"removeView": "Remove view",
- "resetLayerOverrides": "Reset changes to align layer to default value",
+ "linkAllNodes": "Visibiliy and opacity properties of layers and groups will be connected with the main table of content",
+ "unlinkAllNodes": "Visibiliy and opacity properties of layers and groups will be disconneded from the main table of content",
+ "layersLinked": "Visibiliy and opacity properties are connected to the main table of content",
+ "layersUnlinked": "Visibiliy and opacity properties are disconnected from the main table of content",
+ "groupsLinked": "Visibiliy property is connected to the main table of content",
+ "groupsUnlinked": "Visibiliy property is disconnected from the main table of content",
"hideLayer": "Hide layer",
"showLayer": "Show layer",
"undoChanges": "Undo changes",
diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json
index 05ee0381eb..95460de5c8 100644
--- a/web/client/translations/data.es-ES.json
+++ b/web/client/translations/data.es-ES.json
@@ -3677,7 +3677,12 @@
"showViewsGeometries": "Mostrar posiciones de vistas",
"addInitialView": "Haga clic en el botón más para agregar una nueva vista",
"removeView": "Eliminar vista",
- "resetLayerOverrides": "Restablecer cambios para alinear la capa al valor predeterminado",
+ "linkAllNodes": "Las propiedades de visibilidad y opacidad de las capas y los grupos se conectarán con la tabla de contenido principal",
+ "unlinkAllNodes": "Las propiedades de visibilidad y opacidad de las capas y los grupos se desconectarán de la tabla de contenido principal",
+ "layersLinked": "Las propiedades de visibilidad y opacidad están conectadas a la tabla de contenido principal",
+ "layersUnlinked": "Las propiedades de visibilidad y opacidad están desconectadas de la tabla de contenido principal",
+ "groupsLinked": "La propiedad de visibilidad está conectada a la tabla de contenido principal",
+ "groupsUnlinked": "La propiedad de visibilidad está desconectada de la tabla de contenido principal",
"hideLayer": "Ocultar capa",
"showLayer": "Mostrar capa",
"undoChanges": "Deshacer cambios",
diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json
index 61c8d8f522..f1cbd8864b 100644
--- a/web/client/translations/data.fr-FR.json
+++ b/web/client/translations/data.fr-FR.json
@@ -3678,7 +3678,12 @@
"showViewsGeometries": "Afficher les positions des vues",
"addInitialView": "Cliquez sur le bouton plus pour ajouter une nouvelle vue",
"removeView": "Supprimer la vue",
- "resetLayerOverrides": "Réinitialise les modifications pour aligner le calque sur la valeur par défaut",
+ "linkAllNodes": "Les propriétés de visibilité et d'opacité des calques et des groupes seront connectées à la table des matières principale",
+ "unlinkAllNodes": "Les propriétés de visibilité et d'opacité des calques et des groupes seront déconnectées de la table des matières principale",
+ "layersLinked": "Les propriétés de visibilité et d'opacité sont connectées à la table des matières principale",
+ "layersUnlinked": "Les propriétés de visibilité et d'opacité sont déconnectées de la table des matières principale",
+ "groupsLinked": "La propriété de visibilité est connectée à la table des matières principale",
+ "groupsUnlinked": "La propriété de visibilité est déconnectée de la table des matières principale",
"hideLayer": "Masquer le calque",
"showLayer": "Afficher le calque",
"undoChanges": "Annuler les modifications",
diff --git a/web/client/translations/data.is-IS.json b/web/client/translations/data.is-IS.json
index 0a63e5d63a..c4dac582a1 100644
--- a/web/client/translations/data.is-IS.json
+++ b/web/client/translations/data.is-IS.json
@@ -3433,7 +3433,12 @@
"showViewsGeometries": "Show views positions",
"addInitialView": "Click on the plus button to add a new view",
"removeView": "Remove view",
- "resetLayerOverrides": "Reset changes to align layer to default value",
+ "linkAllNodes": "Visibiliy and opacity properties of layers and groups will be connected with the main table of content",
+ "unlinkAllNodes": "Visibiliy and opacity properties of layers and groups will be disconneded from the main table of content",
+ "layersLinked": "Visibiliy and opacity properties are connected to the main table of content",
+ "layersUnlinked": "Visibiliy and opacity properties are disconnected from the main table of content",
+ "groupsLinked": "Visibiliy property is connected to the main table of content",
+ "groupsUnlinked": "Visibiliy property is disconnected from the main table of content",
"hideLayer": "Hide layer",
"showLayer": "Show layer",
"undoChanges": "Undo changes",
diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json
index 9c2e32872f..8d2905dbc3 100644
--- a/web/client/translations/data.it-IT.json
+++ b/web/client/translations/data.it-IT.json
@@ -3676,7 +3676,12 @@
"showViewsGeometries": "Mostra le posizioni delle viste",
"addInitialView": "Fai clic sul pulsante più per aggiungere una nuova vista",
"removeView": "Rimuovi vista",
- "resetLayerOverrides": "Ripristina le modifiche per allineare il livello al valore predefinito",
+ "linkAllNodes": "Le proprietà di visibilità e opacità di livelli e gruppi saranno collegate alla lista dei layer principale",
+ "unlinkAllNodes": "Le proprietà di visibilità e opacità di livelli e gruppi saranno scollegate dalla lista dei layer principale",
+ "layersLinked": "Le proprietà di visibilità e opacità sono collegate alla lista dei layer principale",
+ "layersUnlinked": "Le proprietà di visibilità e opacità sono scollegate dalla lista dei layer principale",
+ "groupsLinked": "La proprietà di visibilità è collegata alla lista dei layer principale",
+ "groupsUnlinked": "La proprietà di visibilità è scollegata dalla lista dei layer principale",
"hideLayer": "Nascondi layer",
"showLayer": "Mostra layer",
"undoChanges": "Annulla modifiche",
diff --git a/web/client/translations/data.nl-NL.json b/web/client/translations/data.nl-NL.json
index f88e2c7bf2..79610ea350 100644
--- a/web/client/translations/data.nl-NL.json
+++ b/web/client/translations/data.nl-NL.json
@@ -3676,7 +3676,12 @@
"showViewsGeometries": "Toon positie van de overzichten",
"addInitialView": "Klik op de plusknop om een nieuw overzicht toe te voegen",
"removeView": "Verwijder overzicht",
- "resetLayerOverrides": "Aanpassingen resetten naar standaardwaarden",
+ "linkAllNodes": "Visibiliy and opacity properties of layers and groups will be connected with the main table of content",
+ "unlinkAllNodes": "Visibiliy and opacity properties of layers and groups will be disconneded from the main table of content",
+ "layersLinked": "Visibiliy and opacity properties are connected to the main table of content",
+ "layersUnlinked": "Visibiliy and opacity properties are disconnected from the main table of content",
+ "groupsLinked": "Visibiliy property is connected to the main table of content",
+ "groupsUnlinked": "Visibiliy property is disconnected from the main table of content",
"hideLayer": "Verberg laag",
"showLayer": "Toon laag",
"undoChanges": "Aanpassingen ongedaan maken",
diff --git a/web/client/utils/MapViewsUtils.js b/web/client/utils/MapViewsUtils.js
index e66030ca0b..63cc35cc0a 100644
--- a/web/client/utils/MapViewsUtils.js
+++ b/web/client/utils/MapViewsUtils.js
@@ -5,7 +5,7 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
-import isNumber from 'lodash/isNumber';
+import { isUndefined, omitBy, isNumber, isObject } from 'lodash';
const METERS_PER_DEGREES = 111194.87428468118;
@@ -99,6 +99,77 @@ export const mergeViewLayers = (layers, { layers: viewLayers = [] } = {}) => {
return layer;
});
};
+/**
+ * detect if a view layer is different from the map layers
+ * @param {object} viewLayer layer object
+ * @param {object} mapLayer layer object
+ */
+export const isViewLayerChanged = (viewLayer, mapLayer) => {
+ return viewLayer.visibility !== mapLayer.visibility
+ || viewLayer.opacity !== mapLayer.opacity
+ || viewLayer.clippingLayerResourceId !== mapLayer.clippingLayerResourceId
+ || viewLayer.clippingPolygonFeatureId !== mapLayer.clippingPolygonFeatureId
+ || viewLayer.clippingPolygonUnion !== mapLayer.clippingPolygonUnion;
+};
+/**
+ * pick view layer properties
+ * @param {object} node layer object
+ */
+export const pickViewLayerProperties = (node) => {
+ return omitBy({
+ id: node.id,
+ visibility: node.visibility,
+ opacity: node.opacity,
+ clippingLayerResourceId: node.clippingLayerResourceId,
+ clippingPolygonFeatureId: node.clippingPolygonFeatureId,
+ clippingPolygonUnion: node.clippingPolygonUnion
+ }, isUndefined);
+};
+/**
+ * pick view group properties
+ * @param {object} node group object
+ */
+export const pickViewGroupProperties = (node) => {
+ return omitBy({
+ id: node.id,
+ visibility: node.visibility
+ }, isUndefined);
+};
+/**
+ * merge the configuration of view groups in the main groups array
+ * @param {array} rawGroups array of group object
+ * @param {object} view map view configuration
+ * @param {boolean} recursive apply recursive merge instead of flat one
+ */
+export const mergeViewGroups = (groups, { groups: viewGroups = [] } = {}, recursive) => {
+ if (viewGroups.length === 0) {
+ return groups || [];
+ }
+ if (recursive) {
+ const recursiveMerge = (nodes) => {
+ return nodes.map((node) => {
+ if (isObject(node)) {
+ const viewGroup = viewGroups.find(vGroup => vGroup.id === node.id);
+ return {
+ ...node,
+ ...viewGroup,
+ ...(node.nodes && { nodes: recursiveMerge(node.nodes) }),
+ changed: true
+ };
+ }
+ return node;
+ });
+ };
+ return recursiveMerge(groups || []);
+ }
+ return (groups || []).map((group) => {
+ const viewGroup = viewGroups.find(vGroup => vGroup.id === group.id);
+ if (viewGroup) {
+ return { ...group, ...viewGroup, changed: true };
+ }
+ return group;
+ });
+};
/**
* Exclude all geometry but polygons and ensure each feature has an identifier
* @param {array} features array of GeoJSON features
diff --git a/web/client/utils/__tests__/MapViewsUtils-test.js b/web/client/utils/__tests__/MapViewsUtils-test.js
index 75a9532b0a..9518af5660 100644
--- a/web/client/utils/__tests__/MapViewsUtils-test.js
+++ b/web/client/utils/__tests__/MapViewsUtils-test.js
@@ -12,7 +12,11 @@ import {
formatClippingFeatures,
getZoomFromHeight,
getHeightFromZoom,
- cleanMapViewSavedPayload
+ cleanMapViewSavedPayload,
+ isViewLayerChanged,
+ pickViewLayerProperties,
+ pickViewGroupProperties,
+ mergeViewGroups
} from '../MapViewsUtils';
describe('Test MapViewsUtils', () => {
@@ -390,4 +394,55 @@ describe('Test MapViewsUtils', () => {
]
);
});
+ it('isViewLayerChanged', () => {
+ expect(isViewLayerChanged({ }, { })).toBe(false);
+ expect(isViewLayerChanged({ title: 'Old title' }, { title: 'New title' })).toBe(false);
+ expect(isViewLayerChanged({ visibility: false }, { visibility: true })).toBe(true);
+ expect(isViewLayerChanged({ opacity: 0 }, { opacity: 1 })).toBe(true);
+ expect(isViewLayerChanged({ clippingLayerResourceId: '01' }, { clippingLayerResourceId: '02' })).toBe(true);
+ expect(isViewLayerChanged({ clippingPolygonFeatureId: '01' }, { clippingPolygonFeatureId: '02' })).toBe(true);
+ expect(isViewLayerChanged({ clippingPolygonUnion: false }, { clippingPolygonUnion: true })).toBe(true);
+ });
+ it('pickViewLayerProperties', () => {
+ expect(pickViewLayerProperties({
+ id: '01',
+ title: 'Layer',
+ url: '/url/to/wms',
+ type: 'wms',
+ layer: 'layer',
+ visibility: true,
+ opacity: 1,
+ clippingLayerResourceId: '01',
+ clippingPolygonFeatureId: '01',
+ clippingPolygonUnion: false,
+ expanded: false
+ })).toEqual({
+ id: '01',
+ visibility: true,
+ opacity: 1,
+ clippingLayerResourceId: '01',
+ clippingPolygonFeatureId: '01',
+ clippingPolygonUnion: false
+ });
+ });
+ it('pickViewGroupProperties', () => {
+ expect(pickViewGroupProperties({
+ id: '01',
+ title: 'Group',
+ visibility: true,
+ expanded: false
+ })).toEqual({
+ id: '01',
+ visibility: true
+ });
+ });
+ it('mergeViewGroups', () => {
+ expect(mergeViewGroups([{ id: '01', visibility: false }])).toEqual([{ id: '01', visibility: false }]);
+ expect(mergeViewGroups([{ id: '01', visibility: false }, { id: '02', visibility: false }], { groups: [{ id: '01', visibility: true }] }))
+ .toEqual([{ id: '01', visibility: true, changed: true }, { id: '02', visibility: false }]);
+ expect(mergeViewGroups(
+ [{ id: '01', visibility: false, nodes: ['layer01', { id: '02', visibility: false }] }],
+ { groups: [{ id: '02', visibility: true }] }
+ ), true).toEqual([ { id: '01', visibility: false, nodes: [ 'layer01', { id: '02', visibility: false } ] } ]);
+ });
});