@eeacms/volto-arcgis-block 0.1.433 → 0.1.435

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,8 +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.435](https://github.com/eea/volto-arcgis-block/compare/0.1.434...0.1.435) - 10 March 2026
8
+
9
+ #### :hammer_and_wrench: Others
10
+
11
+ - (bug): Fix Upload widget TOC menu stored services correct loading in 2D and 3D [Unai Bolivar - [`8b9e5fa`](https://github.com/eea/volto-arcgis-block/commit/8b9e5fad5a7d361e8fa4c8f48ae771422ae1c262)]
12
+ - (bug): fix upload widget error report, loading spinner, and TOC menu cleanup in 3D [Unai Bolivar - [`dc2ab0d`](https://github.com/eea/volto-arcgis-block/commit/dc2ab0d5ecafdb1cdc9c417bd8533c7ff0f28446)]
13
+ ### [0.1.434](https://github.com/eea/volto-arcgis-block/compare/0.1.433...0.1.434) - 10 March 2026
14
+
15
+ #### :hammer_and_wrench: Others
16
+
17
+ - (feat): Uploaded WMTS for external services fails if server tiling scheme displays popup error 'Incorrect tiling scheme for scene view. The service can still be used in 2D map view.' [Unai Bolivar - [`c513433`](https://github.com/eea/volto-arcgis-block/commit/c513433905182949721c13fcc056e67d7d780942)]
18
+ - (bug): 3D and 2D buttons now in place and functioning [Unai Bolivar - [`e93b038`](https://github.com/eea/volto-arcgis-block/commit/e93b0386b6b10f6aff04f4533a2bb425047ae95c)]
19
+ - (feat): New 3D button location and styles [Unai Bolivar - [`c367036`](https://github.com/eea/volto-arcgis-block/commit/c3670363e4440bdabf25cb5b193a39b25077087a)]
20
+ - (bug): Fix WMTS layer load on reselect in 3D Scene view mode [Unai Bolivar - [`0036b17`](https://github.com/eea/volto-arcgis-block/commit/0036b170da08e5c3c3e6bd7609d95873f88d497d)]
21
+ - (feat): Bookmarks widget loads in 3D globe view mode [Unai Bolivar - [`847ed10`](https://github.com/eea/volto-arcgis-block/commit/847ed10d05668678dd356e91ffbf6aa439ba4169)]
7
22
  ### [0.1.433](https://github.com/eea/volto-arcgis-block/compare/0.1.432...0.1.433) - 9 March 2026
8
23
 
24
+ #### :hammer_and_wrench: Others
25
+
26
+ - Merge pull request #1112 from eea/develop [Unai Bolivar - [`c4d87a3`](https://github.com/eea/volto-arcgis-block/commit/c4d87a3e923451e94d0fcddd35b791cae4b1c292)]
9
27
  ### [0.1.432](https://github.com/eea/volto-arcgis-block/compare/0.1.431...0.1.432) - 4 March 2026
10
28
 
11
29
  #### :house: Internal changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-arcgis-block",
3
- "version": "0.1.433",
3
+ "version": "0.1.435",
4
4
  "description": "volto-arcgis-block: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: CodeSyntax",
@@ -237,6 +237,9 @@ class BookmarkWidget extends React.Component {
237
237
  },
238
238
  container: document.querySelector('.bookmark-panel'),
239
239
  bookmarks: this.sessionBookmarks.map((bm) => {
240
+ if (bm && bm.viewpoint) {
241
+ return bm;
242
+ }
240
243
  if (bm.extent) {
241
244
  const { extent, ...rest } = bm;
242
245
  let geometry;
@@ -1075,8 +1078,15 @@ class BookmarkWidget extends React.Component {
1075
1078
  }
1076
1079
  if (name !== undefined) out.name = name;
1077
1080
  let g = null;
1078
- if (b && b.viewpoint && b.viewpoint.targetGeometry) {
1079
- g = b.viewpoint.targetGeometry;
1081
+ let viewpointData;
1082
+ if (b && b.viewpoint) {
1083
+ viewpointData = b.viewpoint.toJSON
1084
+ ? b.viewpoint.toJSON()
1085
+ : b.viewpoint;
1086
+ out.viewpoint = viewpointData;
1087
+ if (b.viewpoint.targetGeometry) {
1088
+ g = b.viewpoint.targetGeometry;
1089
+ }
1080
1090
  } else if (b && b.extent) {
1081
1091
  g = b.extent;
1082
1092
  }
@@ -1246,6 +1256,9 @@ class BookmarkWidget extends React.Component {
1246
1256
  this.loadBookmarksToWidget();
1247
1257
  if (this.Bookmarks && this.Bookmarks.bookmarks) {
1248
1258
  const mapped = this.sessionBookmarks.map((bm) => {
1259
+ if (bm && bm.viewpoint) {
1260
+ return bm;
1261
+ }
1249
1262
  if (bm && bm.extent) {
1250
1263
  const { extent, ...rest } = bm;
1251
1264
  let geometry;
@@ -1341,7 +1354,7 @@ class BookmarkWidget extends React.Component {
1341
1354
  }}
1342
1355
  type="submit"
1343
1356
  >
1344
- <span class="esri-bookmarks__add-bookmark-icon esri-icon-plus"></span>
1357
+ <span className="esri-bookmarks__add-bookmark-icon esri-icon-plus"></span>
1345
1358
  Upload a bookmark
1346
1359
  </button>
1347
1360
  </div>
@@ -37,11 +37,17 @@ class LoadingSpinner extends React.Component {
37
37
  });
38
38
  const handle3 = this.props.view.on('layerview-create-error', (event) => {
39
39
  if (!this._isMounted) return;
40
- if (event.layer.loadError !== null && this.state.loading === true) {
40
+ if (this.state.loading === true) {
41
41
  this.setState({ loading: false }, this.showLoading);
42
42
  }
43
43
  });
44
- this.arcgisEventHandles.push(handle1, handle2, handle3);
44
+ const handle4 = this.props.view.watch('updating', (isUpdating) => {
45
+ if (!this._isMounted) return;
46
+ if (!isUpdating && this.state.loading === true) {
47
+ this.setState({ loading: false }, this.showLoading);
48
+ }
49
+ });
50
+ this.arcgisEventHandles.push(handle1, handle2, handle3, handle4);
45
51
  }
46
52
 
47
53
  showLoading() {
@@ -382,8 +382,14 @@ class MapViewer extends React.Component {
382
382
  this.viewTransitionTaskId = 0;
383
383
  this.isViewSwitchInProgress = false;
384
384
  this.syncViewTask = null;
385
+ this.viewModeButtonTimeout = null;
386
+ this.isViewModeButtonLoaded = false;
385
387
  this.viewUiOperationState = null;
386
388
  this.shouldClearSessionOnUnmount = true;
389
+ this.uploadErrorTimeoutTask = null;
390
+ this.scheduleViewModeButtonLoad = this.scheduleViewModeButtonLoad.bind(
391
+ this,
392
+ );
387
393
  this.processPendingWidgetActivation = this.processPendingWidgetActivation.bind(
388
394
  this,
389
395
  );
@@ -426,6 +432,32 @@ class MapViewer extends React.Component {
426
432
  return viewMode === '3d' ? '3d' : '2d';
427
433
  }
428
434
 
435
+ scheduleViewModeButtonLoad() {
436
+ if (this.viewModeButtonTimeout) {
437
+ clearTimeout(this.viewModeButtonTimeout);
438
+ this.viewModeButtonTimeout = null;
439
+ }
440
+
441
+ if (this.state.viewMode !== '2d') {
442
+ if (!this.isViewModeButtonLoaded) {
443
+ this.isViewModeButtonLoaded = true;
444
+ this.scheduleViewSyncTask();
445
+ }
446
+ return;
447
+ }
448
+
449
+ this.isViewModeButtonLoaded = false;
450
+
451
+ this.viewModeButtonTimeout = setTimeout(() => {
452
+ if (!this.isComponentMounted) {
453
+ return;
454
+ }
455
+ this.isViewModeButtonLoaded = true;
456
+ this.scheduleViewSyncTask();
457
+ this.viewModeButtonTimeout = null;
458
+ }, 1000);
459
+ }
460
+
429
461
  getViewConstraintExtent(useZoomInBounds) {
430
462
  const geometryConfig = useZoomInBounds
431
463
  ? this.mapCfg.geometryZoomIn
@@ -598,10 +630,19 @@ class MapViewer extends React.Component {
598
630
  { selector: '.hotspot-container', position: 'top-right' },
599
631
  { selector: '.bookmark-container', position: 'top-right' },
600
632
  { selector: '.upload-container', position: 'top-right' },
633
+ { selector: '.viewmode-container', position: 'top-right' },
601
634
  { selector: '.error-report-container', position: 'top-right' },
602
635
  ];
603
636
 
604
637
  uiContainerConfig.forEach(({ selector, position }) => {
638
+ if (
639
+ selector === '.viewmode-container' &&
640
+ this.state.viewMode === '2d' &&
641
+ !this.isViewModeButtonLoaded
642
+ ) {
643
+ return;
644
+ }
645
+
605
646
  const containerNodeList = document.querySelectorAll(selector);
606
647
  if (!containerNodeList || containerNodeList.length === 0) return;
607
648
  containerNodeList.forEach((containerNode) => {
@@ -1265,8 +1306,16 @@ class MapViewer extends React.Component {
1265
1306
 
1266
1307
  uploadFileErrorHandler = (errorType = 'uploadError') => {
1267
1308
  this.setState({ uploadError: true, uploadErrorType: errorType });
1268
- setTimeout(() => {
1309
+ if (this.uploadErrorTimeoutTask) {
1310
+ clearTimeout(this.uploadErrorTimeoutTask);
1311
+ this.uploadErrorTimeoutTask = null;
1312
+ }
1313
+ this.uploadErrorTimeoutTask = setTimeout(() => {
1314
+ if (!this.isComponentMounted) {
1315
+ return;
1316
+ }
1269
1317
  this.setState({ uploadError: false, uploadErrorType: 'uploadError' });
1318
+ this.uploadErrorTimeoutTask = null;
1270
1319
  }, 3000);
1271
1320
  };
1272
1321
 
@@ -1395,6 +1444,7 @@ class MapViewer extends React.Component {
1395
1444
  zoom: mapStatus.zoom,
1396
1445
  viewpoint: null,
1397
1446
  });
1447
+ this.scheduleViewModeButtonLoad();
1398
1448
  // After launching the MapViewerConfig action
1399
1449
  // we will have stored the json response here:
1400
1450
  // this.props.mapviewer_config
@@ -1455,6 +1505,7 @@ class MapViewer extends React.Component {
1455
1505
  }
1456
1506
 
1457
1507
  if (prevState.viewMode !== this.state.viewMode) {
1508
+ this.scheduleViewModeButtonLoad();
1458
1509
  this.scheduleViewSyncTask();
1459
1510
  }
1460
1511
 
@@ -1471,6 +1522,16 @@ class MapViewer extends React.Component {
1471
1522
  this.syncViewTask = null;
1472
1523
  }
1473
1524
 
1525
+ if (this.viewModeButtonTimeout) {
1526
+ clearTimeout(this.viewModeButtonTimeout);
1527
+ this.viewModeButtonTimeout = null;
1528
+ }
1529
+
1530
+ if (this.uploadErrorTimeoutTask) {
1531
+ clearTimeout(this.uploadErrorTimeoutTask);
1532
+ this.uploadErrorTimeoutTask = null;
1533
+ }
1534
+
1474
1535
  window.removeEventListener('beforeunload', this.handlePageUnload);
1475
1536
  window.removeEventListener('pagehide', this.handlePageUnload);
1476
1537
  document.removeEventListener(
@@ -1771,28 +1832,21 @@ class MapViewer extends React.Component {
1771
1832
  renderViewModeSwitcher() {
1772
1833
  if (!this.view) return null;
1773
1834
 
1835
+ const nextViewMode = this.state.viewMode === '3d' ? '2d' : '3d';
1836
+ const viewModeLabel = this.state.viewMode === '3d' ? '2D' : '3D';
1837
+ const viewModeAriaLabel =
1838
+ this.state.viewMode === '3d' ? 'Switch to 2D view' : 'Switch to 3D view';
1839
+
1774
1840
  return (
1775
1841
  <div className="viewmode-container esri-component esri-widget">
1776
1842
  <div className="viewmode-button-group">
1777
1843
  <button
1778
- className={classNames('viewmode-button', {
1779
- 'active-widget': this.state.viewMode === '2d',
1780
- })}
1781
- onClick={() => this.switchViewMode('2d')}
1844
+ className="viewmode-button viewmode-toggle-button"
1845
+ onClick={() => this.switchViewMode(nextViewMode)}
1782
1846
  type="button"
1783
- aria-label="Switch to 2D view"
1847
+ aria-label={viewModeAriaLabel}
1784
1848
  >
1785
- 2D
1786
- </button>
1787
- <button
1788
- className={classNames('viewmode-button', {
1789
- 'active-widget': this.state.viewMode === '3d',
1790
- })}
1791
- onClick={() => this.switchViewMode('3d')}
1792
- type="button"
1793
- aria-label="Switch to 3D view"
1794
- >
1795
- 3D
1849
+ {viewModeLabel}
1796
1850
  </button>
1797
1851
  </div>
1798
1852
  </div>
@@ -1876,20 +1930,18 @@ export const CheckUserID = ({ reference }) => {
1876
1930
  {reference.view && (
1877
1931
  <>
1878
1932
  {/* BookmarkWidget with user_id */}
1879
- {reference.view.type === '2d' && (
1880
- <BookmarkWidget
1881
- key={reference.getWidgetRenderKey('bookmark')}
1882
- view={reference.view}
1883
- map={reference.map}
1884
- layers={reference.state.layers}
1885
- mapViewer={reference}
1886
- userID={user_id}
1887
- hotspotData={reference.state.hotspotData}
1888
- bookmarkHandler={reference.bookmarkHandler}
1889
- bookmarkData={reference.state.bookmarkData}
1890
- isLoggedIn={isLoggedIn}
1891
- />
1892
- )}
1933
+ <BookmarkWidget
1934
+ key={reference.getWidgetRenderKey('bookmark')}
1935
+ view={reference.view}
1936
+ map={reference.map}
1937
+ layers={reference.state.layers}
1938
+ mapViewer={reference}
1939
+ userID={user_id}
1940
+ hotspotData={reference.state.hotspotData}
1941
+ bookmarkHandler={reference.bookmarkHandler}
1942
+ bookmarkData={reference.state.bookmarkData}
1943
+ isLoggedIn={isLoggedIn}
1944
+ />
1893
1945
 
1894
1946
  {/* MenuWidget with user_id */}
1895
1947
  <MenuWidget
@@ -2613,36 +2613,44 @@ class MenuWidget extends React.Component {
2613
2613
  if (!layer || layer.type !== 'wmts') {
2614
2614
  return true;
2615
2615
  }
2616
+ const serviceData = layer.ViewService || layer.url || '';
2617
+ if (!serviceData) {
2618
+ return !isSceneViewActive;
2619
+ }
2616
2620
  const activeLayerData = layer.activeLayer || {};
2617
- const layerId = activeLayerData.id;
2618
- if (!layerId) {
2621
+ let layerId = activeLayerData.id;
2622
+
2623
+ this.xml = null;
2624
+ await this.getCapabilities(serviceData, 'WMTS');
2625
+ if (!this.xml) {
2619
2626
  return !isSceneViewActive;
2620
2627
  }
2628
+ const wmtsLayersData = this.parseWMTSLayers(this.xml);
2629
+ const selectedWmtsLayerData = this.resolveWmtsLayerData(
2630
+ wmtsLayersData,
2631
+ layerId ? { [layerId]: true } : {},
2632
+ );
2633
+ if (!selectedWmtsLayerData || !selectedWmtsLayerData.id) {
2634
+ return !isSceneViewActive;
2635
+ }
2636
+ layerId = selectedWmtsLayerData.id;
2637
+ const reconciledActiveLayerData = {
2638
+ id: selectedWmtsLayerData.id,
2639
+ title: selectedWmtsLayerData.title || selectedWmtsLayerData.id,
2640
+ };
2621
2641
 
2622
2642
  if (isSceneViewActive) {
2623
2643
  if (spatialReference) {
2624
2644
  layer.spatialReference = spatialReference;
2625
2645
  }
2626
- if (activeLayerData.tileMatrixSetId) {
2627
- const { tileMatrixSetId, ...sceneActiveLayerData } = activeLayerData;
2628
- layer.activeLayer = sceneActiveLayerData;
2629
- }
2646
+ layer.activeLayer = reconciledActiveLayerData;
2630
2647
  return true;
2631
2648
  }
2632
2649
 
2633
- const serviceData = layer.ViewService || layer.url || '';
2634
- if (!serviceData) {
2635
- return !isSceneViewActive;
2636
- }
2637
2650
  const cacheData = serviceData + '::' + layerId;
2638
2651
  let settingsData = this.wmtsSettingsData[cacheData];
2639
2652
 
2640
2653
  if (!settingsData) {
2641
- this.xml = null;
2642
- await this.getCapabilities(serviceData, 'WMTS');
2643
- if (!this.xml) {
2644
- return !isSceneViewActive;
2645
- }
2646
2654
  const tileMatrixSetId = this.resolveWmtsSettingsData(
2647
2655
  this.xml,
2648
2656
  layerId,
@@ -2658,15 +2666,98 @@ class MenuWidget extends React.Component {
2658
2666
 
2659
2667
  if (settingsData && settingsData.tileMatrixSetId) {
2660
2668
  layer.activeLayer = {
2661
- ...activeLayerData,
2669
+ ...reconciledActiveLayerData,
2662
2670
  tileMatrixSetId: settingsData.tileMatrixSetId,
2663
2671
  };
2664
2672
  return true;
2665
2673
  }
2666
2674
 
2675
+ layer.activeLayer = reconciledActiveLayerData;
2676
+
2667
2677
  return !isSceneViewActive;
2668
2678
  }
2669
2679
 
2680
+ resolveWmtsActiveLayerData(layerData) {
2681
+ if (!layerData || typeof layerData !== 'object') {
2682
+ return null;
2683
+ }
2684
+ const activeLayerData = layerData.activeLayer;
2685
+ if (!activeLayerData || typeof activeLayerData !== 'object') {
2686
+ return null;
2687
+ }
2688
+ const activeLayerId =
2689
+ typeof activeLayerData.id === 'string' ? activeLayerData.id.trim() : '';
2690
+ if (!activeLayerId) {
2691
+ return null;
2692
+ }
2693
+ const activeLayerTitle =
2694
+ typeof activeLayerData.title === 'string' && activeLayerData.title.trim()
2695
+ ? activeLayerData.title.trim()
2696
+ : activeLayerId;
2697
+ return {
2698
+ id: activeLayerId,
2699
+ title: activeLayerTitle,
2700
+ };
2701
+ }
2702
+
2703
+ refreshWmtsLayerData(layerId) {
2704
+ if (!layerId || !this.layers || !this.layers[layerId] || !WMTSLayer) {
2705
+ return false;
2706
+ }
2707
+ const currentLayerData = this.layers[layerId];
2708
+ if (currentLayerData.type !== 'wmts') {
2709
+ return false;
2710
+ }
2711
+ const activeLayerData = this.resolveWmtsActiveLayerData(currentLayerData);
2712
+ if (!activeLayerData) {
2713
+ return false;
2714
+ }
2715
+
2716
+ const nextLayerData = new WMTSLayer({
2717
+ id: currentLayerData.id || layerId,
2718
+ url: currentLayerData.url,
2719
+ title: currentLayerData.title || '',
2720
+ _wmtsTitle: currentLayerData._wmtsTitle,
2721
+ serviceMode: currentLayerData.serviceMode || 'KVP',
2722
+ spatialReference:
2723
+ this.view?.spatialReference || currentLayerData.spatialReference,
2724
+ activeLayer: activeLayerData,
2725
+ ViewService: currentLayerData.ViewService || currentLayerData.url,
2726
+ customLayerParameters: currentLayerData.customLayerParameters || {
2727
+ SHOWLOGO: false,
2728
+ },
2729
+ });
2730
+
2731
+ const customKeys = [
2732
+ 'isTimeSeries',
2733
+ 'fields',
2734
+ 'DatasetId',
2735
+ 'DatasetTitle',
2736
+ 'ProductId',
2737
+ 'StaticImageLegend',
2738
+ 'LayerTitle',
2739
+ 'DatasetDownloadInformation',
2740
+ 'datasetDownloadInformation',
2741
+ 'ServiceDataUrl',
2742
+ 'LayerId',
2743
+ 'featureInfoUrl',
2744
+ ];
2745
+
2746
+ customKeys.forEach((key) => {
2747
+ if (currentLayerData[key] !== undefined) {
2748
+ nextLayerData[key] = currentLayerData[key];
2749
+ }
2750
+ });
2751
+
2752
+ if (typeof currentLayerData.opacity === 'number') {
2753
+ nextLayerData.opacity = currentLayerData.opacity;
2754
+ }
2755
+ nextLayerData.visible = Boolean(currentLayerData.visible);
2756
+
2757
+ this.layers[layerId] = nextLayerData;
2758
+ return true;
2759
+ }
2760
+
2670
2761
  resolveRequestConfig(xml) {
2671
2762
  let doc = xml;
2672
2763
  try {
@@ -2785,7 +2876,26 @@ class MenuWidget extends React.Component {
2785
2876
  );
2786
2877
  }
2787
2878
 
2879
+ isSceneTilingError(error) {
2880
+ const errorName = String((error && error.name) || '').toLowerCase();
2881
+ const message = String((error && error.message) || '').toLowerCase();
2882
+ return (
2883
+ errorName.includes('layerview:no-compatible-tiling-scheme') ||
2884
+ message.includes('no-compatible-tiling-scheme') ||
2885
+ message.includes(
2886
+ 'none of the tiling schemes supported by the service are compatible with the scene',
2887
+ ) ||
2888
+ message.includes('wmts layer is not compatible with current sceneview')
2889
+ );
2890
+ }
2891
+
2788
2892
  resolveUploadErrorType(error) {
2893
+ if (this.isNoGeometryError(error)) {
2894
+ return 'noGeometryError';
2895
+ }
2896
+ if (this.isSceneTilingError(error)) {
2897
+ return 'sceneViewTilingError';
2898
+ }
2789
2899
  const detailsFromRoot =
2790
2900
  error && error.details && Array.isArray(error.details)
2791
2901
  ? error.details
@@ -3419,11 +3529,7 @@ class MenuWidget extends React.Component {
3419
3529
  resourceLayers[0].fullExtent = bboxData;
3420
3530
  }
3421
3531
  } catch (error) {
3422
- if (this.isNoGeometryError(error)) {
3423
- this.props.uploadFileErrorHandler('noGeometryError');
3424
- } else {
3425
- this.props.uploadFileErrorHandler();
3426
- }
3532
+ this.props.uploadFileErrorHandler(this.resolveUploadErrorType(error));
3427
3533
  return;
3428
3534
  }
3429
3535
 
@@ -3477,6 +3583,35 @@ class MenuWidget extends React.Component {
3477
3583
  resourceLayer.ViewService = (viewService || '').trim();
3478
3584
  }
3479
3585
 
3586
+ const isSceneUploadValidation =
3587
+ this.view &&
3588
+ this.view.type === '3d' &&
3589
+ (serviceType === 'WMS' || serviceType === 'WMTS');
3590
+
3591
+ if (isSceneUploadValidation) {
3592
+ try {
3593
+ this.map.add(resourceLayer);
3594
+ await this.view.whenLayerView(resourceLayer);
3595
+ } catch (error) {
3596
+ try {
3597
+ if (this.map.findLayerById(resourceLayer.id)) {
3598
+ this.map.remove(resourceLayer);
3599
+ }
3600
+ } catch (e) {}
3601
+ if (this.isSceneTilingError(error)) {
3602
+ this.props.uploadFileErrorHandler('sceneViewTilingError');
3603
+ continue;
3604
+ }
3605
+ throw error;
3606
+ }
3607
+
3608
+ try {
3609
+ if (this.map.findLayerById(resourceLayer.id)) {
3610
+ this.map.remove(resourceLayer);
3611
+ }
3612
+ } catch (e) {}
3613
+ }
3614
+
3480
3615
  const layerSignatureData = `${(
3481
3616
  resourceLayer.ViewService || ''
3482
3617
  ).trim()}::${resourceLayer.LayerId}`;
@@ -3512,11 +3647,7 @@ class MenuWidget extends React.Component {
3512
3647
 
3513
3648
  this.props.onServiceChange();
3514
3649
  } catch (error) {
3515
- if (this.isNoGeometryError(error)) {
3516
- this.props.uploadFileErrorHandler('noGeometryError');
3517
- } else {
3518
- this.props.uploadFileErrorHandler();
3519
- }
3650
+ this.props.uploadFileErrorHandler(this.resolveUploadErrorType(error));
3520
3651
  return;
3521
3652
  }
3522
3653
  }
@@ -3795,6 +3926,14 @@ class MenuWidget extends React.Component {
3795
3926
  const checkedLayers =
3796
3927
  JSON.parse(sessionStorage.getItem('checkedLayers')) || [];
3797
3928
  const isChecked = checkedLayers.includes(layer.LayerId);
3929
+ const serviceType =
3930
+ layer.type === 'wmts'
3931
+ ? 'WMTS'
3932
+ : layer.type === 'wms'
3933
+ ? 'WMS'
3934
+ : layer.type === 'geojson'
3935
+ ? 'WFS'
3936
+ : 'WMS';
3798
3937
 
3799
3938
  // Create a simplified object for storage
3800
3939
  return {
@@ -3816,9 +3955,18 @@ class MenuWidget extends React.Component {
3816
3955
  })),
3817
3956
  ViewService: layer.ViewService,
3818
3957
  LayerId: layer.LayerId,
3958
+ serviceType: serviceType,
3959
+ activeLayer:
3960
+ serviceType === 'WMTS' && layer.activeLayer
3961
+ ? {
3962
+ id: layer.activeLayer.id,
3963
+ title: layer.activeLayer.title,
3964
+ tileMatrixSetId: layer.activeLayer.tileMatrixSetId,
3965
+ }
3966
+ : null,
3819
3967
  visibility: layer.visible !== false,
3820
3968
  opacity: layer.opacity || 1,
3821
- checked: isChecked || layer.checked || false,
3969
+ checked: isChecked,
3822
3970
  };
3823
3971
  });
3824
3972
 
@@ -3839,26 +3987,65 @@ class MenuWidget extends React.Component {
3839
3987
  if (savedServices && Array.isArray(savedServices)) {
3840
3988
  // Process saved services to recreate actual layer objects
3841
3989
  const recreatedLayers = await Promise.all(
3842
- savedServices.map(async (serviceData) => {
3990
+ savedServices.map(async (serviceData, serviceIndex) => {
3843
3991
  try {
3844
- // Create a new WMSLayer with the saved properties
3845
- const newLayer = new WMSLayer(serviceData);
3992
+ const resolveServiceType =
3993
+ serviceData.serviceType ||
3994
+ (typeof serviceData.ViewService === 'string' &&
3995
+ serviceData.ViewService.toLowerCase().includes('wmts')
3996
+ ? 'WMTS'
3997
+ : 'WMS');
3998
+ const resolvedLayerId =
3999
+ serviceData.LayerId ||
4000
+ serviceData.id ||
4001
+ `user_service_${serviceIndex}`;
4002
+ const newLayer =
4003
+ resolveServiceType === 'WMTS'
4004
+ ? new WMTSLayer({
4005
+ id: resolvedLayerId,
4006
+ url: serviceData.url || serviceData.ViewService,
4007
+ title: serviceData.title || '',
4008
+ spatialReference: this.view?.spatialReference,
4009
+ serviceMode: 'KVP',
4010
+ activeLayer: serviceData.activeLayer || undefined,
4011
+ ViewService:
4012
+ serviceData.ViewService || serviceData.url || '',
4013
+ })
4014
+ : new WMSLayer({
4015
+ id: resolvedLayerId,
4016
+ url: serviceData.url || serviceData.ViewService,
4017
+ featureInfoFormat:
4018
+ serviceData.featureInfoFormat || 'text/html',
4019
+ featureInfoUrl:
4020
+ serviceData.featureInfoUrl ||
4021
+ serviceData.url ||
4022
+ serviceData.ViewService,
4023
+ title: serviceData.title || '',
4024
+ legendEnabled: serviceData.legendEnabled,
4025
+ sublayers: serviceData.sublayers,
4026
+ ViewService:
4027
+ serviceData.ViewService || serviceData.url || '',
4028
+ });
3846
4029
 
3847
4030
  // Set visibility property based on saved value
3848
4031
  newLayer.visible = serviceData.visibility !== false;
4032
+ newLayer.LayerId = resolvedLayerId;
4033
+ if (serviceData.description) {
4034
+ newLayer.description = serviceData.description;
4035
+ }
3849
4036
 
3850
4037
  // Remember the original checked state and visibility
3851
4038
  newLayer.checked = serviceData.checked;
3852
4039
 
3853
4040
  // Initialize visibleLayers for this layer
3854
4041
  if (!this.visibleLayers) this.visibleLayers = {};
3855
- this.visibleLayers[serviceData.LayerId] =
4042
+ this.visibleLayers[resolvedLayerId] =
3856
4043
  serviceData.visibility !== false
3857
4044
  ? ['fas', 'eye']
3858
4045
  : ['fas', 'eye-slash'];
3859
4046
 
3860
4047
  // Add to this.layers
3861
- this.layers[serviceData.LayerId] = newLayer;
4048
+ this.layers[resolvedLayerId] = newLayer;
3862
4049
  return newLayer;
3863
4050
  } catch (error) {
3864
4051
  return null;
@@ -3893,9 +4080,11 @@ class MenuWidget extends React.Component {
3893
4080
 
3894
4081
  // Then check the checkbox and call toggleLayer with a flag to preserve visibility
3895
4082
  node.checked = true;
4083
+ node.dataset.preserveCheckedLayerState = 'true';
3896
4084
 
3897
4085
  // Custom addition to toggleLayer to bypass visibility override
3898
4086
  this.toggleLayerWithoutVisibilityReset(node);
4087
+ delete node.dataset.preserveCheckedLayerState;
3899
4088
  }
3900
4089
  }
3901
4090
  });
@@ -4162,6 +4351,8 @@ class MenuWidget extends React.Component {
4162
4351
  this.state.wmsUserServiceLayers.find(
4163
4352
  (layer) => layer.LayerId === elem.id,
4164
4353
  ) || null;
4354
+ const preserveCheckedLayerState =
4355
+ elem.dataset && elem.dataset.preserveCheckedLayerState === 'true';
4165
4356
  if (elem.checked && !userService) {
4166
4357
  this.findCheckedDatasetNoServiceToVisualize(elem);
4167
4358
  }
@@ -4173,6 +4364,7 @@ class MenuWidget extends React.Component {
4173
4364
  this.layers[elem.id]?.type === 'wmts' ||
4174
4365
  layerViewService.toLowerCase().includes('wmts');
4175
4366
  if (elem.checked && evaluateWmtsLayer) {
4367
+ this.refreshWmtsLayerData(elem.id);
4176
4368
  const canApplyWmtsSettings = await this.applyWmtsSettingsData(
4177
4369
  this.layers[elem.id],
4178
4370
  this.view?.spatialReference,
@@ -4181,7 +4373,9 @@ class MenuWidget extends React.Component {
4181
4373
  if (!canApplyWmtsSettings) {
4182
4374
  elem.checked = false;
4183
4375
  this.layers[elem.id].visible = false;
4184
- this.deleteCheckedLayer(elem.id);
4376
+ if (!preserveCheckedLayerState) {
4377
+ this.deleteCheckedLayer(elem.id);
4378
+ }
4185
4379
  delete this.activeLayersJSON[elem.id];
4186
4380
  if (this.visibleLayers) {
4187
4381
  delete this.visibleLayers[elem.id];
@@ -4200,7 +4394,9 @@ class MenuWidget extends React.Component {
4200
4394
  try {
4201
4395
  await this.layers[elem.id].load();
4202
4396
  } catch (error) {
4203
- this.processUnsupportedWmtsLayer(elem);
4397
+ this.processUnsupportedWmtsLayer(elem, {
4398
+ preserveCheckedLayerState,
4399
+ });
4204
4400
  return;
4205
4401
  }
4206
4402
  }
@@ -4297,7 +4493,9 @@ class MenuWidget extends React.Component {
4297
4493
  try {
4298
4494
  await this.view.whenLayerView(this.layers[elem.id]);
4299
4495
  } catch (error) {
4300
- this.processUnsupportedWmtsLayer(elem);
4496
+ this.processUnsupportedWmtsLayer(elem, {
4497
+ preserveCheckedLayerState,
4498
+ });
4301
4499
  return;
4302
4500
  }
4303
4501
  }
@@ -4395,8 +4593,15 @@ class MenuWidget extends React.Component {
4395
4593
  let mapLayer = this.map.findLayerById(elem.id);
4396
4594
  if (mapLayer) {
4397
4595
  if (!userService) {
4398
- if (mapLayer.type && mapLayer.type !== 'base-tile') mapLayer.clear();
4399
- mapLayer.destroy();
4596
+ if (
4597
+ mapLayer.type &&
4598
+ mapLayer.type !== 'base-tile' &&
4599
+ mapLayer.type !== 'wmts'
4600
+ )
4601
+ mapLayer.clear();
4602
+ if (mapLayer.type !== 'wmts') {
4603
+ mapLayer.destroy();
4604
+ }
4400
4605
  this.map.remove(this.layers[elem.id]);
4401
4606
  } else {
4402
4607
  this.map.remove(mapLayer);
@@ -4429,13 +4634,16 @@ class MenuWidget extends React.Component {
4429
4634
  this.url = null;
4430
4635
  }
4431
4636
 
4432
- processUnsupportedWmtsLayer(elem) {
4637
+ processUnsupportedWmtsLayer(elem, options = {}) {
4638
+ const { preserveCheckedLayerState = false } = options;
4433
4639
  if (!elem || !this.layers || !this.layers[elem.id]) {
4434
4640
  return;
4435
4641
  }
4436
4642
  elem.checked = false;
4437
4643
  this.layers[elem.id].visible = false;
4438
- this.deleteCheckedLayer(elem.id);
4644
+ if (!preserveCheckedLayerState) {
4645
+ this.deleteCheckedLayer(elem.id);
4646
+ }
4439
4647
  const mapLayer = this.map ? this.map.findLayerById(elem.id) : null;
4440
4648
  if (mapLayer) {
4441
4649
  try {
@@ -4454,6 +4662,7 @@ class MenuWidget extends React.Component {
4454
4662
  if (!this.props.download && this.props.hotspotData) {
4455
4663
  this.activeLayersToHotspotData(elem.id);
4456
4664
  }
4665
+ this.props.uploadFileErrorHandler('sceneViewTilingError');
4457
4666
  this.renderHotspot();
4458
4667
  this.url = null;
4459
4668
  }
@@ -6795,7 +7004,9 @@ class MenuWidget extends React.Component {
6795
7004
  const node = document.getElementById(layer.LayerId);
6796
7005
  if (node) {
6797
7006
  node.checked = true;
7007
+ node.dataset.preserveCheckedLayerState = 'true';
6798
7008
  this.toggleLayer(node);
7009
+ delete node.dataset.preserveCheckedLayerState;
6799
7010
  }
6800
7011
  }
6801
7012
  });
@@ -7222,6 +7433,9 @@ class MenuWidget extends React.Component {
7222
7433
  for (var i = layers.length - 1; i >= 0; i--) {
7223
7434
  let layer = layers[i];
7224
7435
  let node = document.getElementById(layer);
7436
+ const restoreLayerState = this.state.wmsUserServiceLayers.some(
7437
+ (serviceLayer) => serviceLayer.LayerId === layer,
7438
+ );
7225
7439
 
7226
7440
  if (node) {
7227
7441
  if (!node.checked) {
@@ -7229,7 +7443,13 @@ class MenuWidget extends React.Component {
7229
7443
  // click event fires toggleLayer()
7230
7444
  //node.dispatchEvent(event);
7231
7445
  node.checked = true;
7446
+ if (restoreLayerState) {
7447
+ node.dataset.preserveCheckedLayerState = 'true';
7448
+ }
7232
7449
  this.toggleLayer(node);
7450
+ if (restoreLayerState) {
7451
+ delete node.dataset.preserveCheckedLayerState;
7452
+ }
7233
7453
  }
7234
7454
 
7235
7455
  // set scroll position
@@ -526,15 +526,15 @@ class UploadWidget extends React.Component {
526
526
 
527
527
  handleSelectLayers = async () => {
528
528
  const { serviceUrl, selectedServiceType } = this.state;
529
+ const trimmedServiceUrl = (serviceUrl || '').trim();
529
530
  if (
530
531
  selectedServiceType === 'WFS' &&
531
- serviceUrl &&
532
- serviceUrl.trim() !== '' &&
533
- this.isValidUrl(serviceUrl) &&
534
- this.isServiceTypeMatchingUrl(serviceUrl, selectedServiceType)
532
+ trimmedServiceUrl !== '' &&
533
+ this.isValidUrl(trimmedServiceUrl) &&
534
+ this.isServiceTypeMatchingUrl(trimmedServiceUrl, selectedServiceType)
535
535
  ) {
536
536
  const normalizedUrl = this.getNormalizedUrlForType(
537
- serviceUrl,
537
+ trimmedServiceUrl,
538
538
  selectedServiceType,
539
539
  );
540
540
  await this.getCapabilities(normalizedUrl, selectedServiceType);
@@ -549,17 +549,17 @@ class UploadWidget extends React.Component {
549
549
 
550
550
  handleUploadService = async () => {
551
551
  const serviceUrl = this.state.serviceUrl;
552
+ const trimmedServiceUrl = (serviceUrl || '').trim();
552
553
  const selectedServiceType = this.state.selectedServiceType;
553
554
  const selectedFeatures = this.state.selectedFeatures;
554
555
  if (
555
556
  selectedServiceType &&
556
- serviceUrl &&
557
- serviceUrl.trim() !== '' &&
558
- this.isValidUrl(serviceUrl) &&
559
- this.isServiceTypeMatchingUrl(serviceUrl, selectedServiceType)
557
+ trimmedServiceUrl !== '' &&
558
+ this.isValidUrl(trimmedServiceUrl) &&
559
+ this.isServiceTypeMatchingUrl(trimmedServiceUrl, selectedServiceType)
560
560
  ) {
561
561
  const normalizedUrl = this.getNormalizedUrlForType(
562
- serviceUrl,
562
+ trimmedServiceUrl,
563
563
  selectedServiceType,
564
564
  );
565
565
  const proxiedUrl = this.buildProxiedUrl(normalizedUrl);
@@ -1006,6 +1006,17 @@ class UploadWidget extends React.Component {
1006
1006
  </div>
1007
1007
  </>
1008
1008
  )}
1009
+ {this.state.infoPopupType === 'sceneViewTilingError' && (
1010
+ <>
1011
+ <span className="drawRectanglePopup-icon">
1012
+ <FontAwesomeIcon icon={['fas', 'info-circle']} />
1013
+ </span>
1014
+ <div className="drawRectanglePopup-text">
1015
+ Incorrect tiling scheme for scene view. The service can
1016
+ still be used in 2D map view.
1017
+ </div>
1018
+ </>
1019
+ )}
1009
1020
  </div>
1010
1021
  </div>
1011
1022
  </div>
@@ -103,9 +103,17 @@
103
103
 
104
104
  .viewmode-container {
105
105
  display: flex;
106
+ order: 9;
106
107
  padding: 0;
107
108
  }
108
109
 
110
+ .viewmode-floating-container {
111
+ position: absolute;
112
+ z-index: 2;
113
+ right: 10px;
114
+ bottom: 20px;
115
+ }
116
+
109
117
  .viewmode-button-group {
110
118
  display: flex;
111
119
  }
@@ -124,6 +132,31 @@
124
132
  border-left: 1px solid #dadada;
125
133
  }
126
134
 
135
+ .viewmode-toggle-button {
136
+ width: 36px;
137
+ height: 36px;
138
+ padding: 0;
139
+ border: none !important;
140
+ background-color: rgba(160, 177, 40, 0.8);
141
+ box-shadow: 0px 0px 4px 0px #dadada;
142
+ color: #fff;
143
+ font-size: 18px;
144
+ line-height: 1.1rem;
145
+ outline: none;
146
+ }
147
+
148
+ .viewmode-toggle-button:hover,
149
+ .viewmode-toggle-button:focus {
150
+ background-color: #7c8921;
151
+ }
152
+
153
+ @media only screen and (max-width: 767px) {
154
+ .viewmode-floating-container {
155
+ right: 10px;
156
+ bottom: 14px;
157
+ }
158
+ }
159
+
127
160
  /* Basemap */
128
161
  .basemap-container {
129
162
  display: flex;
@@ -1812,7 +1845,7 @@ input[type='range']::-ms-track {
1812
1845
  position: absolute;
1813
1846
  display: flex;
1814
1847
  width: 19rem;
1815
- height: 60px;
1848
+ min-height: 60px;
1816
1849
  /* justify-content: space-around; */
1817
1850
  padding: 0.6rem 0.8rem 0.8rem 0.8rem;
1818
1851
  background-color: white;
@@ -1831,6 +1864,9 @@ input[type='range']::-ms-track {
1831
1864
  /* flex: auto; */
1832
1865
  font-family: 'Lato', sans-serif;
1833
1866
  font-size: 0.875rem;
1867
+ line-height: 1.3;
1868
+ overflow-wrap: anywhere;
1869
+ white-space: normal;
1834
1870
  }
1835
1871
 
1836
1872
  div.map-container.popup-block {