diff --git a/web/client/components/map/cesium/Layer.jsx b/web/client/components/map/cesium/Layer.jsx
index c6ae4bfcb4e..1f2b21c1efc 100644
--- a/web/client/components/map/cesium/Layer.jsx
+++ b/web/client/components/map/cesium/Layer.jsx
@@ -9,7 +9,7 @@ import React from 'react';
import Layers from '../../../utils/cesium/Layers';
import PropTypes from 'prop-types';
-import { round, isNil, castArray, isFunction } from 'lodash';
+import { round, isNil, castArray, isFunction, isNumber } from 'lodash';
import { getResolutions } from '../../../utils/MapUtils';
import axios from '../../../libs/ajax';
import { getProxyCacheByUrl } from '../../../utils/ProxyUtils';
@@ -39,6 +39,7 @@ class CesiumLayer extends React.Component {
// in particular for detached layers (eg. Vector, WFS, 3D Tiles, ...)
const visibility = this.getVisibilityOption(this.props);
this._isMounted = true;
+ this.autorefreshTick = -1;
this.createLayer(this.props.type, { ...this.props.options, visibility }, this.props.position, this.props.map, this.props.securityToken);
if (this.props.options && this.layer && visibility) {
this.addLayer(this.props);
@@ -82,6 +83,10 @@ class CesiumLayer extends React.Component {
}
}
this.updateLayer(newProps, this.props);
+
+ if (newProps.autorefreshTicks) {
+ this.tryAutorefresh(newProps);
+ }
}
componentWillUnmount() {
@@ -473,6 +478,17 @@ class CesiumLayer extends React.Component {
}
this.props.map.scene.requestRender();
};
+
+ tryAutorefresh = (newProps) => {
+ const layerId = newProps.options.id;
+ const autorefreshTicks = newProps.autorefreshTicks;
+ const visible = this.getVisibilityOption(newProps);
+
+ if (visible && isNumber(autorefreshTicks[layerId]) && this.autorefreshTick < autorefreshTicks[layerId]) {
+ this.autorefreshTick = autorefreshTicks[layerId];
+ Layers.refreshLayer(this.props.type, this.layer);
+ }
+ };
}
export default CesiumLayer;
diff --git a/web/client/components/map/leaflet/Layer.jsx b/web/client/components/map/leaflet/Layer.jsx
index 6efc12c025c..9d967feaf40 100644
--- a/web/client/components/map/leaflet/Layer.jsx
+++ b/web/client/components/map/leaflet/Layer.jsx
@@ -10,6 +10,7 @@ import PropTypes from 'prop-types';
import Layers from '../../../utils/leaflet/Layers';
import isEqual from 'lodash/isEqual';
import isNil from 'lodash/isNil';
+import { isNumber } from 'lodash';
class LeafletLayer extends React.Component {
static propTypes = {
map: PropTypes.object,
@@ -32,6 +33,7 @@ class LeafletLayer extends React.Component {
componentDidMount() {
this.valid = true;
+ this.autorefreshTick = -1;
this.createLayer(
this.props.type,
this.props.options,
@@ -55,6 +57,10 @@ class LeafletLayer extends React.Component {
this.updateZIndex(newProps.position);
}
this.updateLayer(newProps, this.props);
+
+ if (newProps.autorefreshTicks) {
+ this.tryAutorefresh(newProps);
+ }
}
shouldComponentUpdate(newProps) {
@@ -221,6 +227,17 @@ class LeafletLayer extends React.Component {
}
return false;
};
+
+ tryAutorefresh = (newProps) => {
+ const layerId = newProps.options.id;
+ const autorefreshTicks = newProps.autorefreshTicks;
+ const visible = this.getVisibilityOption(newProps);
+
+ if (visible && isNumber(autorefreshTicks[layerId]) && this.autorefreshTick < autorefreshTicks[layerId]) {
+ this.autorefreshTick = autorefreshTicks[layerId];
+ Layers.refreshLayer(this.props.type, this.layer);
+ }
+ };
}
export default LeafletLayer;
diff --git a/web/client/components/map/openlayers/Layer.jsx b/web/client/components/map/openlayers/Layer.jsx
index c25ad48a511..242bebe8a05 100644
--- a/web/client/components/map/openlayers/Layer.jsx
+++ b/web/client/components/map/openlayers/Layer.jsx
@@ -46,12 +46,14 @@ export default class OpenlayersLayer extends React.Component {
onCreationError: () => {},
onWarning: () => {},
srs: "EPSG:3857"
+
};
componentDidMount() {
this.valid = true;
this.tilestoload = 0;
this.imagestoload = 0;
+ this.autorefreshTick = -1;
this.createLayer(
this.props.type,
this.props.options,
@@ -75,6 +77,9 @@ export default class OpenlayersLayer extends React.Component {
if (this.props.options) {
this.updateLayer(newProps, this.props);
}
+ if (newProps.autorefreshTicks) {
+ this.tryAutorefresh(newProps.options.id, newProps.autorefreshTicks);
+ }
}
componentWillUnmount() {
@@ -93,9 +98,7 @@ export default class OpenlayersLayer extends React.Component {
this.props.map.removeLayer(this.layer);
}
}
- if (this.refreshTimer) {
- clearInterval(this.refreshTimer);
- }
+
Layers.removeLayer(this.props.type, this.props.options, this.props.map, this.props.mapId, this.layer);
}
@@ -344,13 +347,6 @@ export default class OpenlayersLayer extends React.Component {
this.props.onLayerLoad(options.id, {error: true});
}
});
-
- if (options.refresh) {
- let counter = 0;
- this.refreshTimer = setInterval(() => {
- this.layer.getSource().updateParams(Object.assign({}, options.params, {_refreshCounter: counter++}));
- }, options.refresh);
- }
}
};
@@ -359,4 +355,11 @@ export default class OpenlayersLayer extends React.Component {
this.valid = valid;
return valid;
};
+
+ tryAutorefresh = (layerId, autorefreshTicks) => {
+ if (this.layer?.getVisible() && isNumber(autorefreshTicks[layerId]) && this.autorefreshTick < autorefreshTicks[layerId]) {
+ this.autorefreshTick = autorefreshTicks[layerId];
+ Layers.refreshLayer(this.props.type, this.layer);
+ }
+ }
}
diff --git a/web/client/components/map/openlayers/plugins/ArcGISFeatureLayer.js b/web/client/components/map/openlayers/plugins/ArcGISFeatureLayer.js
index 98df9c737a0..5fe3f068fa3 100644
--- a/web/client/components/map/openlayers/plugins/ArcGISFeatureLayer.js
+++ b/web/client/components/map/openlayers/plugins/ArcGISFeatureLayer.js
@@ -137,6 +137,12 @@ Layers.registerType('arcgis-feature', {
layer.setMaxResolution(options.maxResolution === undefined ? Infinity : options.maxResolution);
}
},
+ refresh: (layer) => {
+ const source = layer.getSource();
+ if (source) {
+ source.refresh();
+ }
+ },
render: () => {
return null;
}
diff --git a/web/client/components/map/openlayers/plugins/ArcGISLayer.js b/web/client/components/map/openlayers/plugins/ArcGISLayer.js
index 0fcb35f2644..de320b8f177 100644
--- a/web/client/components/map/openlayers/plugins/ArcGISLayer.js
+++ b/web/client/components/map/openlayers/plugins/ArcGISLayer.js
@@ -60,6 +60,16 @@ registerType('arcgis', {
layer.getSource().setTileLoadFunction(tileLoadFunction(newOptions));
}
},
+ refresh: (layer) => {
+ const source = layer.getSource();
+ if (source) {
+ source.updateParams(
+ Object.assign({}, source.getParams(), {
+ _refreshCounter: Date.now()
+ })
+ );
+ }
+ },
render: () => {
return null;
}
diff --git a/web/client/components/map/openlayers/plugins/FlatGeobufLayer.js b/web/client/components/map/openlayers/plugins/FlatGeobufLayer.js
index 4760eb24fec..6f7da2fb5f4 100644
--- a/web/client/components/map/openlayers/plugins/FlatGeobufLayer.js
+++ b/web/client/components/map/openlayers/plugins/FlatGeobufLayer.js
@@ -241,5 +241,11 @@ Layers.registerType(FGB_LAYER_TYPE, {
}
return null;
+ },
+ refresh: (layer) => {
+ const source = layer.getSource();
+ if (source) {
+ source.refresh();
+ }
}
});
diff --git a/web/client/components/map/openlayers/plugins/TMSLayer.js b/web/client/components/map/openlayers/plugins/TMSLayer.js
index 5dcfe493b73..81f685b6771 100644
--- a/web/client/components/map/openlayers/plugins/TMSLayer.js
+++ b/web/client/components/map/openlayers/plugins/TMSLayer.js
@@ -89,6 +89,16 @@ Layers.registerType('tms', {
|| !isEqual(oldOptions.requestRuleRefreshHash, newOptions.requestRuleRefreshHash)) {
layer.getSource().setTileLoadFunction(tileLoadFunction(newOptions));
}
+ },
+ refresh: (layer) => {
+ const source = layer.getSource();
+ if (source) {
+ // Looks like the cache of the browser is not cleared with source.clear() or source.refresh().
+ // So, if the map is static, the same tiles are requested,
+ // hence the browser doesn't request new tiles to the server (no https request made).
+ // if you disable caching in the browser's dev tools, the new tiles are requested to the server.
+ source.refresh();
+ }
}
});
diff --git a/web/client/components/map/openlayers/plugins/TileProviderLayer.js b/web/client/components/map/openlayers/plugins/TileProviderLayer.js
index e83d0fdfeb4..2798b3163a0 100644
--- a/web/client/components/map/openlayers/plugins/TileProviderLayer.js
+++ b/web/client/components/map/openlayers/plugins/TileProviderLayer.js
@@ -118,6 +118,16 @@ Layers.registerType('tileprovider', {
|| !isEqual(oldOptions.requestRuleRefreshHash, newOptions.requestRuleRefreshHash)) {
layer.getSource().setTileLoadFunction(tileLoadFunction(newOptions));
}
+ },
+ refresh: (layer) => {
+ const source = layer.getSource();
+ if (source) {
+ // Looks like the cache of the browser is not cleared with source.clear() or source.refresh().
+ // So, if the map is static, the same tiles are requested,
+ // hence the browser doesn't request new tiles to the server (no https request made).
+ // if you disable caching in the browser's dev tools, the new tiles are requested to the server.
+ source.refresh();
+ }
}
});
diff --git a/web/client/components/map/openlayers/plugins/WFSLayer.js b/web/client/components/map/openlayers/plugins/WFSLayer.js
index af2fd406356..9c8496c6543 100644
--- a/web/client/components/map/openlayers/plugins/WFSLayer.js
+++ b/web/client/components/map/openlayers/plugins/WFSLayer.js
@@ -190,6 +190,12 @@ Layers.registerType('wfs', {
layer.setMaxResolution(options.maxResolution === undefined ? Infinity : options.maxResolution);
}
},
+ refresh: (layer) => {
+ const source = layer.getSource();
+ if (source) {
+ source.refresh();
+ }
+ },
render: () => {
return null;
}
diff --git a/web/client/components/map/openlayers/plugins/WMSLayer.js b/web/client/components/map/openlayers/plugins/WMSLayer.js
index d44ea44aa37..d0efbe7526b 100644
--- a/web/client/components/map/openlayers/plugins/WMSLayer.js
+++ b/web/client/components/map/openlayers/plugins/WMSLayer.js
@@ -305,5 +305,26 @@ Layers.registerType('wms', {
}
}
return null;
+ },
+ refresh: (layer) => {
+ // *source.refresh() doesn't trigger an HTTPS request to reload tiles
+ // *source.updateParams() with a dummy parameter forces the source to reload tiles
+ const wmsSource = layer.get('wmsSource');
+ if (wmsSource) {
+ wmsSource.updateParams(
+ Object.assign({}, wmsSource.getParams(), {
+ _refreshCounter: Date.now()
+ })
+ );
+ }
+
+ const vectorSource = layer.getSource();
+ if (vectorSource) {
+ vectorSource.updateParams(
+ Object.assign({}, vectorSource.getParams(), {
+ _refreshCounter: Date.now()
+ })
+ );
+ }
}
});
diff --git a/web/client/components/map/openlayers/plugins/WMTSLayer.js b/web/client/components/map/openlayers/plugins/WMTSLayer.js
index 48731414875..8ce0a0364ab 100644
--- a/web/client/components/map/openlayers/plugins/WMTSLayer.js
+++ b/web/client/components/map/openlayers/plugins/WMTSLayer.js
@@ -202,6 +202,17 @@ const updateLayer = (layer, newOptions, oldOptions) => {
return null;
};
+const refreshLayer = (layer) => {
+ const source = layer.getSource();
+ if (source) {
+ // Looks like the cache of the browser is not cleared with source.clear() or source.refresh().
+ // So, if the map is static, the same tiles are requested,
+ // hence the browser doesn't request new tiles to the server (no https request made).
+ // if you disable caching in the browser's dev tools, the new tiles are requested to the server.
+ source.refresh();
+ }
+};
+
const hasSRS = (srs, layer) => {
const { tileMatrixSetName, tileMatrixSet } = WMTSUtils.getTileMatrix(layer, srs);
if (tileMatrixSet) {
@@ -214,4 +225,4 @@ const compatibleLayer = layer =>
head(CoordinatesUtils.getEquivalentSRS(layer.srs || 'EPSG:3857').filter(srs => hasSRS(srs, layer))) ? true : false;
-Layers.registerType('wmts', { create: createLayer, update: updateLayer, isCompatible: compatibleLayer });
+Layers.registerType('wmts', { create: createLayer, update: updateLayer, refresh: refreshLayer, isCompatible: compatibleLayer });
diff --git a/web/client/components/widgets/enhancers/dependenciesToAutorefresh.js b/web/client/components/widgets/enhancers/dependenciesToAutorefresh.js
new file mode 100644
index 00000000000..76de56c1642
--- /dev/null
+++ b/web/client/components/widgets/enhancers/dependenciesToAutorefresh.js
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2026, 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 { compose, withProps } from 'recompose';
+import Rx from 'rxjs';
+import { isNumber } from 'lodash';
+
+import propsStreamFactory from '../../misc/enhancers/propsStreamFactory';
+
+let currentTicks = {};
+
+/**
+ * Set the property `shouldAutorefresh` to true if the widget should autorefresh based on the autorefreshTicks and layer.id.
+ * @param {object} props the widget props
+ * @param {{[prop: string]: number}} props.autorefreshTicks an object with the layer.id as key and the ticks as value
+ * @param {object} props.layer the layer object where to perform the requests, used to get the layer.id to check against the autorefreshTicks
+ * @returns {Rx.Observable<{shouldAutorefresh: boolean}>} an observable that emits an object with the property shouldAutorefresh (boolean)
+ * @example
+ * // in a widget component
+ * const MyWidget = ({shouldAutorefresh}) => {
+ * useEffect(() => {
+ * if (shouldAutorefresh) {
+ * // trigger data fetch
+ * }
+ * }, [shouldAutorefresh]);
+ */
+const dataStreamFactory = ($props) =>
+ $props
+ .switchMap(
+ ({autorefreshTicks, layer}) => {
+ let shouldAutorefresh = false;
+
+ if (layer?.id && Object.keys(autorefreshTicks).length && isNumber(autorefreshTicks[layer.id]) &&
+ currentTicks[layer.id] !== autorefreshTicks[layer.id]) {
+ currentTicks[layer.id] = autorefreshTicks[layer.id];
+ shouldAutorefresh = true;
+ }
+
+ return Rx.Observable.of({
+ shouldAutorefresh
+ });
+ }
+ );
+
+export default compose(
+ withProps( () => ({
+ dataStreamFactory
+ })),
+ propsStreamFactory
+);
diff --git a/web/client/components/widgets/enhancers/wpsCounter.js b/web/client/components/widgets/enhancers/wpsCounter.js
index 5857c96de17..2aeb35a38c5 100644
--- a/web/client/components/widgets/enhancers/wpsCounter.js
+++ b/web/client/components/widgets/enhancers/wpsCounter.js
@@ -23,7 +23,6 @@ const sameOptions = (o1 = {}, o2 = {}) =>
import { getWpsUrl } from '../../../utils/LayersUtils';
import { checkMapSyncWithWidgetOfMapType } from '../../../utils/WidgetsUtils';
-
/**
* Stream of props -> props to retrieve data from WPS aggregate process on params changes.
* Can be used with widgets and charts to auto-update data on property changes.
@@ -32,20 +31,32 @@ import { checkMapSyncWithWidgetOfMapType } from '../../../utils/WidgetsUtils';
*/
const dataStreamFactory = ($props) =>
$props
- .filter(({layer = {}, options, dependencies, mapSync, dependenciesMap, widgets}) => {
+ .filter(({layer = {}, options, dependencies, mapSync, dependenciesMap, widgets, shouldAutorefresh }) => {
+ const isValid = layer.name && getWpsUrl(layer) && options && options.aggregateFunction && options.aggregationAttribute;
+
+ if (shouldAutorefresh) {
+ return isValid;
+ }
+
// Check if mapSync is enabled (true), dependencyMap has mapSync dependency to Map widget and dependencies.viewport is null or falsy
// If this condition is true, return false to filter out the event.
// This prevents an extra API call from being triggered when the viewport is not available.
if (mapSync && checkMapSyncWithWidgetOfMapType(widgets, dependenciesMap) && !dependencies?.viewport) {
return false;
}
- return layer.name && getWpsUrl(layer) && options && options.aggregateFunction && options.aggregationAttribute;
+ return isValid;
})
.distinctUntilChanged(
- ({layer = {}, options = {}, filter}, newProps) =>
- (newProps.layer && layer.name === newProps.layer.name && layer.loadingError === newProps.layer.loadingError)
+ ({layer = {}, options = {}, filter }, newProps) => {
+ if (newProps.shouldAutorefresh) {
+ return false;
+ }
+
+ return (newProps.layer && layer.name === newProps.layer.name && layer.loadingError === newProps.layer.loadingError)
&& sameOptions(options, newProps.options)
- && sameFilter(filter, newProps.filter))
+ && sameFilter(filter, newProps.filter);
+ }
+ )
.switchMap(
({layer = {}, options, filter, onLoad = () => {}, onLoadError = () => {}}) =>
wpsAggregate(
diff --git a/web/client/components/widgets/widget/enhancedWidgets.js b/web/client/components/widgets/widget/enhancedWidgets.js
index 4e82e9a7af0..9752853bd56 100644
--- a/web/client/components/widgets/widget/enhancedWidgets.js
+++ b/web/client/components/widgets/widget/enhancedWidgets.js
@@ -32,6 +32,7 @@ import BaseCounterWidget from './CounterWidget';
import BaseLegendWidget from './LegendWidget';
import BaseFilterWidget from './FilterWidget';
import dependenciesToShapes from '../enhancers/dependenciesToShapes';
+import dependenciesToAutorefresh from '../enhancers/dependenciesToAutorefresh';
//
// connect widgets to dependencies, remote services and add base icons/tools
@@ -110,6 +111,7 @@ export const CounterWidget = compose(
dependenciesToWidget,
dependenciesToFilter,
dependenciesToOptions,
+ dependenciesToAutorefresh,
wpsCounter,
counterWidget
)(BaseCounterWidget);
diff --git a/web/client/configs/pluginsConfig.json b/web/client/configs/pluginsConfig.json
index 2da686f10d5..b3e72320bd1 100644
--- a/web/client/configs/pluginsConfig.json
+++ b/web/client/configs/pluginsConfig.json
@@ -618,6 +618,14 @@
"title": "plugins.Isochrone.title",
"description": "plugins.Isochrone.description",
"dependencies": ["SidebarMenu"]
+ },
+ {
+ "name": "AutoRefresh",
+ "glyph": "refresh",
+ "title": "plugins.AutoRefresh.title",
+ "description": "plugins.AutoRefresh.description",
+ "dependencies": ["MapFooter"],
+ "defaultConfig": {}
}
]
}
diff --git a/web/client/plugins/AutoRefresh/AutoRefresh.jsx b/web/client/plugins/AutoRefresh/AutoRefresh.jsx
new file mode 100644
index 00000000000..64a3100a4f3
--- /dev/null
+++ b/web/client/plugins/AutoRefresh/AutoRefresh.jsx
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2026, 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 PropTypes from "prop-types";
+import { connect } from "react-redux";
+
+import usePluginItems from "../../hooks/usePluginItems";
+import AutoRefreshContainer from './containers/AutoRefresh';
+import { createPlugin } from '../../utils/PluginsUtils';
+import { createStructuredSelector } from 'reselect';
+import { layersSelector } from '../../selectors/layers';
+import { userRoleSelector } from '../../selectors/security';
+import { CONTROL_NAME } from './constants';
+import { autorefreshUpdateAvailableLayers, autorefreshStart, autorefreshStop, autorefreshUpdateActiveLayer } from './actions/autorefresh';
+import { updateNode } from '../../actions/layers';
+import { autorefreshArchivedTicksSelector, autorefreshAvailableLayersSelector, autorefreshEnabledSelector, autorefreshLayersSelector } from './selectors/autorefresh';
+import autorefresh from './reducers/autorefresh';
+import {
+ autorefreshStartEpicCreation,
+ autorefreshUpdateNodeEpicCreation,
+ autorefreshRemoveNodeEpicCreation,
+ autorefreshActiveLayerChangeEpicCreation,
+ autorefreshMapVisualisationModeChangeEpicCreation
+} from './epics/autorefresh';
+import { mapTypeSelector } from '../../selectors/maptype';
+
+const AutoRefresh = ({ items, ...props }, context) => {
+ const { loadedPlugins } = context;
+ const configuredItems = usePluginItems({ items, loadedPlugins });
+
+ return (
+
+ );
+};
+
+AutoRefresh.contextTypes = {
+ loadedPlugins: PropTypes.object
+};
+
+const autoRefreshConnect = connect(
+ createStructuredSelector({
+ userRoles: userRoleSelector,
+ mapType: mapTypeSelector,
+ layers: layersSelector,
+
+ enabled: autorefreshEnabledSelector,
+ availableLayers: autorefreshAvailableLayersSelector,
+ activeLayers: autorefreshLayersSelector,
+ ticks: autorefreshArchivedTicksSelector
+ }), {
+ onStart: autorefreshStart,
+ onStop: autorefreshStop,
+ onUpdateLayer: autorefreshUpdateActiveLayer,
+ onUpdateAvailableLayers: autorefreshUpdateAvailableLayers,
+ onUpdateNode: updateNode
+ }
+);
+
+const AutoRefreshComponent = autoRefreshConnect(AutoRefresh);
+
+AutoRefreshComponent.propTypes = {
+ items: PropTypes.array
+};
+
+export default createPlugin(
+ 'AutoRefresh',
+ {
+ component: AutoRefreshComponent,
+ reducers: {
+ autorefresh
+ },
+ epics: {
+ autorefreshStartEpicCreation,
+ autorefreshUpdateNodeEpicCreation,
+ autorefreshRemoveNodeEpicCreation,
+ autorefreshActiveLayerChangeEpicCreation,
+ autorefreshMapVisualisationModeChangeEpicCreation
+ },
+ containers: {
+ SidebarMenu: {},
+ BurgerMenu: {},
+ MapFooter: {
+ name: CONTROL_NAME,
+ position: 20,
+ target: 'right-footer',
+ priority: 1
+ }
+ }
+ }
+);
diff --git a/web/client/plugins/AutoRefresh/actions/autorefresh.js b/web/client/plugins/AutoRefresh/actions/autorefresh.js
new file mode 100644
index 00000000000..7b89a225692
--- /dev/null
+++ b/web/client/plugins/AutoRefresh/actions/autorefresh.js
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2026, 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.
+ */
+
+export const AUTOREFRESH_START = 'AUTOREFRESH:START';
+export const AUTOREFRESH_STOP = 'AUTOREFRESH:STOP';
+export const AUTOREFRESH_TICK = 'AUTOREFRESH:TICK';
+export const AUTOREFRESH_UPDATE_ACTIVE_LAYER = 'AUTOREFRESH:UPDATE_ACTIVE_LAYER';
+export const AUTOREFRESH_DELETE_ACTIVE_LAYER = 'AUTOREFRESH:DELETE_ACTIVE_LAYER';
+export const AUTOREFRESH_UPDATE_AVAILABLE_LAYERS = 'AUTOREFRESH:UPDATE_AVAILABLE_LAYERS';
+export const AUTOREFRESH_UPDATE_ACTIVE_LAYERS = 'AUTOREFRESH:UPDATE_ACTIVE_LAYERS';
+
+
+export const autorefreshStart = () => {
+ return {
+ type: AUTOREFRESH_START,
+ enabled: true
+ };
+};
+
+export const autorefreshTick = (ticks) => {
+ return {
+ type: AUTOREFRESH_TICK,
+ ticks
+ };
+};
+
+export const autorefreshStop = () => {
+ return {
+ type: AUTOREFRESH_STOP,
+ enabled: false
+ };
+};
+
+export const autorefreshUpdateActiveLayer = (layer) => {
+ return {
+ type: AUTOREFRESH_UPDATE_ACTIVE_LAYER,
+ layer
+ };
+};
+
+export const autorefreshDeleteActiveLayer = (layerId) => {
+ return {
+ type: AUTOREFRESH_DELETE_ACTIVE_LAYER,
+ layerId
+ };
+};
+
+export const autorefreshUpdateAvailableLayers = (layers) => {
+ return {
+ type: AUTOREFRESH_UPDATE_AVAILABLE_LAYERS,
+ availableLayers: layers
+ };
+};
+
+export const autorefreshUpdateActiveLayers = (layers) => {
+ return {
+ type: AUTOREFRESH_UPDATE_ACTIVE_LAYERS,
+ activeLayers: layers
+ };
+};
diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx
new file mode 100644
index 00000000000..18f70cd7ee1
--- /dev/null
+++ b/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2026, 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 { Glyphicon, ControlLabel, Form, FormGroup, FormControl, InputGroup } from "react-bootstrap";
+
+import tooltip from '../../../components/misc/enhancers/tooltip';
+import ButtonRB from '../../../components/misc/Button';
+import Message from '../../../components/I18N/Message';
+import { AUTOREFRESH_STEP_INTERVAL_IN_SECONDS, formatDate } from '../constants';
+
+const Button = tooltip(ButtonRB);
+
+const AutoRefreshForm = ({
+ defaultRefreshInterval,
+ minRefreshInterval,
+ availableLayers = {},
+ activeLayers = {},
+ ticks = {},
+ handleAddLayer,
+ handleRemoveLayer,
+ handleIntervalChange
+}) => {
+
+ const onIntervalChange = (event, layer) => {
+ const { value } = event.target || {};
+ const numericValue = Number(value);
+
+ handleIntervalChange(numericValue < minRefreshInterval ? minRefreshInterval : numericValue, layer.id);
+ };
+
+ const onAddLayer = (e) => {
+ if (e.target.value === "none") {
+ return;
+ }
+
+ handleAddLayer(e.target.value, defaultRefreshInterval);
+ e.target.value = "none";
+ };
+
+
+ return (
+
+
+
+
+ );
+};
+
+export default AutoRefreshForm;
diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx
new file mode 100644
index 00000000000..6ffc8ab1ad3
--- /dev/null
+++ b/web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2026, 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, { useCallback } from 'react';
+import { Glyphicon, Dropdown, MenuItem } from 'react-bootstrap';
+
+import Message from '../../../components/I18N/Message';
+import tooltip from '../../../components/misc/enhancers/tooltip';
+import ButtonRB from '../../../components/misc/Button';
+import { formatDate } from '../constants';
+
+const Button = tooltip(ButtonRB);
+
+const AutoRefreshInformationsMenu = React.forwardRef((props, ref) => {
+ return (
+
+
+
+ {props.children}
+
+ );
+});
+
+
+const AutoRefreshInformations = ({
+ layers = [],
+ ticks = {}
+}) => {
+ const getFullyQualifiedLayerTitle = useCallback((layer) => {
+ const title = layer.title;
+ const interval = `(${layer.autorefreshInterval}s)`;
+ const lastUpdate = ticks[layer.id] && layer.visibility ? formatDate(ticks[layer.id]) : '';
+ return `${title} ${interval} ${lastUpdate}`;
+ }, [ticks]);
+
+ return (
+
+ }
+ tooltipPosition="top">
+
+
+
+
+ {layers.length === 0 &&
}
+ {layers.length > 0 &&
+ layers.map(l => (
+ {l.title}
+ {ticks[l.id] && l.visibility && {formatDate(ticks[l.id])}}
+ {l.autorefreshInterval}s
+
))}
+
+
+
+
);
+};
+
+export default AutoRefreshInformations;
diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshMenu.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshMenu.jsx
new file mode 100644
index 00000000000..44794a6c07f
--- /dev/null
+++ b/web/client/plugins/AutoRefresh/components/AutoRefreshMenu.jsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2026, 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';
+
+const AutoRefreshMenu = React.forwardRef((props, ref) => {
+ return (
+
+ {props.children}
+
+ );
+});
+
+export default AutoRefreshMenu;
diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshSettings.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshSettings.jsx
new file mode 100644
index 00000000000..028c8648b0f
--- /dev/null
+++ b/web/client/plugins/AutoRefresh/components/AutoRefreshSettings.jsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2026, 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 { Glyphicon, Dropdown } from 'react-bootstrap';
+
+import Message from '../../../components/I18N/Message';
+import AutoRefreshMenu from './AutoRefreshMenu';
+import AutoRefreshForm from './AutoRefreshForm';
+import { NodeTypes } from '../../../utils/LayersUtils';
+import { generateAutorefreshLayerOptions } from '../constants';
+import tooltip from '../../../components/misc/enhancers/tooltip';
+import ButtonRB from '../../../components/misc/Button';
+
+const Button = tooltip(ButtonRB);
+
+const AutoRefreshSettings = ({
+ defaultRefreshInterval,
+ minimumRefreshInterval,
+ availableLayers,
+ activeLayers,
+ ticks,
+ onUpdateNode
+}) => {
+
+ const handleIntervalChange = (interval, layerId) => {
+ onUpdateNode(layerId, NodeTypes.LAYER, generateAutorefreshLayerOptions(interval));
+ };
+
+ const handleAddLayer = (layerId, interval) => {
+ onUpdateNode(layerId, NodeTypes.LAYER, generateAutorefreshLayerOptions(interval));
+ };
+
+ const handleRemoveLayer = (layerId) =>{
+ onUpdateNode(layerId, NodeTypes.LAYER, generateAutorefreshLayerOptions(-1));
+ };
+
+ return (
+ }
+ tooltipPosition="top">
+
+
+
+
+
+ );
+};
+
+export default AutoRefreshSettings;
diff --git a/web/client/plugins/AutoRefresh/constants.js b/web/client/plugins/AutoRefresh/constants.js
new file mode 100644
index 00000000000..2db4cad92f7
--- /dev/null
+++ b/web/client/plugins/AutoRefresh/constants.js
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2026, 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.
+ */
+
+export const CONTROL_NAME = "autorefresh";
+export const AUTOREFRESH_STEP_INTERVAL_IN_SECONDS = 5;
+export const AUTOREFRESH_MINIMUM_REFRESH_INTERVAL = 30000;
+export const AUTOREFRESH_DEFAULT_REFRESH_INTERVAL = 60000;
+
+export const generateAutorefreshLayerOptions = (interval) => ({
+ autorefreshInterval: interval
+});
+
+export const formatDate = (timestamp) => {
+ if (!timestamp) {
+ return null;
+ }
+
+ const date = new Date(timestamp);
+
+ const options = {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ timeZone: new Intl.DateTimeFormat().resolvedOptions().timeZone
+ };
+ return `(${date.toLocaleString(navigator.language, options)})`;
+};
diff --git a/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx b/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx
new file mode 100644
index 00000000000..a4ad6ad773c
--- /dev/null
+++ b/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx
@@ -0,0 +1,102 @@
+/*
+ * 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, { useEffect, useState } from 'react';
+import {Checkbox } from "react-bootstrap";
+
+import {
+ hasAutoRefreshCapability
+} from '../../../utils/LayersUtils';
+import Message from '../../../components/I18N/Message';
+import {
+ AUTOREFRESH_DEFAULT_REFRESH_INTERVAL,
+ AUTOREFRESH_MINIMUM_REFRESH_INTERVAL
+} from '../constants';
+import AutoRefreshInformations from '../components/AutoRefreshInformations';
+import AutoRefreshSettings from '../components/AutoRefreshSettings';
+
+// Do not consider background layers, since they are not expected to be updated frequently
+// and they are not visible in the layer switcher,
+// so they cannot be selected by the user in the settings
+const LAYER_GROUPS_TO_IGNORE = ['background'];
+const AUTHORIZED_ACCESS_ROLES = ['ADMIN'];
+
+/**
+ * AutoRefresh container component. It manages the state and the logic of the AutoRefresh plugin.
+ * @param {object} props - The props of the container
+ * @param {number} props.defaultRefreshInterval - The default refresh interval in milliseconds
+ * @param {number} props.minimumRefreshInterval - The minimum refresh interval in milliseconds
+ */
+const AutoRefreshContainer = ({
+ // Configured by the user when added to a Context
+ defaultRefreshInterval = AUTOREFRESH_DEFAULT_REFRESH_INTERVAL,
+ minimumRefreshInterval = AUTOREFRESH_MINIMUM_REFRESH_INTERVAL,
+
+ // Common store
+ userRoles,
+ mapType,
+ layers,
+
+ // Common actions
+ onUpdateNode,
+
+ // AutoRefresh store
+ enabled,
+ availableLayers,
+ activeLayers,
+ ticks,
+
+ // AutoRefresh actions
+ onStart,
+ onStop,
+ onUpdateAvailableLayers
+}) => {
+ const [lastUpdatedText] = useState(null);
+
+ const handleAutorefreshActivated = (event) => {
+ const { checked } = event.target || {};
+ if (checked) {
+ onStart();
+ } else {
+ onStop();
+ }
+ };
+
+ useEffect(() => {
+ const availables = layers.filter(l => !LAYER_GROUPS_TO_IGNORE.includes(l.group) && hasAutoRefreshCapability(l.type, mapType));
+
+ onUpdateAvailableLayers(availables.reduce((acc, layer) => {
+ acc[layer.id] = layer;
+ return acc;
+ }, {}));
+ }, [layers]);
+
+
+ return (
+
+
+
+
+ {/* Non-admin users see the layers information's panel,
+ Admin users see the settings panel
+ */}
+ {AUTHORIZED_ACCESS_ROLES.includes(userRoles) &&
}
+ {!AUTHORIZED_ACCESS_ROLES.includes(userRoles) &&
}
+ );
+};
+
+export default AutoRefreshContainer;
diff --git a/web/client/plugins/AutoRefresh/epics/autorefresh.js b/web/client/plugins/AutoRefresh/epics/autorefresh.js
new file mode 100644
index 00000000000..ba6557c1e93
--- /dev/null
+++ b/web/client/plugins/AutoRefresh/epics/autorefresh.js
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2026, 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 { Observable } from 'rxjs';
+
+import { AUTOREFRESH_TICK,
+ AUTOREFRESH_START,
+ AUTOREFRESH_STOP,
+ autorefreshUpdateActiveLayer,
+ autorefreshDeleteActiveLayer,
+ autorefreshStart,
+ autorefreshStop,
+ AUTOREFRESH_UPDATE_ACTIVE_LAYER,
+ AUTOREFRESH_DELETE_ACTIVE_LAYER,
+ autorefreshUpdateAvailableLayers,
+ autorefreshUpdateActiveLayers
+} from "../actions/autorefresh";
+import { REMOVE_NODE, UPDATE_NODE } from '../../../actions/layers';
+import { hasAutoRefreshCapability, NodeTypes } from '../../../utils/LayersUtils';
+import { VISUALIZATION_MODE_CHANGED } from '../../../actions/maptype';
+import { layersSelector } from '../../../selectors/layers';
+import { mapTypeSelector } from '../../../selectors/maptype';
+
+export const autorefreshStartEpicCreation = (action$, store) => action$
+ .ofType(AUTOREFRESH_START)
+ .switchMap(() => {
+ const ticks = {};
+ const activeLayers = store.getState()?.autorefresh.activeLayers || {};
+ Object.values(activeLayers).forEach(layer => {
+ const interval = layer.autorefreshInterval || -1;
+ if (interval > 0) {
+ ticks[interval] = {
+ ...(ticks[interval] || {}),
+ [layer.id]: Date.now()
+ };
+ }
+ });
+
+ return Observable.from(
+ Object.keys(ticks).map(interval =>
+ Observable.interval(interval * 1000)
+ .map(() => ({
+ type: AUTOREFRESH_TICK,
+ ticks: Object.keys(ticks[interval]).reduce((acc, layerId) => {
+ acc[layerId] = Date.now();
+ return acc;
+ }, {})
+ }))
+ .takeUntil(action$.ofType(AUTOREFRESH_STOP))
+ )
+ ).mergeAll();
+ });
+
+export const autorefreshActiveLayerChangeEpicCreation = (action$, store) => action$
+ .ofType(AUTOREFRESH_UPDATE_ACTIVE_LAYER, AUTOREFRESH_DELETE_ACTIVE_LAYER)
+ .debounceTime(200)
+ .switchMap(() => store.getState().autorefresh.enabled ? Observable.of(
+ autorefreshStop(),
+ autorefreshStart()
+ ) : Observable.of(autorefreshStop()));
+
+/**
+ * Follow update on nodes (layers) to update the AutoRefresh plugin accordingly
+ * (add/update layer on AutoRefresh)
+ * @param {*} action$
+ * @param {*} store
+ * @returns
+ */
+export const autorefreshUpdateNodeEpicCreation = (action$, store) => action$
+ .ofType(UPDATE_NODE)
+ .filter(nodeConfig => nodeConfig.nodeType === NodeTypes.LAYER)
+ .filter(nodeConfig => {
+ const activeLayers = store.getState()?.autorefresh.activeLayers || {};
+ const isActiveLayer = activeLayers[nodeConfig.node] !== undefined;
+ const isAutorefreshIntervalChange = 'autorefreshInterval' in nodeConfig.options;
+
+ return isActiveLayer && isAutorefreshIntervalChange;
+ })
+ .switchMap((nodeConfig) => {
+ const autorefreshInterval = 'autorefreshInterval' in nodeConfig.options ?
+ nodeConfig.options.autorefreshInterval :
+ layersSelector(store.getState())
+ .find(l => l.id === nodeConfig.node)?.autorefreshInterval || -1;
+
+ return Observable.of(
+ autorefreshUpdateActiveLayer({
+ id: nodeConfig.node,
+ autorefreshInterval
+ })
+ );
+ });
+
+/**
+ * Follow removal of nodes (layers) to update the AutoRefresh plugin accordingly
+ * (remove layer on AutoRefresh)
+ * @param {*} action$
+ * @returns
+ */
+export const autorefreshRemoveNodeEpicCreation = (action$) => action$
+ .ofType(REMOVE_NODE)
+ .filter(nodeConfig => nodeConfig.nodeType === NodeTypes.LAYER)
+ .switchMap((nodeConfig) => {
+ return Observable.of(
+ autorefreshDeleteActiveLayer(nodeConfig.node)
+ );
+ });
+
+/**
+ * Follow changes in the map visualization mode (2D/3D) to update the AutoRefresh plugin accordingly
+ * (update available and active layers on AutoRefresh)
+ * @param {*} action$
+ * @param {*} store
+ * @returns
+ */
+export const autorefreshMapVisualisationModeChangeEpicCreation = (action$, store) => action$
+ .ofType(VISUALIZATION_MODE_CHANGED)
+ .switchMap(() => {
+ const layers = layersSelector(store.getState());
+ const mapType = mapTypeSelector(store.getState());
+
+ const availableLayers = layers.filter(l => hasAutoRefreshCapability(l.type, mapType));
+ const activeLayers = availableLayers.filter(l => l.autorefreshInterval > 0).reduce((acc, layer) => {
+ acc[layer.id] = {
+ id: layer.id,
+ autorefreshInterval: layer.autorefreshInterval
+ };
+ return acc;
+ }, {});
+
+ return Observable.of(
+ autorefreshStop(),
+ autorefreshUpdateActiveLayers(activeLayers),
+ autorefreshUpdateAvailableLayers(availableLayers.reduce((acc, layer) => {
+ acc[layer.id] = layer;
+ return acc;
+ }, {}))
+ );
+ });
diff --git a/web/client/plugins/AutoRefresh/index.js b/web/client/plugins/AutoRefresh/index.js
new file mode 100644
index 00000000000..6af648436d4
--- /dev/null
+++ b/web/client/plugins/AutoRefresh/index.js
@@ -0,0 +1,11 @@
+/*
+ * 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 AutoRefresh from './AutoRefresh';
+
+export default AutoRefresh;
diff --git a/web/client/plugins/AutoRefresh/reducers/autorefresh.js b/web/client/plugins/AutoRefresh/reducers/autorefresh.js
new file mode 100644
index 00000000000..2f8cc7e9e1c
--- /dev/null
+++ b/web/client/plugins/AutoRefresh/reducers/autorefresh.js
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2026, 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 {
+ AUTOREFRESH_TICK,
+ AUTOREFRESH_START,
+ AUTOREFRESH_STOP,
+ AUTOREFRESH_UPDATE_ACTIVE_LAYER,
+ AUTOREFRESH_UPDATE_AVAILABLE_LAYERS,
+ AUTOREFRESH_DELETE_ACTIVE_LAYER,
+ AUTOREFRESH_UPDATE_ACTIVE_LAYERS} from "../actions/autorefresh";
+
+
+const defaultState = {
+ enabled: false,
+ availableLayers: {},
+ activeLayers: {},
+ ticks: {},
+ archivedTicks: {}
+};
+
+const autorefresh = (state = {...defaultState}, action) => {
+ const activeLayers = {...state.activeLayers};
+
+ switch (action.type) {
+ case AUTOREFRESH_START:
+ return {
+ ...state,
+ enabled: true,
+ ticks: {}
+ };
+ case AUTOREFRESH_STOP:
+ return {
+ ...state,
+ enabled: false,
+ archivedTicks: {
+ ...state.archivedTicks,
+ ...state.ticks
+ }
+ };
+ case AUTOREFRESH_TICK:
+ return {
+ ...state,
+ ticks: action.ticks,
+ archivedTicks: {
+ ...state.archivedTicks,
+ ...action.ticks
+ }
+ };
+ case AUTOREFRESH_UPDATE_ACTIVE_LAYERS:
+ return {
+ ...state,
+ activeLayers: action.activeLayers,
+ enabled: Object.keys(action.activeLayers).length > 0 && state.enabled ? state.enabled : false
+ };
+ case AUTOREFRESH_UPDATE_AVAILABLE_LAYERS:
+ const availableLayers = {
+ ...state.availableLayers,
+ ...action.availableLayers
+ };
+ const actives = {
+ ...state.activeLayers
+ };
+
+ Object.keys(availableLayers).forEach(layerId => {
+ if (availableLayers[layerId].autorefreshInterval > -1) {
+ actives[layerId] = availableLayers[layerId];
+ }
+ if (actives[layerId]) {
+ delete availableLayers[layerId];
+ }
+ });
+
+ return {
+ ...state,
+ availableLayers: availableLayers,
+ activeLayers: actives
+ };
+ case AUTOREFRESH_UPDATE_ACTIVE_LAYER:
+ if (action.layer.autorefreshInterval === -1) {
+ delete activeLayers[action.layer.id];
+ } else {
+ activeLayers[action.layer.id] = {
+ ...activeLayers[action.layer.id],
+ ...action.layer
+ };
+ }
+
+ return {
+ ...state,
+ activeLayers,
+ enabled: Object.keys(activeLayers).length > 0 && state.enabled ? state.enabled : false
+ };
+ case AUTOREFRESH_DELETE_ACTIVE_LAYER:
+ const al = {
+ ...state.availableLayers
+ };
+
+ delete activeLayers[action.layerId];
+ delete al[action.layerId];
+
+ return {
+ ...state,
+ activeLayers,
+ availableLayers: al,
+ enabled: Object.keys(activeLayers).length > 0 && state.enabled ? state.enabled : false
+ };
+ default:
+ return state;
+ }
+};
+
+export default autorefresh;
diff --git a/web/client/plugins/AutoRefresh/selectors/autorefresh.js b/web/client/plugins/AutoRefresh/selectors/autorefresh.js
new file mode 100644
index 00000000000..ddbcf272cac
--- /dev/null
+++ b/web/client/plugins/AutoRefresh/selectors/autorefresh.js
@@ -0,0 +1,16 @@
+import get from "lodash/get";
+import { CONTROL_NAME } from "../constants";
+import { createControlEnabledSelector } from "../../../selectors/controls";
+
+export const enabledSelector = createControlEnabledSelector(CONTROL_NAME);
+
+export const autorefreshEnabledSelector = (state) => get(state, `${CONTROL_NAME}.enabled`, false);
+
+export const autorefreshLayersSelector = (state) => get(state, `${CONTROL_NAME}.activeLayers`, {});
+
+export const autorefreshTicksSelector = (state) => get(state, `${CONTROL_NAME}.ticks`, {});
+
+export const autorefreshArchivedTicksSelector = (state) => get(state, `${CONTROL_NAME}.archivedTicks`, {});
+
+export const autorefreshAvailableLayersSelector = (state) => get(state, `${CONTROL_NAME}.availableLayers`, {});
+
diff --git a/web/client/plugins/Widgets.jsx b/web/client/plugins/Widgets.jsx
index 4a7da5815d9..9fc11d59700 100644
--- a/web/client/plugins/Widgets.jsx
+++ b/web/client/plugins/Widgets.jsx
@@ -43,6 +43,7 @@ const RIGHT_MARGIN = 55;
import WidgetsViewBase from '../components/widgets/view/WidgetsView';
import {mapLayoutValuesSelector} from "../selectors/maplayout";
+import { autorefreshTicksSelector } from './AutoRefresh/selectors/autorefresh';
const WidgetsView =
compose(
@@ -57,7 +58,8 @@ compose(
state => state.browser && state.browser.mobile,
getFloatingWidgets,
getTblWidgetZoomLoader,
- (id, widgets, layouts, maximized, dependencies, mapLayout, isMobileAgent, dropdownWidgets, recordZoomLoading) => ({
+ autorefreshTicksSelector,
+ (id, widgets, layouts, maximized, dependencies, mapLayout, isMobileAgent, dropdownWidgets, recordZoomLoading, autorefreshTicks) => ({
id,
widgets,
layouts,
@@ -66,7 +68,8 @@ compose(
mapLayout,
isMobileAgent,
dropdownWidgets,
- recordZoomLoading
+ recordZoomLoading,
+ autorefreshTicks
})
), {
editWidget,
diff --git a/web/client/plugins/map/index.js b/web/client/plugins/map/index.js
index 34bc61a911b..5813cc5fa11 100644
--- a/web/client/plugins/map/index.js
+++ b/web/client/plugins/map/index.js
@@ -35,6 +35,7 @@ import {
snappingLayerSelector
} from "../../selectors/draw";
import { mapPopupsSelector } from '../../selectors/mapPopups';
+import { autorefreshTicksSelector } from '../AutoRefresh/selectors/autorefresh';
const Empty = () => { return ; };
@@ -99,7 +100,9 @@ const pluginsCreator = (mapType, actions) => {
changeSelectionState
})(components.SelectionSupport || Empty);
- const LLayer = connect(null, {onWarning: warning})( components.Layer || Empty);
+ const LLayer = connect((state) => ({
+ autorefreshTicks: autorefreshTicksSelector(state)
+ }), {onWarning: warning})( components.Layer || Empty);
const PopupSupport = connect(
createSelector(
diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js
index 2dda3344e89..9ba36ec8cdc 100644
--- a/web/client/product/plugins.js
+++ b/web/client/product/plugins.js
@@ -150,7 +150,8 @@ export const plugins = {
ZoomOutPlugin: toModulePlugin('ZoomOut', () => import(/* webpackChunkName: 'plugins/zoomOut' */ '../plugins/ZoomOut')),
AddWidgetDashboardPlugin: toModulePlugin('AddWidgetDashboard', () => import(/* webpackChunkName: 'plugins/AddWidgetDashboard' */ '../plugins/AddWidgetDashboard')),
MapConnectionDashboardPlugin: toModulePlugin('MapConnectionDashboard', () => import(/* webpackChunkName: 'plugins/MapConnectionDashboard' */ '../plugins/MapConnectionDashboard')),
- IPManagerPlugin: toModulePlugin('IPManager', () => import(/* webpackChunkName: 'plugins/IPManager' */ '../plugins/ResourcesCatalog/IPManager'))
+ IPManagerPlugin: toModulePlugin('IPManager', () => import(/* webpackChunkName: 'plugins/IPManager' */ '../plugins/ResourcesCatalog/IPManager')),
+ AutoRefreshPlugin: toModulePlugin('AutoRefresh', () => import(/* webpackChunkName: 'plugins/AutoRefresh' */ '../plugins/AutoRefresh'))
};
const pluginsDefinition = {
diff --git a/web/client/themes/default/less/autorefresh.less b/web/client/themes/default/less/autorefresh.less
new file mode 100644
index 00000000000..02c45322256
--- /dev/null
+++ b/web/client/themes/default/less/autorefresh.less
@@ -0,0 +1,176 @@
+// **************
+// Theme
+// **************
+#ms-components-theme(@theme-vars) {
+ .ms-autorefresh-divider {
+ .border-color-var(@theme-vars[main-border-color]);
+ }
+}
+
+// **************
+// Layout
+// **************
+.ms-autorefresh-wrapper {
+ display: flex;
+ gap: 0.5ch;
+ align-items: center;
+ height: 24px;
+ border-width: 1px;
+ border-style: solid;
+ padding: 2px;
+ border-radius: 4px;
+ border-color: var(--ms-main-border-color);
+ background-color: var(--ms-main-bg);
+}
+
+.ms-autorefresh-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 20px;
+ width: 20px;
+ background-color: transparent;
+ border: none;
+ box-shadow: none;
+ text-decoration: none;
+ color: var(--ms-main-color);
+
+ &:hover,
+ &:focus,
+ &:active,
+ &:active:focus {
+ background-color: var(--ms-main-hover-bg);
+ box-shadow: none;
+ outline: none;
+ border: none;
+ color: inherit;
+ text-decoration: none;
+ }
+}
+
+.ms-autorefresh-informations {
+ display: flex;
+ align-items: center;
+}
+
+.ms-autorefresh-form-container{
+ min-width: 300px;
+ overflow: hidden;
+
+ .ms-autorefresh-form-group {
+ display: grid;
+ align-items: center;
+ column-gap: 0.5ch;
+ grid-template-areas:
+ "button title input"
+ "button summary input";
+ grid-template-columns: auto 1fr 100px;
+
+ &.ms-autorefresh-form-group-hidden {
+ .ms-autorefresh-form-group__title,
+ .ms-autorefresh-form-group__input,
+ .ms-autorefresh-form-group__summary {
+ opacity: 0.6;
+ }
+ }
+
+ &.ms-autorefresh-form-group-inactive {
+ .ms-autorefresh-form-group__title {
+ grid-row-end: summary;
+ }
+ }
+
+ .ms-autorefresh-form-group__button {
+ grid-area: button;
+ }
+ .ms-autorefresh-form-group__title {
+ display: flex;
+ align-items: center;
+ grid-area: title;
+ font-size: 12px;
+
+ .glyphicon-eye-close {
+ margin-right: 0.5ch;
+ }
+ }
+
+ .ms-autorefresh-form-group__input {
+ grid-area: input;
+ }
+ .ms-autorefresh-form-group__summary {
+ grid-area: summary;
+ font-size: 10px;
+ }
+ }
+
+ > form {
+ display: flex;
+ flex-direction: column;
+ gap: 1ch;
+ }
+}
+
+.ms-autorefresh-layer-title {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding: 0;
+ margin: 0;
+
+ span {
+ vertical-align: sub;
+ }
+}
+
+.ms-autorefresh-layers-summary {
+ display: grid;
+ flex-direction: column;
+ gap: 0.5ch;
+ max-height: 50vh;
+ overflow-y: auto;
+
+ .ms-autorefresh-layer-summary__row{
+ display: grid;
+ grid-template-areas:
+ "title interval"
+ "summary interval";
+ grid-template-columns: 1fr 70px;
+ column-gap: 1ch;
+ align-items: center;
+
+ .glyphicon {
+ margin-right: 0;
+ }
+
+ em {
+ font-size: 10px;
+ grid-area: summary;
+ }
+
+ .ms-autorefresh-layer-summary__row__title {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ grid-area: title;
+ }
+
+ .ms-autorefresh-layer-summary__row__interval {
+ grid-area: interval;
+ }
+ }
+
+ .ms-autorefresh-layer-summary__row-hidden {
+ opacity: 0.6;
+ }
+
+ .ms-autorefresh-layer-summary__row-inactive {
+ .ms-autorefresh-layer-summary__row__title {
+ grid-row-end: summary;
+ }
+ }
+}
+
+#ms-autorefresh-selector {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
diff --git a/web/client/themes/default/less/mapstore.less b/web/client/themes/default/less/mapstore.less
index ca6cb97062a..4f46962fc2a 100644
--- a/web/client/themes/default/less/mapstore.less
+++ b/web/client/themes/default/less/mapstore.less
@@ -11,6 +11,7 @@
@import "ag-grid.less";
@import "autocomplete.less";
+@import "autorefresh.less";
@import "cookie.less";
@import "code-editor.less";
@import "colorrangeselector.less";
diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json
index 73f3d64d61c..0169614df80 100644
--- a/web/client/translations/data.en-US.json
+++ b/web/client/translations/data.en-US.json
@@ -49,6 +49,17 @@
"descriptionAdmin": "Please contact the administrator"
},
"autorefresh": {
+ "selector":"Set layers auto refresh interval",
+ "label": {
+ "default":"Activate AutoRefresh" ,
+ "lastUpdated": "Last Refresh",
+ "addLayer": "Add layer",
+ "addLayerPlaceholder": "Select...",
+ "removeLayer": "Remove layer",
+ "layersSummary": "Layers automatically refreshed",
+ "noLayers":"None found",
+ "informations": "Which layers are configured"
+ },
"of": "of",
"updating": "Updating...",
"layers": "layers"
@@ -3912,6 +3923,10 @@
"Isochrone": {
"description": "This plugin allows to calculate isochrones and isodistances",
"title": "Isochrone"
+ },
+ "AutoRefresh": {
+ "description": "This plugin allows to set an auto refresh time interval on layers",
+ "title": "Auto Refresh"
}
},
"contextCreator": {
diff --git a/web/client/utils/LayersUtils.js b/web/client/utils/LayersUtils.js
index 3d7adee06cb..de99bd49a0c 100644
--- a/web/client/utils/LayersUtils.js
+++ b/web/client/utils/LayersUtils.js
@@ -109,7 +109,6 @@ const isSupportedLayerFunc = (layer, maptype) => {
return Layers.isSupported(layer.type) && !layer.invalid;
};
-
const checkInvalidParam = (layer) => {
return layer && layer.invalid ? Object.assign({}, layer, {invalid: false}) : layer;
};
@@ -739,7 +738,8 @@ export const saveLayer = (layer) => {
legendOptions: layer.legendOptions,
tileSize: layer.tileSize,
version: layer.version,
- expanded: layer.expanded || false
+ expanded: layer.expanded || false,
+ autorefreshInterval: layer.autorefreshInterval
},
layer?.enableInteractiveLegend !== undefined ? { enableInteractiveLegend: layer?.enableInteractiveLegend } : {},
layer?.enableDynamicLegend !== undefined ? { enableDynamicLegend: layer?.enableDynamicLegend } : {},
@@ -1227,6 +1227,19 @@ export const isBackgroundCompatibleWithProjection = (background, projection) =>
return valid;
};
+/**
+ * Checks if a layer has auto-refresh capability based on its type and the map type.
+ * @param {string} layerType - The type of the layer
+ * @param {string} maptype - The type of the map
+ * @returns {boolean} True if the layer has auto-refresh capability
+ */
+export const hasAutoRefreshCapability = (layerType, maptype) => {
+ const LayersUtil = require('./' + maptype + '/Layers');
+ const Layers = LayersUtil.default || LayersUtil;
+
+ return Layers.hasAutoRefreshCapability(layerType);
+};
+
LayersUtils = {
getGroupByName,
getLayerId,
diff --git a/web/client/utils/cesium/Layers.js b/web/client/utils/cesium/Layers.js
index 4a1a7865ead..55244567584 100644
--- a/web/client/utils/cesium/Layers.js
+++ b/web/client/utils/cesium/Layers.js
@@ -41,6 +41,34 @@ const Layers = {
},
isSupported(type) {
return !!layerTypes[type];
+ },
+ /**
+ * Call the refresh method of the layer implementation located in web/client/components/map/cesium/plugins/[layerType]
+ * where layerType = WMSLayer, WFSLayer, WMTSLayer, ArcGISLayer, etc
+ * if implemented, that is used for autorefresh of layers.
+ * @param {string} type
+ * @param {object} layer
+ * @returns {void}
+ */
+ refreshLayer(type, layer) {
+ var layerCreator = layerTypes[type];
+ if (layerCreator && layerCreator.refresh) {
+ layerCreator.refresh(layer);
+ }
+ },
+ /**
+ * Check if the layer implementation located in web/client/components/map/cesium/plugins/[layerType]
+ * where layerType = WMSLayer, WFSLayer, WMTSLayer, ArcGISLayer, etc
+ * implements the refresh method, that is used for autorefresh of layers.
+ * @param {string} type
+ * @returns {boolean}
+ */
+ hasAutoRefreshCapability(type) {
+ const layerCreator = layerTypes[type];
+ if (layerCreator && layerCreator.refresh) {
+ return true;
+ }
+ return false;
}
};
diff --git a/web/client/utils/leaflet/Layers.js b/web/client/utils/leaflet/Layers.js
index eebf15ed120..d689f5d07eb 100644
--- a/web/client/utils/leaflet/Layers.js
+++ b/web/client/utils/leaflet/Layers.js
@@ -46,6 +46,34 @@ var Layers = {
}
return true;
},
+ /**
+ * Call the refresh method of the layer implementation located in web/client/components/map/leaflet/plugins/[layerType]
+ * where layerType = WMSLayer, WFSLayer, WMTSLayer, ArcGISLayer, etc
+ * if implemented, that is used for autorefresh of layers.
+ * @param {string} type
+ * @param {object} layer
+ * @returns {void}
+ */
+ refreshLayer(type, layer) {
+ var layerCreator = layerTypes[type];
+ if (layerCreator && layerCreator.refresh) {
+ layerCreator.refresh(layer);
+ }
+ },
+ /**
+ * Check if the layer implementation located in web/client/components/map/leaflet/plugins/[layerType]
+ * where layerType = WMSLayer, WFSLayer, WMTSLayer, ArcGISLayer, etc
+ * implements the refresh method, that is used for autorefresh of layers.
+ * @param {string} type
+ * @returns {boolean}
+ */
+ hasAutoRefreshCapability(type) {
+ const layerCreator = layerTypes[type];
+ if (layerCreator && layerCreator.refresh) {
+ return true;
+ }
+ return false;
+ },
isSupported(type) {
return !!layerTypes[type];
}
diff --git a/web/client/utils/openlayers/Layers.js b/web/client/utils/openlayers/Layers.js
index e12891b238f..47fa5fa2959 100644
--- a/web/client/utils/openlayers/Layers.js
+++ b/web/client/utils/openlayers/Layers.js
@@ -82,14 +82,46 @@ export const isCompatible = function(type, options) {
return true;
};
+/**
+ * Call the refresh method of the layer implementation located in web/client/components/map/openlayers/plugins/[layerType]
+ * where layerType = WMSLayer, WFSLayer, WMTSLayer, ArcGISLayer, etc
+ * if implemented, that is used for autorefresh of layers.
+ * @param {string} type
+ * @param {object} layer
+ * @returns {void}
+ */
+export const refreshLayer = function(type, layer) {
+ var layerCreator = layerTypes[type];
+ if (layerCreator && layerCreator.refresh) {
+ layerCreator.refresh(layer);
+ }
+};
+
+/**
+ * Check if the layer implementation located in web/client/components/map/openlayers/plugins/[layerType]
+ * where layerType = WMSLayer, WFSLayer, WMTSLayer, ArcGISLayer, etc
+ * implements the refresh method, that is used for autorefresh of layers.
+ * @param {string} type
+ * @returns {boolean}
+ */
+export const hasAutoRefreshCapability = function(type) {
+ const layerCreator = layerTypes[type];
+ if (layerCreator && layerCreator.refresh) {
+ return true;
+ }
+ return false;
+};
+
export default {
registerType,
createLayer,
+ refreshLayer,
updateLayer,
removeLayer,
renderLayer,
isValid,
isSupported,
- isCompatible
+ isCompatible,
+ hasAutoRefreshCapability
};