@eeacms/volto-arcgis-block 0.1.452 → 0.1.453

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,21 @@ 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.453](https://github.com/eea/volto-arcgis-block/compare/0.1.452...0.1.453) - 3 June 2026
8
+
9
+ #### :house: Internal changes
10
+
11
+ - style: Automated code fix [eea-jenkins - [`7e20e98`](https://github.com/eea/volto-arcgis-block/commit/7e20e98f5f5e497b79557453c464dee98753ea6e)]
12
+ - chore: upgrade Makefile to latest [valentinab25 - [`fb2e387`](https://github.com/eea/volto-arcgis-block/commit/fb2e387f9ec8cfac7be5894d775313dddaf80897)]
13
+
14
+ #### :hammer_and_wrench: Others
15
+
16
+ - (task): add stylelint dev dependency [Unai Bolivar - [`d11c354`](https://github.com/eea/volto-arcgis-block/commit/d11c3540d3eb4bfc064768cb70d9d1809c60ec3a)]
17
+ - (bug): fix isuess found by jenkins [Unai Bolivar - [`74638f9`](https://github.com/eea/volto-arcgis-block/commit/74638f9047b7868a7a64a7b3ca3bbdfa1ff8a8cd)]
18
+ - (bug): fix isuess found by jenkins [Unai Bolivar - [`98c1258`](https://github.com/eea/volto-arcgis-block/commit/98c125831bd9d0290087c244777c7b6764d406f0)]
19
+ - (task): update volto version in jenkinsfile [Unai Bolivar - [`f0c13e8`](https://github.com/eea/volto-arcgis-block/commit/f0c13e84a71063f4181d5cc59406132db7b29793)]
20
+ - (bug): Error handling when uploading external services [Unai Bolivar - [`f8f873d`](https://github.com/eea/volto-arcgis-block/commit/f8f873d53064def279101a711df99da09a4dd6b6)]
21
+ - (bug): In the legend widget, retrieve the legends from CDSE services [Unai Bolivar - [`b909191`](https://github.com/eea/volto-arcgis-block/commit/b9091910c676568f2070b3b689972d169d8a2880)]
7
22
  ### [0.1.452](https://github.com/eea/volto-arcgis-block/compare/0.1.451...0.1.452) - 28 May 2026
8
23
 
9
24
  ### [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.453",
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
  }
@@ -2017,6 +2016,14 @@ class MenuWidget extends React.Component {
2017
2016
  //For Legend request
2018
2017
  const legendRequest =
2019
2018
  'request=GetLegendGraphic&version=1.0.0&format=image/png&layer=';
2019
+ const isCdseService =
2020
+ !!viewService &&
2021
+ ['/ogc/', '/cdse/'].some((segment) =>
2022
+ viewService.toLowerCase().includes(segment),
2023
+ );
2024
+ const cdseLegendUrl = isCdseService
2025
+ ? this.buildCdseLegendUrl(viewService, layer.Title || layer.LayerId)
2026
+ : null;
2020
2027
  //For each layer
2021
2028
  let inheritedIndexLayer = inheritedIndex + '_' + layerIndex;
2022
2029
  let style = this.props.download ? { paddingLeft: '4rem' } : {};
@@ -2025,16 +2032,15 @@ class MenuWidget extends React.Component {
2025
2032
  !this.layers.hasOwnProperty(layer.LayerId + '_' + inheritedIndexLayer)
2026
2033
  ) {
2027
2034
  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
- });
2035
+ this.layers[layer.LayerId + '_' + inheritedIndexLayer] =
2036
+ new MapImageLayer({
2037
+ url: viewService,
2038
+ title: layer.Title,
2039
+ DatasetId: DatasetId,
2040
+ DatasetTitle: DatasetTitle,
2041
+ ProductId: ProductId,
2042
+ LayerTitle: layer.Title,
2043
+ });
2038
2044
  //iterate sublayers fetching all sublayer data
2039
2045
  } else if (viewService?.toLowerCase().includes('wms')) {
2040
2046
  viewService = viewService?.includes('?')
@@ -2056,6 +2062,8 @@ class MenuWidget extends React.Component {
2056
2062
  legendEnabled: true,
2057
2063
  legendUrl: layer.StaticImageLegend
2058
2064
  ? layer.StaticImageLegend
2065
+ : cdseLegendUrl
2066
+ ? cdseLegendUrl
2059
2067
  : viewService + legendRequest + layer.LayerId,
2060
2068
  featureInfoUrl: featureInfoUrl,
2061
2069
  },
@@ -2068,9 +2076,10 @@ class MenuWidget extends React.Component {
2068
2076
  ViewService: viewService,
2069
2077
  });
2070
2078
  } else if (viewService?.toLowerCase().includes('wmts')) {
2071
- const resolveSentinelLayer = /(?:sh\.dataspace\.copernicus\.eu|services\.sentinel-hub\.com)\/ogc\/wmts/i.test(
2072
- viewService || '',
2073
- );
2079
+ const resolveSentinelLayer =
2080
+ /(?:sh\.dataspace\.copernicus\.eu|services\.sentinel-hub\.com)\/ogc\/wmts/i.test(
2081
+ viewService || '',
2082
+ );
2074
2083
  this.layers[layer.LayerId + '_' + inheritedIndexLayer] = new WMTSLayer({
2075
2084
  url: viewService?.includes('?')
2076
2085
  ? viewService + '&'
@@ -2091,7 +2100,7 @@ class MenuWidget extends React.Component {
2091
2100
  DatasetTitle: DatasetTitle,
2092
2101
  ProductId: ProductId,
2093
2102
  ViewService: viewService,
2094
- StaticImageLegend: layer.StaticImageLegend,
2103
+ StaticImageLegend: layer.StaticImageLegend || cdseLegendUrl,
2095
2104
  LayerTitle: layer.Title,
2096
2105
  DatasetDownloadInformation: dataset_download_information || {},
2097
2106
  customLayerParameters: {
@@ -2100,24 +2109,23 @@ class MenuWidget extends React.Component {
2100
2109
  },
2101
2110
  });
2102
2111
  } 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
- });
2112
+ this.layers[layer.LayerId + '_' + inheritedIndexLayer] =
2113
+ new FeatureLayer({
2114
+ url:
2115
+ viewService +
2116
+ (viewService?.endsWith('/') ? '' : '/') +
2117
+ layer.LayerId,
2118
+ id: layer.LayerId,
2119
+ title: layer.Title,
2120
+ featureInfoUrl: featureInfoUrl,
2121
+ popupEnabled: true,
2122
+ isTimeSeries: isTimeSeries,
2123
+ fields: layer.Fields,
2124
+ DatasetId: DatasetId,
2125
+ DatasetTitle: DatasetTitle,
2126
+ ProductId: ProductId,
2127
+ ViewService: viewService,
2128
+ });
2121
2129
  }
2122
2130
  }
2123
2131
  // const isCDSE = !!this.url && this.url.toLowerCase().includes('/ogc/');
@@ -2312,6 +2320,26 @@ class MenuWidget extends React.Component {
2312
2320
  return this.getProxyBase() + this.stripProtocol(url);
2313
2321
  }
2314
2322
 
2323
+ buildCdseLegendUrl(serviceUrl, layerTitle) {
2324
+ if (!serviceUrl || !layerTitle) return null;
2325
+ const collectionMatch =
2326
+ /\/cdse\/([^/?#]+)/i.exec(serviceUrl) ||
2327
+ /\/ogc\/(?:wmts|wms)\/([^/?#]+)/i.exec(serviceUrl);
2328
+ if (!collectionMatch || !collectionMatch[1]) return null;
2329
+ const legendParams = new URLSearchParams({
2330
+ service: 'WMS',
2331
+ request: 'GetLegendGraphic',
2332
+ version: '1.3.0',
2333
+ format: 'image/png',
2334
+ layer: layerTitle,
2335
+ style: 'default',
2336
+ });
2337
+ return (
2338
+ this.getProxyBase() +
2339
+ `land.copernicus.eu/cdse/${collectionMatch[1]}?${legendParams.toString()}`
2340
+ );
2341
+ }
2342
+
2315
2343
  parseWMSLayers(xml) {
2316
2344
  let doc = xml;
2317
2345
  try {
@@ -2708,13 +2736,14 @@ class MenuWidget extends React.Component {
2708
2736
  if (!activeLayerData) {
2709
2737
  return false;
2710
2738
  }
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
- );
2739
+ const resolveSentinelLayer =
2740
+ /(?:sh\.dataspace\.copernicus\.eu|services\.sentinel-hub\.com)\/ogc\/wmts/i.test(
2741
+ (
2742
+ currentLayerData.ViewService ||
2743
+ currentLayerData.url ||
2744
+ ''
2745
+ ).toLowerCase(),
2746
+ );
2718
2747
  const nextCustomLayerParameters = {
2719
2748
  ...(currentLayerData.customLayerParameters || {}),
2720
2749
  SHOWLOGO: false,
@@ -3249,6 +3278,22 @@ class MenuWidget extends React.Component {
3249
3278
  try {
3250
3279
  const rawUrl = (proxiedUrl || '').trim();
3251
3280
  const baseUrl = rawUrl.split('?')[0];
3281
+ const resolveCapabilitiesText = (xmlData) => {
3282
+ if (!xmlData) {
3283
+ return '';
3284
+ }
3285
+ if (typeof xmlData === 'string') {
3286
+ return xmlData;
3287
+ }
3288
+ try {
3289
+ return new XMLSerializer().serializeToString(xmlData);
3290
+ } catch (e) {
3291
+ return '';
3292
+ }
3293
+ };
3294
+ const isInvalidCapabilitiesData = (xmlData) => {
3295
+ return !xmlData || !xmlData.documentElement;
3296
+ };
3252
3297
  const processBboxData = (xml) => {
3253
3298
  const buildExtentResult = (xmin, ymin, xmax, ymax) => {
3254
3299
  const numericValues = [xmin, ymin, xmax, ymax].map((value) =>
@@ -3407,10 +3452,21 @@ class MenuWidget extends React.Component {
3407
3452
  if (serviceType === 'WMTS') {
3408
3453
  this.xml = null;
3409
3454
  await this.getCapabilities(viewService, 'WMTS');
3410
- if (!this.xml) {
3455
+ if (isInvalidCapabilitiesData(this.xml)) {
3411
3456
  throw new Error('Unable to load WMTS capabilities');
3412
3457
  }
3458
+ const wmtsCapabilitiesText = resolveCapabilitiesText(this.xml);
3459
+ if (this.hasExceptionResponse(wmtsCapabilitiesText)) {
3460
+ const exceptionMessage =
3461
+ this.resolveExceptionMessage(wmtsCapabilitiesText);
3462
+ throw new Error(exceptionMessage || 'Unable to load WMTS service');
3463
+ }
3413
3464
  const wmtsLayers = this.parseWMTSLayers(this.xml);
3465
+ if (!Array.isArray(wmtsLayers) || wmtsLayers.length === 0) {
3466
+ throw new Error(
3467
+ 'Selected service has no geometry data and cannot be displayed on map',
3468
+ );
3469
+ }
3414
3470
  const active = this.resolveWmtsLayerData(wmtsLayers, serviceSelection);
3415
3471
  const serviceTitle = this.parseWMTSServiceTitle(this.xml);
3416
3472
  const isSceneViewActive = this.view && this.view.type === '3d';
@@ -3453,8 +3509,22 @@ class MenuWidget extends React.Component {
3453
3509
  ];
3454
3510
  } else if (isWFS) {
3455
3511
  await this.getCapabilities(viewService, 'WFS');
3512
+ if (isInvalidCapabilitiesData(this.xml)) {
3513
+ throw new Error('Unable to load WFS capabilities');
3514
+ }
3515
+ const wfsCapabilitiesText = resolveCapabilitiesText(this.xml);
3516
+ if (this.hasExceptionResponse(wfsCapabilitiesText)) {
3517
+ const exceptionMessage =
3518
+ this.resolveExceptionMessage(wfsCapabilitiesText);
3519
+ throw new Error(exceptionMessage || 'Unable to load WFS service');
3520
+ }
3456
3521
  const requestConfig = this.resolveRequestConfig(this.xml);
3457
3522
  const serviceEntries = Object.entries(serviceSelection || {});
3523
+ if (!serviceEntries.length) {
3524
+ throw new Error(
3525
+ 'Selected service has no geometry data and cannot be displayed on map',
3526
+ );
3527
+ }
3458
3528
  const layerResults = await Promise.all(
3459
3529
  serviceEntries.map(async ([name, title]) => {
3460
3530
  if (!name) return null;
@@ -3503,7 +3573,21 @@ class MenuWidget extends React.Component {
3503
3573
  resourceLayers = layerResults.filter(Boolean);
3504
3574
  } else {
3505
3575
  await this.getCapabilities(viewService, 'WMS');
3576
+ if (isInvalidCapabilitiesData(this.xml)) {
3577
+ throw new Error('Unable to load WMS capabilities');
3578
+ }
3579
+ const wmsCapabilitiesText = resolveCapabilitiesText(this.xml);
3580
+ if (this.hasExceptionResponse(wmsCapabilitiesText)) {
3581
+ const exceptionMessage =
3582
+ this.resolveExceptionMessage(wmsCapabilitiesText);
3583
+ throw new Error(exceptionMessage || 'Unable to load WMS service');
3584
+ }
3506
3585
  const wmsLayers = this.parseWMSLayers(this.xml);
3586
+ if (!Array.isArray(wmsLayers) || wmsLayers.length === 0) {
3587
+ throw new Error(
3588
+ 'Selected service has no geometry data and cannot be displayed on map',
3589
+ );
3590
+ }
3507
3591
  const serviceTitle = this.parseWMSServiceTitle(this.xml);
3508
3592
  const legendRequest =
3509
3593
  'request=GetLegendGraphic&version=1.0.0&format=image/png&layer=';
@@ -4210,9 +4294,8 @@ class MenuWidget extends React.Component {
4210
4294
  for (let g = 1; g < dataSetContents.length; g++) {
4211
4295
  if (dataSetContents[g].checked) {
4212
4296
  currentDataSetLayer = dataSetContents[g];
4213
- currentDataSetLayerSpan = currentDataSetLayer.nextSibling?.querySelector(
4214
- 'span',
4215
- );
4297
+ currentDataSetLayerSpan =
4298
+ currentDataSetLayer.nextSibling?.querySelector('span');
4216
4299
  currentElemContainerSpan = elemContainer.querySelector('span');
4217
4300
 
4218
4301
  if (
@@ -4459,11 +4542,12 @@ class MenuWidget extends React.Component {
4459
4542
  if (
4460
4543
  this.layers[elem.id].ViewService.toLowerCase().includes('wmts')
4461
4544
  ) {
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
- );
4545
+ const resolveSentinelLayer =
4546
+ /(?:sh\.dataspace\.copernicus\.eu|services\.sentinel-hub\.com)\/ogc\/wmts/i.test(
4547
+ this.layers[elem.id].ViewService ||
4548
+ this.layers[elem.id].url ||
4549
+ '',
4550
+ );
4467
4551
  const nextCustomLayerParameters = {
4468
4552
  ...(this.layers[elem.id].customLayerParameters || {}),
4469
4553
  SHOWLOGO: false,
@@ -4488,8 +4572,8 @@ class MenuWidget extends React.Component {
4488
4572
  ViewService: this.layers[elem.id].ViewService,
4489
4573
  StaticImageLegend: this.layers[elem.id].StaticImageLegend,
4490
4574
  LayerTitle: this.layers[elem.id].LayerTitle,
4491
- DatasetDownloadInformation: this.layers[elem.id]
4492
- .DatasetDownloadInformation,
4575
+ DatasetDownloadInformation:
4576
+ this.layers[elem.id].DatasetDownloadInformation,
4493
4577
  customLayerParameters: nextCustomLayerParameters,
4494
4578
  });
4495
4579
  }
@@ -7201,11 +7285,8 @@ class MenuWidget extends React.Component {
7201
7285
  if (this.props.download) return;
7202
7286
 
7203
7287
  if (prevProps.userServiceUrl !== this.props.userServiceUrl) {
7204
- const {
7205
- userServiceUrl,
7206
- userServiceType,
7207
- userServiceSelection,
7208
- } = this.props;
7288
+ const { userServiceUrl, userServiceType, userServiceSelection } =
7289
+ this.props;
7209
7290
  if (
7210
7291
  userServiceUrl &&
7211
7292
  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