@eeacms/volto-arcgis-block 0.1.452 → 0.1.454

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [0.1.454](https://github.com/eea/volto-arcgis-block/compare/0.1.453...0.1.454) - 8 June 2026
8
+
9
+ #### :hammer_and_wrench: Others
10
+
11
+ - (feat): For 10m CDSE datasets, remove the message from the TOC menu once the correct zoom level has been reached [Unai Bolivar - [`f5a2f19`](https://github.com/eea/volto-arcgis-block/commit/f5a2f19e2ca8f8311bfcd69bbf90d14c168f0f9b)]
12
+ ### [0.1.453](https://github.com/eea/volto-arcgis-block/compare/0.1.452...0.1.453) - 3 June 2026
13
+
14
+ #### :house: Internal changes
15
+
16
+ - style: Automated code fix [eea-jenkins - [`7e20e98`](https://github.com/eea/volto-arcgis-block/commit/7e20e98f5f5e497b79557453c464dee98753ea6e)]
17
+ - chore: upgrade Makefile to latest [valentinab25 - [`fb2e387`](https://github.com/eea/volto-arcgis-block/commit/fb2e387f9ec8cfac7be5894d775313dddaf80897)]
18
+
19
+ #### :hammer_and_wrench: Others
20
+
21
+ - (task): add stylelint dev dependency [Unai Bolivar - [`d11c354`](https://github.com/eea/volto-arcgis-block/commit/d11c3540d3eb4bfc064768cb70d9d1809c60ec3a)]
22
+ - (bug): fix isuess found by jenkins [Unai Bolivar - [`74638f9`](https://github.com/eea/volto-arcgis-block/commit/74638f9047b7868a7a64a7b3ca3bbdfa1ff8a8cd)]
23
+ - (bug): fix isuess found by jenkins [Unai Bolivar - [`98c1258`](https://github.com/eea/volto-arcgis-block/commit/98c125831bd9d0290087c244777c7b6764d406f0)]
24
+ - (task): update volto version in jenkinsfile [Unai Bolivar - [`f0c13e8`](https://github.com/eea/volto-arcgis-block/commit/f0c13e84a71063f4181d5cc59406132db7b29793)]
25
+ - (bug): Error handling when uploading external services [Unai Bolivar - [`f8f873d`](https://github.com/eea/volto-arcgis-block/commit/f8f873d53064def279101a711df99da09a4dd6b6)]
26
+ - (bug): In the legend widget, retrieve the legends from CDSE services [Unai Bolivar - [`b909191`](https://github.com/eea/volto-arcgis-block/commit/b9091910c676568f2070b3b689972d169d8a2880)]
7
27
  ### [0.1.452](https://github.com/eea/volto-arcgis-block/compare/0.1.451...0.1.452) - 28 May 2026
8
28
 
9
29
  ### [0.1.451](https://github.com/eea/volto-arcgis-block/compare/0.1.450...0.1.451) - 21 May 2026
package/Jenkinsfile CHANGED
@@ -10,7 +10,7 @@ pipeline {
10
10
  DEPENDENCIES = ""
11
11
  BACKEND_PROFILES = "eea.kitkat:testing"
12
12
  BACKEND_ADDONS = "clms.addon,clms.types,clms.downloadtool,clms.statstool"
13
- VOLTO = "16.31.1"
13
+ VOLTO = "17"
14
14
  IMAGE_NAME = BUILD_TAG.toLowerCase().replaceAll('[^a-z0-9]', '-')
15
15
  NODEJS_VERSION = "22"
16
16
  }
package/Makefile CHANGED
@@ -46,7 +46,7 @@ endif
46
46
  DIR=$(shell basename $$(pwd))
47
47
  NODE_MODULES?="../../../node_modules"
48
48
  PLONE_VERSION?=6
49
- VOLTO_VERSION?=16.31.1
49
+ VOLTO_VERSION?=17
50
50
  ADDON_PATH="${DIR}"
51
51
  ADDON_NAME="@eeacms/${ADDON_PATH}"
52
52
  DOCKER_COMPOSE=PLONE_VERSION=${PLONE_VERSION} VOLTO_VERSION=${VOLTO_VERSION} ADDON_NAME=${ADDON_NAME} ADDON_PATH=${ADDON_PATH} docker compose
@@ -86,19 +86,19 @@ cypress-open: ## Open cypress integration tests
86
86
 
87
87
  .PHONY: cypress-run
88
88
  cypress-run: ## Run cypress integration tests
89
- CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress run --browser chrome
89
+ CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress run
90
90
 
91
91
  .PHONY: test
92
92
  test: ## Run jest tests
93
- ${DOCKER_COMPOSE} run -e CI=1 frontend test
93
+ ${DOCKER_COMPOSE} run --no-deps -e CI=1 frontend test
94
94
 
95
95
  .PHONY: test-update
96
96
  test-update: ## Update jest tests snapshots
97
- ${DOCKER_COMPOSE} run -e CI=1 frontend test -u
97
+ ${DOCKER_COMPOSE} run --no-deps -e CI=1 frontend test -u
98
98
 
99
99
  .PHONY: stylelint
100
100
  stylelint: ## Stylelint
101
- $(NODE_MODULES)/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}'
101
+ $(NODE_MODULES)/.bin/stylelint --allow-empty-input 'src/**/*.{css,less}'
102
102
 
103
103
  .PHONY: stylelint-overrides
104
104
  stylelint-overrides:
@@ -106,7 +106,7 @@ stylelint-overrides:
106
106
 
107
107
  .PHONY: stylelint-fix
108
108
  stylelint-fix: ## Fix stylelint
109
- $(NODE_MODULES)/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}' --fix
109
+ $(NODE_MODULES)/.bin/stylelint --allow-empty-input 'src/**/*.{css,less}' --fix
110
110
  $(NODE_MODULES)/.bin/stylelint --custom-syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides' --fix
111
111
 
112
112
  .PHONY: prettier
@@ -119,11 +119,11 @@ prettier-fix: ## Fix prettier
119
119
 
120
120
  .PHONY: lint
121
121
  lint: ## ES Lint
122
- $(NODE_MODULES)/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx}'
122
+ $(NODE_MODULES)/.bin/eslint --max-warnings=0 'src/**/*.{js,jsx}'
123
123
 
124
124
  .PHONY: lint-fix
125
125
  lint-fix: ## Fix ES Lint
126
- $(NODE_MODULES)/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx}'
126
+ $(NODE_MODULES)/.bin/eslint --fix 'src/**/*.{js,jsx}'
127
127
 
128
128
  .PHONY: i18n
129
129
  i18n: ## i18n
@@ -155,7 +155,11 @@ start-ci:
155
155
  cd ../..
156
156
  yarn start
157
157
 
158
+ .PHONY: check-ci
159
+ check-ci:
160
+ $(NODE_MODULES)/.bin/wait-on -t 240000 http://localhost:3000
161
+
158
162
  .PHONY: cypress-ci
159
163
  cypress-ci:
160
164
  $(NODE_MODULES)/.bin/wait-on -t 240000 http://localhost:3000
161
- NODE_ENV=development make cypress-run
165
+ CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress run --browser chromium
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-arcgis-block",
3
- "version": "0.1.452",
3
+ "version": "0.1.454",
4
4
  "description": "volto-arcgis-block: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: CodeSyntax",
@@ -75,6 +75,7 @@
75
75
  "dotenv": "^16.3.2",
76
76
  "husky": "^8.0.3",
77
77
  "lint-staged": "13.1.4",
78
- "md5": "^2.3.0"
78
+ "md5": "^2.3.0",
79
+ "stylelint": "^13.12.0"
79
80
  }
80
81
  }
@@ -924,8 +924,9 @@ class AreaWidget extends React.Component {
924
924
  }
925
925
 
926
926
  checkExtent(extent) {
927
- const areaLimit = this.mapviewer_config.Components[0].Products[0]
928
- .Datasets[0].DownloadLimitAreaExtent;
927
+ const areaLimit =
928
+ this.mapviewer_config.Components[0].Products[0].Datasets[0]
929
+ .DownloadLimitAreaExtent;
929
930
  if (
930
931
  extent.width * extent.height > areaLimit ||
931
932
  extent.width * extent.height === 0
@@ -169,21 +169,23 @@ class BasemapWidget extends React.Component {
169
169
  }
170
170
  url = this.parseCapabilities(layers[i], 'ResourceURL')[0].attributes
171
171
  .template.textContent;
172
- let basemapCode = `
173
- let basemap${i} = new Basemap({
174
- title: this.parseCapabilities(layers[${i}], 'ows:title')[0].innerText,
175
- thumbnailUrl: ${url}.replace('{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}', '/${proj}/0/0/0'),
176
- baseLayers: [
177
- new WebTileLayer({
178
- urlTemplate: ${url}.replace('{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}', '/${proj}/{z}/{x}/{y}'),
179
- copyright: '© OpenStreetMap (and) contributors, CC-BY-SA',
180
- })
181
- ]
182
- });
183
- this.layerArray.push(basemap${i});
184
- `;
185
-
186
- Function.apply(null, [basemapCode]).call(this);
172
+ const basemap = new Basemap({
173
+ title: this.parseCapabilities(layers[i], 'ows:title')[0].innerText,
174
+ thumbnailUrl: url.replace(
175
+ '{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}',
176
+ `/${proj}/0/0/0`,
177
+ ),
178
+ baseLayers: [
179
+ new WebTileLayer({
180
+ urlTemplate: url.replace(
181
+ '{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}',
182
+ `/${proj}/{z}/{x}/{y}`,
183
+ ),
184
+ copyright: '© OpenStreetMap (and) contributors, CC-BY-SA',
185
+ }),
186
+ ],
187
+ });
188
+ this.layerArray.push(basemap);
187
189
  }
188
190
  this.basemapGallery = new BasemapGallery({
189
191
  view: this.props.view,
@@ -433,11 +433,10 @@ class BookmarkWidget extends React.Component {
433
433
  ) {
434
434
  Object.keys(this.props.hotspotData.filteredLayers).forEach(
435
435
  (key) => {
436
- hotspotFilters.filteredLayers[
437
- key
438
- ] = this.props.hotspotData.filteredLayers[
439
- key
440
- ].customLayerParameters['CQL_FILTER'];
436
+ hotspotFilters.filteredLayers[key] =
437
+ this.props.hotspotData.filteredLayers[
438
+ key
439
+ ].customLayerParameters['CQL_FILTER'];
441
440
  },
442
441
  );
443
442
  }
@@ -619,11 +618,10 @@ class BookmarkWidget extends React.Component {
619
618
  ) {
620
619
  Object.keys(this.props.hotspotData.filteredLayers).forEach(
621
620
  (key) => {
622
- hotspotFilters.filteredLayers[
623
- key
624
- ] = this.props.hotspotData.filteredLayers[
625
- key
626
- ].customLayerParameters['CQL_FILTER'];
621
+ hotspotFilters.filteredLayers[key] =
622
+ this.props.hotspotData.filteredLayers[
623
+ key
624
+ ].customLayerParameters['CQL_FILTER'];
627
625
  },
628
626
  );
629
627
  }
@@ -44,9 +44,8 @@ class HotspotWidget extends React.Component {
44
44
  this.layerModelInit = this.layerModelInit.bind(this);
45
45
  this.getBBoxData = this.getBBoxData.bind(this);
46
46
  this.handleApplyFilter = this.handleApplyFilter.bind(this);
47
- this.filteredLayersToHotspotData = this.filteredLayersToHotspotData.bind(
48
- this,
49
- );
47
+ this.filteredLayersToHotspotData =
48
+ this.filteredLayersToHotspotData.bind(this);
50
49
  this.mapCfg = this.props.mapCfg;
51
50
  this.selectedArea = null;
52
51
  this.lcYear = null;
@@ -268,8 +267,8 @@ class HotspotWidget extends React.Component {
268
267
  let filterLayer;
269
268
 
270
269
  if (type === 'lcc') {
271
- let selectLccBoxTime = document.getElementById('select-klc-lccTime')
272
- .value;
270
+ let selectLccBoxTime =
271
+ document.getElementById('select-klc-lccTime').value;
273
272
  //this.lccYear = selectLccBoxTime;
274
273
  this.setState({ lccYear: selectLccBoxTime });
275
274
  if (document.getElementById('select-klc-lccTime').value.match(/\d+/g)) {
@@ -300,9 +299,8 @@ class HotspotWidget extends React.Component {
300
299
  filterLayer = this.esriLayer_lcc;
301
300
 
302
301
  filterLayer.sublayers.items[0].name = this.addLegendName(typeLegend);
303
- filterLayer.sublayers.items[0].legendUrl = this.addLegendNameToUrl(
304
- typeLegend,
305
- );
302
+ filterLayer.sublayers.items[0].legendUrl =
303
+ this.addLegendNameToUrl(typeLegend);
306
304
  filterLayer.sublayers.items[0].title = title;
307
305
  if (
308
306
  bookmarkHotspotFilter !== null &&
@@ -334,8 +332,8 @@ class HotspotWidget extends React.Component {
334
332
  }
335
333
  }
336
334
 
337
- let selectLcBoxTime = document.getElementById('select-klc-lcTime')
338
- .value;
335
+ let selectLcBoxTime =
336
+ document.getElementById('select-klc-lcTime').value;
339
337
 
340
338
  this.addFilteredLayersData(
341
339
  filteredLayersData,
@@ -356,9 +354,8 @@ class HotspotWidget extends React.Component {
356
354
  filterLayer = this.esriLayer_lc;
357
355
 
358
356
  filterLayer.sublayers.items[0].name = this.addLegendName(typeLegend);
359
- filterLayer.sublayers.items[0].legendUrl = this.addLegendNameToUrl(
360
- typeLegend,
361
- );
357
+ filterLayer.sublayers.items[0].legendUrl =
358
+ this.addLegendNameToUrl(typeLegend);
362
359
  filterLayer.sublayers.items[0].title = title;
363
360
  if (
364
361
  bookmarkHotspotFilter !== null &&
@@ -389,12 +389,10 @@ class MapViewer extends React.Component {
389
389
  this.viewUiOperationState = null;
390
390
  this.shouldClearSessionOnUnmount = true;
391
391
  this.uploadErrorTimeoutTask = null;
392
- this.scheduleViewModeButtonLoad = this.scheduleViewModeButtonLoad.bind(
393
- this,
394
- );
395
- this.processPendingWidgetActivation = this.processPendingWidgetActivation.bind(
396
- this,
397
- );
392
+ this.scheduleViewModeButtonLoad =
393
+ this.scheduleViewModeButtonLoad.bind(this);
394
+ this.processPendingWidgetActivation =
395
+ this.processPendingWidgetActivation.bind(this);
398
396
  }
399
397
 
400
398
  processPendingWidgetActivation() {
@@ -1051,9 +1049,8 @@ class MapViewer extends React.Component {
1051
1049
  this.isComponentMounted &&
1052
1050
  transitionTaskId === this.viewTransitionTaskId
1053
1051
  ) {
1054
- const fallbackViewState = this.buildFallbackViewState(
1055
- normalizedNextMode,
1056
- );
1052
+ const fallbackViewState =
1053
+ this.buildFallbackViewState(normalizedNextMode);
1057
1054
  isViewCreated = await this.createView(
1058
1055
  normalizedNextMode,
1059
1056
  fallbackViewState,
@@ -1158,10 +1155,8 @@ class MapViewer extends React.Component {
1158
1155
  // Method to handle session state updates when user authentication changes
1159
1156
  handleSessionStateUpdate(newUserState, previousUserState) {
1160
1157
  const { user_id: newUserId, isLoggedIn: newIsLoggedIn } = newUserState;
1161
- const {
1162
- user_id: prevUserId,
1163
- isLoggedIn: prevIsLoggedIn,
1164
- } = previousUserState;
1158
+ const { user_id: prevUserId, isLoggedIn: prevIsLoggedIn } =
1159
+ previousUserState;
1165
1160
 
1166
1161
  // Handle login/logout transitions
1167
1162
  if (prevIsLoggedIn !== newIsLoggedIn) {
@@ -1293,9 +1288,8 @@ class MapViewer extends React.Component {
1293
1288
 
1294
1289
  activeLayersHandler(newActiveLayers) {
1295
1290
  try {
1296
- const layersWithoutCircularReferences = this.removeCircularReferences(
1297
- newActiveLayers,
1298
- );
1291
+ const layersWithoutCircularReferences =
1292
+ this.removeCircularReferences(newActiveLayers);
1299
1293
  this.activeLayers = layersWithoutCircularReferences;
1300
1294
  mapStatus.activeLayers = layersWithoutCircularReferences;
1301
1295
  sessionStorage.setItem('mapStatus', JSON.stringify(mapStatus));
@@ -1523,10 +1517,8 @@ class MapViewer extends React.Component {
1523
1517
  }
1524
1518
 
1525
1519
  // Handle user state changes from context
1526
- const {
1527
- user_id: currentUserId,
1528
- isLoggedIn: currentIsLoggedIn,
1529
- } = this.context;
1520
+ const { user_id: currentUserId, isLoggedIn: currentIsLoggedIn } =
1521
+ this.context;
1530
1522
  const { currentUserState: prevUserState } = prevState;
1531
1523
 
1532
1524
  // Compare current context values with previous state
@@ -186,9 +186,8 @@ export const AddCartItem = ({
186
186
  });
187
187
  }
188
188
  if ((check === 'area' || fileUpload) && isMapServer) {
189
- const transformedLayerExtent = WebMercatorUtils.webMercatorToGeographic(
190
- layerExtent,
191
- );
189
+ const transformedLayerExtent =
190
+ WebMercatorUtils.webMercatorToGeographic(layerExtent);
192
191
  if (transformedLayerExtent.intersects(areaExtent)) {
193
192
  intersection = true;
194
193
  }
@@ -538,6 +537,24 @@ class MenuWidget extends React.Component {
538
537
  });
539
538
  }
540
539
  }
540
+ let zoomInMessageContainers = document.getElementsByClassName(
541
+ 'zoom-in-message-container',
542
+ );
543
+ let zoomInMessageContainersList = [...zoomInMessageContainers];
544
+ zoomInMessageContainersList.forEach((container) => {
545
+ if (container && container !== null) {
546
+ let nodes = [
547
+ ...document.getElementsByClassName('zoom-in-message-dataset'),
548
+ ];
549
+ nodes.forEach((node) => {
550
+ if (node && node !== null) {
551
+ if (node.innerText === 'Zoom in') {
552
+ node.style.display = zoom > 6 ? 'none' : 'block';
553
+ }
554
+ }
555
+ });
556
+ }
557
+ });
541
558
  if (!this.visibleLayers) this.visibleLayers = {};
542
559
  this.handleRasterVectorLegend();
543
560
  this.setState({});
@@ -2017,6 +2034,14 @@ class MenuWidget extends React.Component {
2017
2034
  //For Legend request
2018
2035
  const legendRequest =
2019
2036
  'request=GetLegendGraphic&version=1.0.0&format=image/png&layer=';
2037
+ const isCdseService =
2038
+ !!viewService &&
2039
+ ['/ogc/', '/cdse/'].some((segment) =>
2040
+ viewService.toLowerCase().includes(segment),
2041
+ );
2042
+ const cdseLegendUrl = isCdseService
2043
+ ? this.buildCdseLegendUrl(viewService, layer.Title || layer.LayerId)
2044
+ : null;
2020
2045
  //For each layer
2021
2046
  let inheritedIndexLayer = inheritedIndex + '_' + layerIndex;
2022
2047
  let style = this.props.download ? { paddingLeft: '4rem' } : {};
@@ -2025,16 +2050,15 @@ class MenuWidget extends React.Component {
2025
2050
  !this.layers.hasOwnProperty(layer.LayerId + '_' + inheritedIndexLayer)
2026
2051
  ) {
2027
2052
  if (viewService?.toLowerCase().endsWith('mapserver')) {
2028
- this.layers[
2029
- layer.LayerId + '_' + inheritedIndexLayer
2030
- ] = new MapImageLayer({
2031
- url: viewService,
2032
- title: layer.Title,
2033
- DatasetId: DatasetId,
2034
- DatasetTitle: DatasetTitle,
2035
- ProductId: ProductId,
2036
- LayerTitle: layer.Title,
2037
- });
2053
+ this.layers[layer.LayerId + '_' + inheritedIndexLayer] =
2054
+ new MapImageLayer({
2055
+ url: viewService,
2056
+ title: layer.Title,
2057
+ DatasetId: DatasetId,
2058
+ DatasetTitle: DatasetTitle,
2059
+ ProductId: ProductId,
2060
+ LayerTitle: layer.Title,
2061
+ });
2038
2062
  //iterate sublayers fetching all sublayer data
2039
2063
  } else if (viewService?.toLowerCase().includes('wms')) {
2040
2064
  viewService = viewService?.includes('?')
@@ -2056,6 +2080,8 @@ class MenuWidget extends React.Component {
2056
2080
  legendEnabled: true,
2057
2081
  legendUrl: layer.StaticImageLegend
2058
2082
  ? layer.StaticImageLegend
2083
+ : cdseLegendUrl
2084
+ ? cdseLegendUrl
2059
2085
  : viewService + legendRequest + layer.LayerId,
2060
2086
  featureInfoUrl: featureInfoUrl,
2061
2087
  },
@@ -2068,9 +2094,10 @@ class MenuWidget extends React.Component {
2068
2094
  ViewService: viewService,
2069
2095
  });
2070
2096
  } else if (viewService?.toLowerCase().includes('wmts')) {
2071
- const resolveSentinelLayer = /(?:sh\.dataspace\.copernicus\.eu|services\.sentinel-hub\.com)\/ogc\/wmts/i.test(
2072
- viewService || '',
2073
- );
2097
+ const resolveSentinelLayer =
2098
+ /(?:sh\.dataspace\.copernicus\.eu|services\.sentinel-hub\.com)\/ogc\/wmts/i.test(
2099
+ viewService || '',
2100
+ );
2074
2101
  this.layers[layer.LayerId + '_' + inheritedIndexLayer] = new WMTSLayer({
2075
2102
  url: viewService?.includes('?')
2076
2103
  ? viewService + '&'
@@ -2091,7 +2118,7 @@ class MenuWidget extends React.Component {
2091
2118
  DatasetTitle: DatasetTitle,
2092
2119
  ProductId: ProductId,
2093
2120
  ViewService: viewService,
2094
- StaticImageLegend: layer.StaticImageLegend,
2121
+ StaticImageLegend: layer.StaticImageLegend || cdseLegendUrl,
2095
2122
  LayerTitle: layer.Title,
2096
2123
  DatasetDownloadInformation: dataset_download_information || {},
2097
2124
  customLayerParameters: {
@@ -2100,24 +2127,23 @@ class MenuWidget extends React.Component {
2100
2127
  },
2101
2128
  });
2102
2129
  } else {
2103
- this.layers[
2104
- layer.LayerId + '_' + inheritedIndexLayer
2105
- ] = new FeatureLayer({
2106
- url:
2107
- viewService +
2108
- (viewService?.endsWith('/') ? '' : '/') +
2109
- layer.LayerId,
2110
- id: layer.LayerId,
2111
- title: layer.Title,
2112
- featureInfoUrl: featureInfoUrl,
2113
- popupEnabled: true,
2114
- isTimeSeries: isTimeSeries,
2115
- fields: layer.Fields,
2116
- DatasetId: DatasetId,
2117
- DatasetTitle: DatasetTitle,
2118
- ProductId: ProductId,
2119
- ViewService: viewService,
2120
- });
2130
+ this.layers[layer.LayerId + '_' + inheritedIndexLayer] =
2131
+ new FeatureLayer({
2132
+ url:
2133
+ viewService +
2134
+ (viewService?.endsWith('/') ? '' : '/') +
2135
+ layer.LayerId,
2136
+ id: layer.LayerId,
2137
+ title: layer.Title,
2138
+ featureInfoUrl: featureInfoUrl,
2139
+ popupEnabled: true,
2140
+ isTimeSeries: isTimeSeries,
2141
+ fields: layer.Fields,
2142
+ DatasetId: DatasetId,
2143
+ DatasetTitle: DatasetTitle,
2144
+ ProductId: ProductId,
2145
+ ViewService: viewService,
2146
+ });
2121
2147
  }
2122
2148
  }
2123
2149
  // const isCDSE = !!this.url && this.url.toLowerCase().includes('/ogc/');
@@ -2312,6 +2338,26 @@ class MenuWidget extends React.Component {
2312
2338
  return this.getProxyBase() + this.stripProtocol(url);
2313
2339
  }
2314
2340
 
2341
+ buildCdseLegendUrl(serviceUrl, layerTitle) {
2342
+ if (!serviceUrl || !layerTitle) return null;
2343
+ const collectionMatch =
2344
+ /\/cdse\/([^/?#]+)/i.exec(serviceUrl) ||
2345
+ /\/ogc\/(?:wmts|wms)\/([^/?#]+)/i.exec(serviceUrl);
2346
+ if (!collectionMatch || !collectionMatch[1]) return null;
2347
+ const legendParams = new URLSearchParams({
2348
+ service: 'WMS',
2349
+ request: 'GetLegendGraphic',
2350
+ version: '1.3.0',
2351
+ format: 'image/png',
2352
+ layer: layerTitle,
2353
+ style: 'default',
2354
+ });
2355
+ return (
2356
+ this.getProxyBase() +
2357
+ `land.copernicus.eu/cdse/${collectionMatch[1]}?${legendParams.toString()}`
2358
+ );
2359
+ }
2360
+
2315
2361
  parseWMSLayers(xml) {
2316
2362
  let doc = xml;
2317
2363
  try {
@@ -2708,13 +2754,14 @@ class MenuWidget extends React.Component {
2708
2754
  if (!activeLayerData) {
2709
2755
  return false;
2710
2756
  }
2711
- const resolveSentinelLayer = /(?:sh\.dataspace\.copernicus\.eu|services\.sentinel-hub\.com)\/ogc\/wmts/i.test(
2712
- (
2713
- currentLayerData.ViewService ||
2714
- currentLayerData.url ||
2715
- ''
2716
- ).toLowerCase(),
2717
- );
2757
+ const resolveSentinelLayer =
2758
+ /(?:sh\.dataspace\.copernicus\.eu|services\.sentinel-hub\.com)\/ogc\/wmts/i.test(
2759
+ (
2760
+ currentLayerData.ViewService ||
2761
+ currentLayerData.url ||
2762
+ ''
2763
+ ).toLowerCase(),
2764
+ );
2718
2765
  const nextCustomLayerParameters = {
2719
2766
  ...(currentLayerData.customLayerParameters || {}),
2720
2767
  SHOWLOGO: false,
@@ -3249,6 +3296,22 @@ class MenuWidget extends React.Component {
3249
3296
  try {
3250
3297
  const rawUrl = (proxiedUrl || '').trim();
3251
3298
  const baseUrl = rawUrl.split('?')[0];
3299
+ const resolveCapabilitiesText = (xmlData) => {
3300
+ if (!xmlData) {
3301
+ return '';
3302
+ }
3303
+ if (typeof xmlData === 'string') {
3304
+ return xmlData;
3305
+ }
3306
+ try {
3307
+ return new XMLSerializer().serializeToString(xmlData);
3308
+ } catch (e) {
3309
+ return '';
3310
+ }
3311
+ };
3312
+ const isInvalidCapabilitiesData = (xmlData) => {
3313
+ return !xmlData || !xmlData.documentElement;
3314
+ };
3252
3315
  const processBboxData = (xml) => {
3253
3316
  const buildExtentResult = (xmin, ymin, xmax, ymax) => {
3254
3317
  const numericValues = [xmin, ymin, xmax, ymax].map((value) =>
@@ -3407,10 +3470,21 @@ class MenuWidget extends React.Component {
3407
3470
  if (serviceType === 'WMTS') {
3408
3471
  this.xml = null;
3409
3472
  await this.getCapabilities(viewService, 'WMTS');
3410
- if (!this.xml) {
3473
+ if (isInvalidCapabilitiesData(this.xml)) {
3411
3474
  throw new Error('Unable to load WMTS capabilities');
3412
3475
  }
3476
+ const wmtsCapabilitiesText = resolveCapabilitiesText(this.xml);
3477
+ if (this.hasExceptionResponse(wmtsCapabilitiesText)) {
3478
+ const exceptionMessage =
3479
+ this.resolveExceptionMessage(wmtsCapabilitiesText);
3480
+ throw new Error(exceptionMessage || 'Unable to load WMTS service');
3481
+ }
3413
3482
  const wmtsLayers = this.parseWMTSLayers(this.xml);
3483
+ if (!Array.isArray(wmtsLayers) || wmtsLayers.length === 0) {
3484
+ throw new Error(
3485
+ 'Selected service has no geometry data and cannot be displayed on map',
3486
+ );
3487
+ }
3414
3488
  const active = this.resolveWmtsLayerData(wmtsLayers, serviceSelection);
3415
3489
  const serviceTitle = this.parseWMTSServiceTitle(this.xml);
3416
3490
  const isSceneViewActive = this.view && this.view.type === '3d';
@@ -3453,8 +3527,22 @@ class MenuWidget extends React.Component {
3453
3527
  ];
3454
3528
  } else if (isWFS) {
3455
3529
  await this.getCapabilities(viewService, 'WFS');
3530
+ if (isInvalidCapabilitiesData(this.xml)) {
3531
+ throw new Error('Unable to load WFS capabilities');
3532
+ }
3533
+ const wfsCapabilitiesText = resolveCapabilitiesText(this.xml);
3534
+ if (this.hasExceptionResponse(wfsCapabilitiesText)) {
3535
+ const exceptionMessage =
3536
+ this.resolveExceptionMessage(wfsCapabilitiesText);
3537
+ throw new Error(exceptionMessage || 'Unable to load WFS service');
3538
+ }
3456
3539
  const requestConfig = this.resolveRequestConfig(this.xml);
3457
3540
  const serviceEntries = Object.entries(serviceSelection || {});
3541
+ if (!serviceEntries.length) {
3542
+ throw new Error(
3543
+ 'Selected service has no geometry data and cannot be displayed on map',
3544
+ );
3545
+ }
3458
3546
  const layerResults = await Promise.all(
3459
3547
  serviceEntries.map(async ([name, title]) => {
3460
3548
  if (!name) return null;
@@ -3503,7 +3591,21 @@ class MenuWidget extends React.Component {
3503
3591
  resourceLayers = layerResults.filter(Boolean);
3504
3592
  } else {
3505
3593
  await this.getCapabilities(viewService, 'WMS');
3594
+ if (isInvalidCapabilitiesData(this.xml)) {
3595
+ throw new Error('Unable to load WMS capabilities');
3596
+ }
3597
+ const wmsCapabilitiesText = resolveCapabilitiesText(this.xml);
3598
+ if (this.hasExceptionResponse(wmsCapabilitiesText)) {
3599
+ const exceptionMessage =
3600
+ this.resolveExceptionMessage(wmsCapabilitiesText);
3601
+ throw new Error(exceptionMessage || 'Unable to load WMS service');
3602
+ }
3506
3603
  const wmsLayers = this.parseWMSLayers(this.xml);
3604
+ if (!Array.isArray(wmsLayers) || wmsLayers.length === 0) {
3605
+ throw new Error(
3606
+ 'Selected service has no geometry data and cannot be displayed on map',
3607
+ );
3608
+ }
3507
3609
  const serviceTitle = this.parseWMSServiceTitle(this.xml);
3508
3610
  const legendRequest =
3509
3611
  'request=GetLegendGraphic&version=1.0.0&format=image/png&layer=';
@@ -4210,9 +4312,8 @@ class MenuWidget extends React.Component {
4210
4312
  for (let g = 1; g < dataSetContents.length; g++) {
4211
4313
  if (dataSetContents[g].checked) {
4212
4314
  currentDataSetLayer = dataSetContents[g];
4213
- currentDataSetLayerSpan = currentDataSetLayer.nextSibling?.querySelector(
4214
- 'span',
4215
- );
4315
+ currentDataSetLayerSpan =
4316
+ currentDataSetLayer.nextSibling?.querySelector('span');
4216
4317
  currentElemContainerSpan = elemContainer.querySelector('span');
4217
4318
 
4218
4319
  if (
@@ -4459,11 +4560,12 @@ class MenuWidget extends React.Component {
4459
4560
  if (
4460
4561
  this.layers[elem.id].ViewService.toLowerCase().includes('wmts')
4461
4562
  ) {
4462
- const resolveSentinelLayer = /(?:sh\.dataspace\.copernicus\.eu|services\.sentinel-hub\.com)\/ogc\/wmts/i.test(
4463
- this.layers[elem.id].ViewService ||
4464
- this.layers[elem.id].url ||
4465
- '',
4466
- );
4563
+ const resolveSentinelLayer =
4564
+ /(?:sh\.dataspace\.copernicus\.eu|services\.sentinel-hub\.com)\/ogc\/wmts/i.test(
4565
+ this.layers[elem.id].ViewService ||
4566
+ this.layers[elem.id].url ||
4567
+ '',
4568
+ );
4467
4569
  const nextCustomLayerParameters = {
4468
4570
  ...(this.layers[elem.id].customLayerParameters || {}),
4469
4571
  SHOWLOGO: false,
@@ -4488,8 +4590,8 @@ class MenuWidget extends React.Component {
4488
4590
  ViewService: this.layers[elem.id].ViewService,
4489
4591
  StaticImageLegend: this.layers[elem.id].StaticImageLegend,
4490
4592
  LayerTitle: this.layers[elem.id].LayerTitle,
4491
- DatasetDownloadInformation: this.layers[elem.id]
4492
- .DatasetDownloadInformation,
4593
+ DatasetDownloadInformation:
4594
+ this.layers[elem.id].DatasetDownloadInformation,
4493
4595
  customLayerParameters: nextCustomLayerParameters,
4494
4596
  });
4495
4597
  }
@@ -7201,11 +7303,8 @@ class MenuWidget extends React.Component {
7201
7303
  if (this.props.download) return;
7202
7304
 
7203
7305
  if (prevProps.userServiceUrl !== this.props.userServiceUrl) {
7204
- const {
7205
- userServiceUrl,
7206
- userServiceType,
7207
- userServiceSelection,
7208
- } = this.props;
7306
+ const { userServiceUrl, userServiceType, userServiceSelection } =
7307
+ this.props;
7209
7308
  if (
7210
7309
  userServiceUrl &&
7211
7310
  typeof userServiceUrl === 'string' &&
@@ -186,7 +186,8 @@ class SwipeWidget extends React.Component {
186
186
  this._isMounted = true;
187
187
  await this.loader();
188
188
  if (!this.container.current) return;
189
- this.container.current.__mapViewerContainerParentNode = this.container.current.parentNode;
189
+ this.container.current.__mapViewerContainerParentNode =
190
+ this.container.current.parentNode;
190
191
  this.props.view.when(() => {
191
192
  if (!this._isMounted || !this.props.view || !this.props.view.ui) {
192
193
  return;
@@ -422,10 +423,12 @@ class SwipeWidget extends React.Component {
422
423
  this.props.view.ui.remove(this.swipe);
423
424
  this.props.view.ui.add(this.swipe);
424
425
  this.hasSwipe = true;
425
- let selectedLeadingLayer = document.getElementById('select-leading-layer')
426
- .value;
427
- let selectedTrailingLayer = document.getElementById('select-trailing-layer')
428
- .value;
426
+ let selectedLeadingLayer = document.getElementById(
427
+ 'select-leading-layer',
428
+ ).value;
429
+ let selectedTrailingLayer = document.getElementById(
430
+ 'select-trailing-layer',
431
+ ).value;
429
432
  let selectedSwipeDirection = document.getElementById(
430
433
  'select-swipe-direction',
431
434
  ).value;
@@ -196,8 +196,9 @@ class TimesliderWidget extends React.Component {
196
196
  } else {
197
197
  if (xml.querySelector('Dimension') !== null) {
198
198
  // There is a common time dimension to all layers
199
- dimension = xml.querySelector('Dimension').querySelector('Extent')
200
- .innerText;
199
+ dimension = xml
200
+ .querySelector('Dimension')
201
+ .querySelector('Extent').innerText;
201
202
  } else {
202
203
  dimension = false;
203
204
  }
@@ -275,7 +276,8 @@ class TimesliderWidget extends React.Component {
275
276
  }
276
277
 
277
278
  parserPeriod(iso8601Duration) {
278
- var iso8601DurationRegex = /(-)?P(?:([.,\d]+)Y)?(?:([.,\d]+)M)?(?:([.,\d]+)W)?(?:([.,\d]+)D)?T?(?:([.,\d]+)H)?(?:([.,\d]+)M)?(?:([.,\d]+)S)?/;
279
+ var iso8601DurationRegex =
280
+ /(-)?P(?:([.,\d]+)Y)?(?:([.,\d]+)M)?(?:([.,\d]+)W)?(?:([.,\d]+)D)?T?(?:([.,\d]+)H)?(?:([.,\d]+)M)?(?:([.,\d]+)S)?/;
279
281
  var matches = iso8601Duration.match(iso8601DurationRegex);
280
282
  return {
281
283
  sign: matches[1] === undefined ? '+' : '-',
@@ -372,7 +374,8 @@ class TimesliderWidget extends React.Component {
372
374
  ? this.container.current.parentNode
373
375
  : null;
374
376
  if (this.container && this.container.current) {
375
- this.container.current.__mapViewerContainerParentNode = this.containerParentNode;
377
+ this.container.current.__mapViewerContainerParentNode =
378
+ this.containerParentNode;
376
379
  }
377
380
  await this.loader();
378
381
  let playRateValue =
@@ -394,42 +397,40 @@ class TimesliderWidget extends React.Component {
394
397
  }
395
398
  if (value) {
396
399
  const normal = new Intl.DateTimeFormat('en-gb');
400
+ const timeSelectedValuesC = Array.isArray(
401
+ this.state.timeSelectedValuesC,
402
+ )
403
+ ? [...this.state.timeSelectedValuesC]
404
+ : [];
405
+ let timeSelectedValues = this.state.timeSelectedValues;
397
406
  switch (type) {
398
407
  case 'min':
399
- if (this.state.timeSelectedValuesC == null)
408
+ if (timeSelectedValuesC[0] == null)
400
409
  // In case of first iteration
401
- this.state.timeSelectedValuesC[0] = value;
410
+ timeSelectedValuesC[0] = value;
402
411
  element.innerText = normal.format(value).replaceAll('/', '.');
403
412
  break;
404
413
  case 'max':
405
- if (this.state.timeSelectedValuesC == null)
414
+ if (timeSelectedValuesC[1] == null)
406
415
  // In case of first iteration
407
- this.state.timeSelectedValuesC[1] = value;
416
+ timeSelectedValuesC[1] = value;
408
417
  element.innerText = normal.format(value).replaceAll('/', '.');
409
418
  break;
410
419
  case 'extent':
411
- this.state.timeSelectedValues = value;
420
+ timeSelectedValues = value;
412
421
  if (
413
- normal
414
- .format(this.state.timeSelectedValues[0])
415
- .replaceAll('/', '.') !==
416
- normal
417
- .format(this.state.timeSelectedValuesC[0])
418
- .replaceAll('/', '.')
422
+ normal.format(timeSelectedValues[0]).replaceAll('/', '.') !==
423
+ normal.format(timeSelectedValuesC[0]).replaceAll('/', '.')
419
424
  ) {
420
- this.state.timeSelectedValuesC[0] = value[0];
425
+ timeSelectedValuesC[0] = value[0];
421
426
  element.innerText = normal
422
427
  .format(value[0])
423
428
  .replaceAll('/', '.');
424
429
  } else if (
425
- normal
426
- .format(this.state.timeSelectedValues[1])
427
- .replaceAll('/', '.') !==
428
- normal
429
- .format(this.state.timeSelectedValuesC[1])
430
- .replaceAll('/', '.')
430
+ normal.format(timeSelectedValues[1]).replaceAll('/', '.') !==
431
+ normal.format(timeSelectedValuesC[1]).replaceAll('/', '.')
431
432
  ) {
432
- this.state.timeSelectedValuesC[1] = value[1];
433
+ timeSelectedValuesC[1] = value[1];
433
434
  element.innerText = normal
434
435
  .format(value[1])
435
436
  .replaceAll('/', '.');
@@ -440,6 +441,8 @@ class TimesliderWidget extends React.Component {
440
441
  break;
441
442
  }
442
443
  this.setState({
444
+ timeSelectedValues: timeSelectedValues,
445
+ timeSelectedValuesC: timeSelectedValuesC,
443
446
  lockDatePanel: false,
444
447
  });
445
448
  }
@@ -478,7 +481,8 @@ class TimesliderWidget extends React.Component {
478
481
  const isCDSE =
479
482
  urlNorm.includes('/ogc/') || urlNorm.includes('/cdse/');
480
483
  if (this.layer.type === 'feature') {
481
- this.TimesliderWidget.fullTimeExtent = this.layer.timeInfo.fullTimeExtent;
484
+ this.TimesliderWidget.fullTimeExtent =
485
+ this.layer.timeInfo.fullTimeExtent;
482
486
  this.TimesliderWidget.stops = {
483
487
  interval: this.layer.timeInfo.interval,
484
488
  };
@@ -751,12 +751,12 @@ div.upload-container div.wfs-features-list label.field input[type='checkbox'] {
751
751
  box-sizing: border-box;
752
752
  flex: 0 0 18px !important;
753
753
  border: 1px solid #8a8a8a;
754
+ border-radius: 3px;
754
755
  margin: 0 6px 0 0;
755
756
  -webkit-appearance: none !important;
756
757
  -moz-appearance: none !important;
757
758
  appearance: none !important;
758
759
  background-color: #ffffff;
759
- border-radius: 3px;
760
760
  vertical-align: middle;
761
761
  }
762
762
 
@@ -1759,9 +1759,9 @@ div.upload-container
1759
1759
  input[type='range'] {
1760
1760
  width: 100%;
1761
1761
  height: 4px;
1762
+ border-radius: 5px;
1762
1763
  -webkit-appearance: none;
1763
1764
  background: #c5c5c5;
1764
- border-radius: 5px;
1765
1765
  cursor: pointer;
1766
1766
  }
1767
1767
 
@@ -1769,9 +1769,9 @@ input[type='range']::-webkit-slider-thumb {
1769
1769
  width: 16px;
1770
1770
  height: 16px;
1771
1771
  border: solid 3px black;
1772
+ border-radius: 50%;
1772
1773
  -webkit-appearance: none;
1773
1774
  background: white;
1774
- border-radius: 50%;
1775
1775
  cursor: move;
1776
1776
  transition: all 0.3s ease-in-out;
1777
1777
  }
@@ -1780,9 +1780,9 @@ input[type='range']::-moz-range-thumb {
1780
1780
  width: 16px;
1781
1781
  height: 16px;
1782
1782
  border: solid 3px black;
1783
+ border-radius: 50%;
1783
1784
  -webkit-appearance: none;
1784
1785
  background: white;
1785
- border-radius: 50%;
1786
1786
  cursor: grab;
1787
1787
  transition: all 0.3s ease-in-out;
1788
1788
  }
@@ -1791,9 +1791,9 @@ input[type='range']::-ms-thumb {
1791
1791
  width: 16px;
1792
1792
  height: 16px;
1793
1793
  border: solid 3px black;
1794
+ border-radius: 50%;
1794
1795
  -webkit-appearance: none;
1795
1796
  background: white;
1796
- border-radius: 50%;
1797
1797
  cursor: grab;
1798
1798
  transition: all 0.3s ease-in-out;
1799
1799
  }
@@ -1844,9 +1844,9 @@ input[type='range']::-ms-track {
1844
1844
  .hotspot-filter-message {
1845
1845
  width: 247 !important;
1846
1846
  padding: 0.3rem 0.6rem 0.1rem 0.6rem;
1847
+ border-radius: 10px;
1847
1848
  margin-top: 0.2rem;
1848
1849
  background: #a0b128;
1849
- border-radius: 10px;
1850
1850
  color: white;
1851
1851
  font-size: 0.875rem;
1852
1852
  text-align: center;
@@ -2023,9 +2023,9 @@ div.map-container.popup-block {
2023
2023
  height: 2rem;
2024
2024
  border-width: 1px;
2025
2025
  border-color: rgba(110, 110, 110, 0.3);
2026
+ border-radius: 0;
2026
2027
  margin-left: 0.5rem;
2027
2028
  background-color: #a0b128;
2028
- border-radius: 0;
2029
2029
  color: white;
2030
2030
  }
2031
2031