@geogirafe/lib-geoportal 1.2.0-dev.2813242211 → 1.2.0-dev.2829121016

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.
Files changed (85) hide show
  1. package/api/apicontext.d.ts +4 -0
  2. package/api/apicontext.js +8 -0
  3. package/api/apigeogirafeapp.js +7 -5
  4. package/api/style.css +18 -1
  5. package/assets/i18n/de.json +1 -0
  6. package/assets/i18n/en.json +1 -0
  7. package/assets/i18n/fr.json +1 -0
  8. package/assets/i18n/it.json +1 -0
  9. package/assets/icons/theme-checked.svg +1 -0
  10. package/components/drawing/component.d.ts +9 -2
  11. package/components/drawing/component.js +27 -10
  12. package/components/drawing-toolbar/component.js +2 -2
  13. package/components/extlayers/component.d.ts +0 -1
  14. package/components/extlayers/component.js +1 -6
  15. package/components/main.d.ts +3 -1
  16. package/components/main.js +3 -1
  17. package/components/{map → map-2d}/component.d.ts +9 -24
  18. package/components/{map → map-2d}/component.js +28 -260
  19. package/components/map-2d/deprecatedmapcomponent.d.ts +15 -0
  20. package/components/map-2d/deprecatedmapcomponent.js +24 -0
  21. package/components/{map → map-2d}/style.css +0 -30
  22. package/components/{map → map-2d}/tools/swipemanager.d.ts +1 -1
  23. package/components/map-3d/component.d.ts +50 -0
  24. package/components/map-3d/component.js +139 -0
  25. package/components/map-3d/style.css +34 -0
  26. package/components/map-3d/tools/globe.d.ts +42 -0
  27. package/components/map-3d/tools/globe.js +198 -0
  28. package/components/search/component.d.ts +5 -0
  29. package/components/search/component.js +71 -19
  30. package/components/search/style.css +36 -9
  31. package/components/selectiontool/component.d.ts +2 -2
  32. package/components/selectiontool/component.js +2 -2
  33. package/components/treeview/style.css +4 -0
  34. package/components/treeview/treeviewgroup/component.js +1 -1
  35. package/components/treeview/treeviewitem/component.js +1 -1
  36. package/components/treeview/treeviewtheme/component.d.ts +2 -0
  37. package/components/treeview/treeviewtheme/component.js +18 -3
  38. package/package.json +1 -1
  39. package/styles/iframe.css +18 -1
  40. package/styles/index.css +18 -1
  41. package/styles/mobile.css +18 -1
  42. package/templates/iframe.html +4 -1
  43. package/templates/index.html +4 -1
  44. package/templates/mobile.html +4 -1
  45. package/templates/public/about.json +1 -1
  46. package/templates/vite.config.js +43 -25
  47. package/tools/app/geogirafeapp-mobile.js +7 -2
  48. package/tools/app/geogirafeapp.js +7 -2
  49. package/tools/context/context.d.ts +4 -0
  50. package/tools/context/context.js +8 -0
  51. package/tools/context/icontext.d.ts +4 -0
  52. package/tools/drawing/cesiumDrawing.d.ts +9 -5
  53. package/tools/drawing/cesiumDrawing.js +8 -8
  54. package/tools/drawing/olDrawing.d.ts +2 -2
  55. package/tools/drawing/olDrawing.js +1 -1
  56. package/tools/main.d.ts +2 -0
  57. package/tools/main.js +2 -0
  58. package/tools/selection/selectionmanager.d.ts +19 -0
  59. package/tools/selection/selectionmanager.js +80 -0
  60. package/tools/state/componentManager.d.ts +1 -1
  61. package/tools/tests/mockcontext.d.ts +4 -0
  62. package/tools/tests/mockcontext.js +8 -0
  63. package/tools/themes/themeshelper.d.ts +44 -1
  64. package/tools/themes/themeshelper.js +149 -16
  65. package/{components/map/tools → tools/wmts}/wmtsmanager.d.ts +4 -7
  66. package/{components/map/tools → tools/wmts}/wmtsmanager.js +7 -10
  67. /package/components/{map → map-2d}/tools/cogmanager.d.ts +0 -0
  68. /package/components/{map → map-2d}/tools/cogmanager.js +0 -0
  69. /package/components/{map → map-2d}/tools/contextmenu.d.ts +0 -0
  70. /package/components/{map → map-2d}/tools/contextmenu.js +0 -0
  71. /package/components/{map → map-2d}/tools/drawingmanager.d.ts +0 -0
  72. /package/components/{map → map-2d}/tools/drawingmanager.js +0 -0
  73. /package/components/{map → map-2d}/tools/focusfeature.d.ts +0 -0
  74. /package/components/{map → map-2d}/tools/focusfeature.js +0 -0
  75. /package/components/{map → map-2d}/tools/osmmanager.d.ts +0 -0
  76. /package/components/{map → map-2d}/tools/osmmanager.js +0 -0
  77. /package/components/{map → map-2d}/tools/swipemanager.js +0 -0
  78. /package/components/{map → map-2d}/tools/vectortilesmanager.d.ts +0 -0
  79. /package/components/{map → map-2d}/tools/vectortilesmanager.js +0 -0
  80. /package/components/{map → map-2d}/tools/viewmanager.d.ts +0 -0
  81. /package/components/{map → map-2d}/tools/viewmanager.js +0 -0
  82. /package/components/{map → map-2d}/tools/xyzmanager.d.ts +0 -0
  83. /package/components/{map → map-2d}/tools/xyzmanager.js +0 -0
  84. /package/components/{map → map-3d}/tools/wmsmanager3d.d.ts +0 -0
  85. /package/components/{map → map-3d}/tools/wmsmanager3d.js +0 -0
@@ -26,17 +26,17 @@ export default class CesiumDrawing {
26
26
  get config() {
27
27
  return this.context.configManager.Config;
28
28
  }
29
+ /**
30
+ * The 3D Map has to be loaded before creating a CesiumDrawing.
31
+ * This class is loaded on demand, so that Cesium stays out of the initial bundle.
32
+ */
29
33
  constructor(map, stateLocation, context) {
30
34
  this.stateLocation = stateLocation;
31
35
  this.context = context;
32
- map.subscribe('globe.loaded', () => {
33
- if (this.state.globe.loaded) {
34
- this.scene = map.map3d.getCesiumScene();
35
- this.handler = new Cesium.ScreenSpaceEventHandler(this.scene.canvas);
36
- this.entities = map.map3d.getDataSourceDisplay().defaultDataSource.entities;
37
- map.subscribe(`extendedState.${this.stateLocation}.activeTool`, (_oldTool, newTool) => newTool === null ? this.deactivateTool() : this.activateTool(newTool));
38
- }
39
- });
36
+ this.scene = map.map3d.getCesiumScene();
37
+ this.handler = new Cesium.ScreenSpaceEventHandler(this.scene.canvas);
38
+ this.entities = map.map3d.getDataSourceDisplay().defaultDataSource.entities;
39
+ map.subscribe(`extendedState.${this.stateLocation}.activeTool`, (_oldTool, newTool) => newTool === null ? this.deactivateTool() : this.activateTool(newTool));
40
40
  }
41
41
  setFixedLineLength(length) {
42
42
  this.fixedLineLength = Number.isNaN(length) ? 0 : length;
@@ -1,5 +1,5 @@
1
1
  import DrawingFeature from './drawingFeature.js';
2
- import MapComponent from '../../components/map/component.js';
2
+ import Map2dComponent from '../../components/map-2d/component.js';
3
3
  import { Feature } from 'ol';
4
4
  import { Geometry } from 'ol/geom.js';
5
5
  import VectorSource from 'ol/source/Vector.js';
@@ -36,7 +36,7 @@ export default class OlDrawing {
36
36
  private get state();
37
37
  private get drawingState();
38
38
  private get config();
39
- constructor(mapComponent: MapComponent, stateLocation: string, context: IGirafeContext, deleteHandler: (featureId: string) => void);
39
+ constructor(mapComponent: Map2dComponent, stateLocation: string, context: IGirafeContext, deleteHandler: (featureId: string) => void);
40
40
  private addModifyInteraction;
41
41
  private addSnapInteraction;
42
42
  private addTranslateInteraction;
@@ -11,7 +11,7 @@ import { getPointResolution } from 'ol/proj.js';
11
11
  import { always, never, platformModifierKeyOnly, primaryAction } from 'ol/events/condition.js';
12
12
  import { unByKey } from 'ol/Observable.js';
13
13
  import { ensurePolygonIsProperlyClosed, getAreaOfPolygon, getDistance, getHalfPoint, getLabelStyle, getLengthAsMetricText, getRadiusDataForCircle, unByKeyAll } from '../utils/olutils.js';
14
- import { ContextMenu, EntryInteractionType } from '../../components/map/tools/contextmenu.js';
14
+ import { ContextMenu, EntryInteractionType } from '../../components/map-2d/tools/contextmenu.js';
15
15
  import { formatCoordinates } from '../geometrytools.js';
16
16
  import { v4 as uuidv4 } from 'uuid';
17
17
  import { isAlternateMouseClick, isPrimaryPointerAction } from '../state/userinteractionevent.js';
package/tools/main.d.ts CHANGED
@@ -65,6 +65,7 @@ export type { GmfSearchConfig } from './search/gmfsearchclient.js';
65
65
  export { default as GmfSearchClient } from './search/gmfsearchclient.js';
66
66
  export type { SearchProviderConfig } from './search/searchmanager.js';
67
67
  export { default as SearchManager } from './search/searchmanager.js';
68
+ export { default as SelectionManager } from './selection/selectionmanager.js';
68
69
  export type { default as ISessionManager } from './share/isessionmanager.js';
69
70
  export type { UrlShortenerResponse, IUrlShortener } from './share/iurlshortener.js';
70
71
  export { default as ActiveBasemapsSerializer } from './share/serializers/activebasemapsserializer.js';
@@ -148,3 +149,4 @@ export { default as WfsParser } from './wfs/wfsparser.js';
148
149
  export { default as WmsClient, WmsClientQgis, WmsClientMapServer, WmsClientDefault, WmsClientGeoServer } from './wms/wmsclient.js';
149
150
  export { default as WmsLegendHelper } from './wms/wmslegendhelper.js';
150
151
  export { default as WmsManager } from './wms/wmsmanager.js';
152
+ export { default as WmtsManager } from './wmts/wmtsmanager.js';
package/tools/main.js CHANGED
@@ -52,6 +52,7 @@ export { default as AbstractSearchClient, SEARCH_PROVIDER_DEFAULT, SEARCH_GROUP_
52
52
  export { default as GeoAdminChSearchClient } from './search/geoadminchsearchclient.js';
53
53
  export { default as GmfSearchClient } from './search/gmfsearchclient.js';
54
54
  export { default as SearchManager } from './search/searchmanager.js';
55
+ export { default as SelectionManager } from './selection/selectionmanager.js';
55
56
  export { default as ActiveBasemapsSerializer } from './share/serializers/activebasemapsserializer.js';
56
57
  export { default as CustomLayersSerializer } from './share/serializers/customlayersserializer.js';
57
58
  export { default as GlobeSerializer } from './share/serializers/globeserializer.js';
@@ -113,3 +114,4 @@ export { default as WfsParser } from './wfs/wfsparser.js';
113
114
  export { default as WmsClient, WmsClientQgis, WmsClientMapServer, WmsClientDefault, WmsClientGeoServer } from './wms/wmsclient.js';
114
115
  export { default as WmsLegendHelper } from './wms/wmslegendhelper.js';
115
116
  export { default as WmsManager } from './wms/wmsmanager.js';
117
+ export { default as WmtsManager } from './wmts/wmtsmanager.js';
@@ -0,0 +1,19 @@
1
+ import GirafeSingleton from '../../base/GirafeSingleton.js';
2
+ /**
3
+ * Handles the feature selection, independently from the map (2D or 3D) the selection was triggered from.
4
+ * The selection is done in two steps:
5
+ * 1. `select()` asks every layer manager to add its own <code>SelectionParam</code> to the state.
6
+ * 2. The change of <code>selection.selectionParameters</code> triggers the effective query
7
+ * (WMS GetFeatureInfo and WFS GetFeature) and writes the resulting features into the state.
8
+ */
9
+ export default class SelectionManager extends GirafeSingleton {
10
+ private get state();
11
+ initializeSingleton(): void;
12
+ /**
13
+ * Selects all the features of the queryable layers that are inside the given extent.
14
+ * Layers selectable today are WMS, WMTS (with wms layer) and Local files.
15
+ */
16
+ select(extent: number[]): void;
17
+ selectNone(): void;
18
+ private onSelectFeatures;
19
+ }
@@ -0,0 +1,80 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import GirafeSingleton from '../../base/GirafeSingleton.js';
3
+ import { SelectionMode } from '../../models/selection.js';
4
+ import { applyFeaturesToSelection } from '../utils/utils.js';
5
+ /**
6
+ * Handles the feature selection, independently from the map (2D or 3D) the selection was triggered from.
7
+ * The selection is done in two steps:
8
+ * 1. `select()` asks every layer manager to add its own <code>SelectionParam</code> to the state.
9
+ * 2. The change of <code>selection.selectionParameters</code> triggers the effective query
10
+ * (WMS GetFeatureInfo and WFS GetFeature) and writes the resulting features into the state.
11
+ */
12
+ export default class SelectionManager extends GirafeSingleton {
13
+ get state() {
14
+ return this.context.stateManager.state;
15
+ }
16
+ initializeSingleton() {
17
+ this.context.stateManager.subscribe('selection.selectionParameters', (_, newParams) => {
18
+ // The querying is asynchronous and nobody awaits it here,
19
+ // so a failing request must not end up in an unhandled rejection.
20
+ this.onSelectFeatures(newParams).catch((error) => console.error('Could not select the features.', error));
21
+ });
22
+ }
23
+ /**
24
+ * Selects all the features of the queryable layers that are inside the given extent.
25
+ * Layers selectable today are WMS, WMTS (with wms layer) and Local files.
26
+ */
27
+ select(extent) {
28
+ // Reset current selection if SelectionMode is Replace
29
+ if (this.state.selection.selectionMode === SelectionMode.Replace) {
30
+ this.selectNone();
31
+ }
32
+ else if (this.state.selection.selectionMode === SelectionMode.Add) {
33
+ this.state.selection.selectionParameters = [];
34
+ }
35
+ // Use batch for changes to prevent multiple selection of objects
36
+ this.context.stateManager.batchChanges(() => {
37
+ this.context.wmsManager.selectFeatures(extent);
38
+ this.context.wmtsManager.selectFeatures(extent);
39
+ this.context.localFileManager.selectFeatures(extent);
40
+ });
41
+ }
42
+ selectNone() {
43
+ this.state.selection.selectedFeatures = [];
44
+ this.state.selection.selectionParameters = [];
45
+ this.state.selection.highlightedFeatures = [];
46
+ }
47
+ async onSelectFeatures(selectionParams) {
48
+ if (selectionParams.length === 0) {
49
+ return;
50
+ }
51
+ this.state.loading = true;
52
+ try {
53
+ // WMS GetFeatureInfo
54
+ const wmsPromises = selectionParams.map((param) => {
55
+ const wmsGetFeatureInfoSelectionParam = param.clone((l) => l.queryable && l.wmsQueryableOnly);
56
+ const client = this.context.wmsManager.getClient(wmsGetFeatureInfoSelectionParam.ogcServer);
57
+ return client.getFeatureInfo(wmsGetFeatureInfoSelectionParam);
58
+ });
59
+ // WFS GetFeature
60
+ const wfsPromises = selectionParams.map((param) => {
61
+ const wfsGetFeatureInfoSelectionParam = param.clone((l) => l.wfsQueryable);
62
+ const client = this.context.wfsManager.getClient(wfsGetFeatureInfoSelectionParam.ogcServer);
63
+ const features = client.getFeature(wfsGetFeatureInfoSelectionParam);
64
+ return features;
65
+ });
66
+ const wmsGmlFeatures = (await Promise.all(wmsPromises)).flat();
67
+ const wfsGmlFeatures = (await Promise.all(wfsPromises)).flat();
68
+ const gmlFeatures = [...wmsGmlFeatures, ...wfsGmlFeatures];
69
+ if (gmlFeatures.length === 0 && this.state.selection.selectedFeatures.length === 0) {
70
+ this.state.interface.selectionComponentVisible = false;
71
+ }
72
+ else {
73
+ applyFeaturesToSelection(gmlFeatures, this.state);
74
+ }
75
+ }
76
+ finally {
77
+ this.state.loading = false;
78
+ }
79
+ }
80
+ }
@@ -1,7 +1,7 @@
1
1
  import GirafeHTMLElement from '../../base/GirafeHTMLElement.js';
2
2
  import GirafeSingleton from '../../base/GirafeSingleton.js';
3
3
  type Callback = (elem: GirafeHTMLElement) => unknown;
4
- type Constructor<T> = new (...args: unknown[]) => T;
4
+ type Constructor<T> = new (...args: never[]) => T;
5
5
  export default class ComponentManager extends GirafeSingleton {
6
6
  private readonly components;
7
7
  private readonly registeredCallbacks;
@@ -29,6 +29,8 @@ import UrlManager from '../url/urlmanager';
29
29
  import UserDataManager from '../userdata/userdatamanager';
30
30
  import WfsManager from '../wfs/wfsmanager';
31
31
  import WmsManager from '../wms/wmsmanager';
32
+ import WmtsManager from '../wmts/wmtsmanager';
33
+ import SelectionManager from '../selection/selectionmanager';
32
34
  import OnBoardingManager from '../onboarding/onboardingmanager';
33
35
  import ThemeFavoritesManager from '../themes/themefavoritesmanager';
34
36
  import SearchManager from '../search/searchmanager';
@@ -62,6 +64,8 @@ export default class MockGirafeContext implements IGirafeContext {
62
64
  readonly orderingManager: OrderingManager;
63
65
  readonly userLayerManager: UserLayerManager;
64
66
  readonly wmsManager: WmsManager;
67
+ readonly wmtsManager: WmtsManager;
68
+ readonly selectionManager: SelectionManager;
65
69
  readonly ogcApiFeaturesManager: OgcApiFeaturesManager;
66
70
  readonly localFileManager: LocalFileManager;
67
71
  readonly onBoardingManager: OnBoardingManager;
@@ -30,6 +30,8 @@ import UrlManager from '../url/urlmanager';
30
30
  import UserDataManager from '../userdata/userdatamanager';
31
31
  import WfsManager from '../wfs/wfsmanager';
32
32
  import WmsManager from '../wms/wmsmanager';
33
+ import WmtsManager from '../wmts/wmtsmanager';
34
+ import SelectionManager from '../selection/selectionmanager';
33
35
  import { MockConfig } from './mockconfig';
34
36
  import OnBoardingManager from '../onboarding/onboardingmanager';
35
37
  import ThemeFavoritesManager from '../themes/themefavoritesmanager';
@@ -64,6 +66,8 @@ export default class MockGirafeContext {
64
66
  orderingManager;
65
67
  userLayerManager;
66
68
  wmsManager;
69
+ wmtsManager;
70
+ selectionManager;
67
71
  ogcApiFeaturesManager;
68
72
  localFileManager;
69
73
  onBoardingManager;
@@ -99,6 +103,8 @@ export default class MockGirafeContext {
99
103
  this.customThemesManager = new CustomThemesManager(this);
100
104
  this.themesManager = new ThemesManager(this);
101
105
  this.wmsManager = new WmsManager(this);
106
+ this.wmtsManager = new WmtsManager(this);
107
+ this.selectionManager = new SelectionManager(this);
102
108
  this.localFileManager = new LocalFileManager(this);
103
109
  this.ogcApiFeaturesManager = new OgcApiFeaturesManager(this);
104
110
  this.onBoardingManager = new OnBoardingManager(this);
@@ -143,7 +149,9 @@ export default class MockGirafeContext {
143
149
  this.customThemesManager.initializeSingleton();
144
150
  this.themesManager.initializeSingleton();
145
151
  this.wmsManager.initializeSingleton();
152
+ this.wmtsManager.initializeSingleton();
146
153
  this.localFileManager.initializeSingleton();
154
+ this.selectionManager.initializeSingleton();
147
155
  this.ogcApiFeaturesManager.initializeSingleton();
148
156
  this.onBoardingManager.initializeSingleton();
149
157
  this.themeFavoritesManager.initializeSingleton();
@@ -11,13 +11,48 @@ export default class ThemesHelper extends GirafeSingleton {
11
11
  initializeSingleton(): void;
12
12
  private get state();
13
13
  findBaseLayerById(layerId: number): BaseLayer;
14
- private findBaseLayerRecursiveById;
14
+ findBaseLayerRecursiveById(layers: BaseLayer[], layerId: number): BaseLayer | null;
15
15
  findThemeByName(themename: string): ThemeLayer | null;
16
16
  findGroupByName(groupname: string): GroupLayer | null;
17
17
  findLayerByName(layername: string): Layer | null;
18
18
  private findBaseLayerByName;
19
19
  private findLayerRecursive;
20
+ /**
21
+ * @param layer any layer or layer group (or theme) to find the related theme.
22
+ * @returns The original theme configuration.
23
+ */
24
+ getOriginalTheme(layer: BaseLayer): ThemeLayer | null;
25
+ /**
26
+ * Count the layer in both the given theme and the matching original theme.
27
+ * @param theme the theme to check.
28
+ * @returns true if the count is the same. False otherwise.
29
+ */
30
+ isThemeComplete(theme: ThemeLayer): boolean;
31
+ /**
32
+ * @return the number of all sublayers (recursively) of the given layer.
33
+ * @private
34
+ */
35
+ private countLayer;
36
+ /**
37
+ * Add the related theme if it's not already (even partially) in the layertree. If it's
38
+ * already in it, don't add it, warn the user, and highlight it.
39
+ * @returns true if the related theme is added to the layertree, false otherwise.
40
+ */
41
+ addThemeIfNotPresent(theme: ThemeLayer): boolean;
42
+ /**
43
+ * Add the missing layers of an in-use theme, from an original theme description.
44
+ * The added layers are deactivated.
45
+ * @param originalTheme a Theme object, or model, as initially defined.
46
+ */
47
+ completeTheme(originalTheme: ThemeLayer): void;
48
+ /**
49
+ * Deactivate all layers and sublayer recursively.
50
+ * Meaning state="off" and isDefaultChecked=false.
51
+ * @private
52
+ */
53
+ private deactivateAllLayers;
20
54
  private onSelectedThemeChanged;
55
+ private getThemeInLayersList;
21
56
  private onThemeChanged;
22
57
  getMinimalClonedThemeForLayer(layer: BaseLayer): ThemeLayer;
23
58
  private removeOthers;
@@ -50,6 +85,14 @@ export default class ThemesHelper extends GirafeSingleton {
50
85
  private addLayerToLayerTree;
51
86
  removeLayersFromLayerTree(layersToRemove: BaseLayer[]): void;
52
87
  private removeLayersFromExistingLayerTree;
88
+ /**
89
+ * Find the initial position of the layer (relative to its sibling layers) by looking
90
+ * at the original theme object and insert the layer following this original order.
91
+ * (The "layer.order" value is incremented with the user operations, that's why we can
92
+ * rely on it for new layers and must adapt it to the existing sibling layers.)
93
+ * @private
94
+ */
95
+ private insertLayerToLayerTreeAtInitialPosition;
53
96
  /**
54
97
  * Removes all layers from the layer tree except themes that are pinned.
55
98
  */
@@ -93,6 +93,97 @@ export default class ThemesHelper extends GirafeSingleton {
93
93
  }
94
94
  return null;
95
95
  }
96
+ /**
97
+ * @param layer any layer or layer group (or theme) to find the related theme.
98
+ * @returns The original theme configuration.
99
+ */
100
+ getOriginalTheme(layer) {
101
+ const hierarchy = this.getHierarchyFromLayer(layer);
102
+ const theme = hierarchy?.length ? hierarchy[0] : undefined;
103
+ return theme ? this.findThemeByName(theme.name) : null;
104
+ }
105
+ /**
106
+ * Count the layer in both the given theme and the matching original theme.
107
+ * @param theme the theme to check.
108
+ * @returns true if the count is the same. False otherwise.
109
+ */
110
+ isThemeComplete(theme) {
111
+ const originalTheme = this.getOriginalTheme(theme);
112
+ if (!originalTheme) {
113
+ return false;
114
+ }
115
+ const themeCount = this.countLayer(theme);
116
+ const originalThemeCount = this.countLayer(originalTheme);
117
+ return themeCount === originalThemeCount;
118
+ }
119
+ /**
120
+ * @return the number of all sublayers (recursively) of the given layer.
121
+ * @private
122
+ */
123
+ countLayer(layer) {
124
+ let count = 1;
125
+ const layerAsUnknownGroup = layer;
126
+ if (layerAsUnknownGroup.children) {
127
+ layerAsUnknownGroup.children.forEach((child) => {
128
+ count += this.countLayer(child);
129
+ });
130
+ }
131
+ return count;
132
+ }
133
+ /**
134
+ * Add the related theme if it's not already (even partially) in the layertree. If it's
135
+ * already in it, don't add it, warn the user, and highlight it.
136
+ * @returns true if the related theme is added to the layertree, false otherwise.
137
+ */
138
+ addThemeIfNotPresent(theme) {
139
+ const themeInLayersList = this.getThemeInLayersList(theme);
140
+ // The theme is not yet added, add it!
141
+ if (themeInLayersList === undefined) {
142
+ if (this.state.themes.lastSelectedTheme?.id === theme.id) {
143
+ this.state.themes.lastSelectedTheme = null;
144
+ }
145
+ this.state.themes.lastSelectedTheme = theme;
146
+ return true;
147
+ }
148
+ // The theme is in the layer tree, then warn the user.
149
+ themeInLayersList.isHighlighted = true;
150
+ const layerName = this.context.i18nManager.getTranslation(theme.name);
151
+ const msg = this.context.i18nManager
152
+ .getTranslation('The theme {name} is already present in the treeview.')
153
+ .replace('{name}', layerName);
154
+ this.state.infobox.elements.push({
155
+ id: theme.treeItemId,
156
+ text: msg,
157
+ type: 'info',
158
+ duration: 5000
159
+ });
160
+ return false;
161
+ }
162
+ /**
163
+ * Add the missing layers of an in-use theme, from an original theme description.
164
+ * The added layers are deactivated.
165
+ * @param originalTheme a Theme object, or model, as initially defined.
166
+ */
167
+ completeTheme(originalTheme) {
168
+ const clonedTheme = originalTheme.clone();
169
+ this.deactivateAllLayers(clonedTheme);
170
+ this.mergeLayerWithExistingLayerTree(clonedTheme, this.state.layers.layersList, false);
171
+ }
172
+ /**
173
+ * Deactivate all layers and sublayer recursively.
174
+ * Meaning state="off" and isDefaultChecked=false.
175
+ * @private
176
+ */
177
+ deactivateAllLayers(layer) {
178
+ layer.activeState = 'off';
179
+ layer.isDefaultChecked = false;
180
+ const layerAsUnknownGroup = layer;
181
+ if (layerAsUnknownGroup.children) {
182
+ layerAsUnknownGroup.children.forEach((child) => {
183
+ this.deactivateAllLayers(child);
184
+ });
185
+ }
186
+ }
96
187
  onSelectedThemeChanged(theme) {
97
188
  if (!theme) {
98
189
  // Theme is null, nothing to do here
@@ -108,7 +199,11 @@ export default class ThemesHelper extends GirafeSingleton {
108
199
  this.onThemeChanged(theme);
109
200
  }
110
201
  }
202
+ getThemeInLayersList(theme) {
203
+ return this.state.layers.layersList.find((l) => l.id == theme.id);
204
+ }
111
205
  onThemeChanged(theme) {
206
+ const themeInLayersList = this.getThemeInLayersList(theme);
112
207
  // Create a clone of the theme object to use it in the treeview.
113
208
  // This is essential, otherwise all changes are done in the layers
114
209
  // (For example, when expanding legend, expanding a group, or activating the layer).
@@ -116,20 +211,10 @@ export default class ThemesHelper extends GirafeSingleton {
116
211
  // And when a theme is selected aging from the themes-selector
117
212
  // The default configuration will have been overwritten.
118
213
  const clonedTheme = theme.clone();
119
- const themeAlreadyInLayersList = this.state.layers.layersList.find((l) => l.id == clonedTheme.id);
120
- if (themeAlreadyInLayersList) {
121
- themeAlreadyInLayersList.isHighlighted = true;
122
- const layername = this.context.i18nManager.getTranslation(clonedTheme.name);
123
- const msg = this.context.i18nManager
124
- .getTranslation('The theme {name} is already present in the treeview.')
125
- .replace('{name}', layername);
126
- this.state.infobox.elements.push({
127
- id: theme.treeItemId,
128
- text: msg,
129
- type: 'info',
130
- duration: 5000
131
- });
132
- return;
214
+ // Delete (replace) this theme if it already exists.
215
+ if (themeInLayersList !== undefined) {
216
+ this.context.stateManager.batchChanges(() => this.removeLayersFromLayerTree([themeInLayersList]));
217
+ clonedTheme.order = themeInLayersList.order;
133
218
  }
134
219
  if (this.context.configManager.Config.themes.selectionMode === 'replace') {
135
220
  // Mode is <replace>
@@ -138,7 +223,6 @@ export default class ThemesHelper extends GirafeSingleton {
138
223
  }
139
224
  else {
140
225
  // Mode is <add>
141
- clonedTheme.order = this.getInitialOrderForNewTheme();
142
226
  this.state.layers.layersList.push(clonedTheme);
143
227
  }
144
228
  const themeFunctionalities = this.state.themes._allFunctionalities[theme.id];
@@ -384,7 +468,7 @@ export default class ThemesHelper extends GirafeSingleton {
384
468
  if (parent) {
385
469
  newLayer.parent = parent;
386
470
  }
387
- existingList.push(newLayer);
471
+ this.insertLayerToLayerTreeAtInitialPosition(newLayer, existingList);
388
472
  if (activate) {
389
473
  this.context.layerManager.toggle(newLayer, 'on');
390
474
  }
@@ -411,6 +495,55 @@ export default class ThemesHelper extends GirafeSingleton {
411
495
  }
412
496
  }
413
497
  }
498
+ /**
499
+ * Find the initial position of the layer (relative to its sibling layers) by looking
500
+ * at the original theme object and insert the layer following this original order.
501
+ * (The "layer.order" value is incremented with the user operations, that's why we can
502
+ * rely on it for new layers and must adapt it to the existing sibling layers.)
503
+ * @private
504
+ */
505
+ insertLayerToLayerTreeAtInitialPosition(newLayer, existingList) {
506
+ const originalLayer = this.findBaseLayerById(newLayer.id);
507
+ const originalParent = originalLayer.parent;
508
+ if (!originalParent || existingList.length === 0) {
509
+ existingList.push(newLayer);
510
+ return;
511
+ }
512
+ // Following the original order, find the nearest previous or next sibling layer in the existingList.
513
+ const originalSiblings = originalParent.children;
514
+ const existingListIds = new Set(existingList.map((layer) => layer.id));
515
+ let previousOriginalSibling;
516
+ let nextOriginalSibling;
517
+ let originalLayerEncountered = false;
518
+ originalSiblings.some((oLayer) => {
519
+ if (originalLayerEncountered && (previousOriginalSibling || nextOriginalSibling)) {
520
+ return true;
521
+ }
522
+ if (oLayer.id === newLayer.id) {
523
+ originalLayerEncountered = true;
524
+ }
525
+ else if (existingListIds.has(oLayer.id)) {
526
+ if (originalLayerEncountered) {
527
+ nextOriginalSibling = oLayer;
528
+ }
529
+ else {
530
+ previousOriginalSibling = oLayer;
531
+ }
532
+ }
533
+ });
534
+ // Find back the matching sibling layer in the ExistingList and update the "newLayer.order"
535
+ // to be just before or just after the sibling layer.
536
+ if (previousOriginalSibling) {
537
+ const previousSibling = existingList.find((layer) => layer.id === previousOriginalSibling?.id);
538
+ newLayer.order = previousSibling?.order ? previousSibling.order : newLayer.order;
539
+ }
540
+ else if (nextOriginalSibling) {
541
+ const nextSibling = existingList.find((layer) => layer.id === nextOriginalSibling?.id);
542
+ newLayer.order = nextSibling?.order ? nextSibling.order - 1 : newLayer.order;
543
+ }
544
+ // Push the layer in the list. The orderManager will move the layer regarding the order values.
545
+ existingList.push(newLayer);
546
+ }
414
547
  /**
415
548
  * Removes all layers from the layer tree except themes that are pinned.
416
549
  */
@@ -1,12 +1,9 @@
1
1
  import WMTS from 'ol/source/WMTS.js';
2
2
  import TileLayer from 'ol/layer/Tile.js';
3
- import type { Map as olMap } from 'ol';
4
3
  import type { Layer as OLayer } from 'ol/layer.js';
5
- import type LayerWmts from '../../../models/layers/layerwmts.js';
6
- import StateManager from '../../../tools/state/statemanager.js';
7
- declare class WmtsManager {
8
- map: olMap;
9
- private readonly stateManager;
4
+ import type LayerWmts from '../../models/layers/layerwmts.js';
5
+ import GirafeSingleton from '../../base/GirafeSingleton.js';
6
+ declare class WmtsManager extends GirafeSingleton {
10
7
  wmtsCapabilitiesByServer: Record<string, Record<string, unknown>>;
11
8
  wmtsPromisesByServer: Record<string, Promise<Record<string, unknown>>>;
12
9
  wmtsAbortsByLayer: Record<string, AbortController>;
@@ -19,7 +16,7 @@ declare class WmtsManager {
19
16
  layerWmts: LayerWmts;
20
17
  }>;
21
18
  private get state();
22
- constructor(map: olMap, stateManager: StateManager);
19
+ private get map();
23
20
  removeAllBasemapLayers(): void;
24
21
  addLayer(layer: LayerWmts): Promise<void>;
25
22
  addBasemapLayer(basemap: LayerWmts): void;
@@ -2,25 +2,22 @@
2
2
  import WMTS, { optionsFromCapabilities } from 'ol/source/WMTS.js';
3
3
  import WMTSCapabilities from 'ol/format/WMTSCapabilities.js';
4
4
  import TileLayer from 'ol/layer/Tile.js';
5
- import SelectionParam from '../../../models/selectionparam.js';
6
- import LayerWms from '../../../models/layers/layerwms.js';
5
+ import SelectionParam from '../../models/selectionparam.js';
6
+ import LayerWms from '../../models/layers/layerwms.js';
7
7
  import ImageWMS from 'ol/source/ImageWMS.js';
8
8
  import ImageLayer from 'ol/layer/Image.js';
9
- // TODO REG : Move to core, because we have a cross-dependency issue with the extlayers component
10
- class WmtsManager {
11
- map;
12
- stateManager;
9
+ import GirafeSingleton from '../../base/GirafeSingleton.js';
10
+ class WmtsManager extends GirafeSingleton {
13
11
  wmtsCapabilitiesByServer = {};
14
12
  wmtsPromisesByServer = {};
15
13
  wmtsAbortsByLayer = {};
16
14
  wmtsLayers = {};
17
15
  basemapLayers = {};
18
16
  get state() {
19
- return this.stateManager.state;
17
+ return this.context.stateManager.state;
20
18
  }
21
- constructor(map, stateManager) {
22
- this.map = map;
23
- this.stateManager = stateManager;
19
+ get map() {
20
+ return this.context.mapManager.getMap();
24
21
  }
25
22
  removeAllBasemapLayers() {
26
23
  Object.values(this.basemapLayers).forEach((basemap) => {
File without changes
File without changes
File without changes
File without changes
File without changes