@geogirafe/lib-geoportal 1.0.2251126674 → 1.0.2259617981

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.
@@ -18,12 +18,11 @@ class BasemapComponent extends GirafeHTMLElement {
18
18
  if (basemap.opacity == 0)
19
19
  return;
20
20
  const newBasemapProjection = basemap.projection ?? this.context.configManager.Config.map.srid;
21
- const opacityDisabled = basemap.opacity == -1;
22
21
  const activeBasemaps = [...this.state.activeBasemaps];
23
- if (opacityDisabled) {
22
+ if (basemap.opacityDisabled) {
24
23
  // Normal basemap change (no opacity basemap)
25
24
  this.state.projection = newBasemapProjection;
26
- const idx = activeBasemaps.findIndex((activeBasemap) => activeBasemap.opacity == -1);
25
+ const idx = activeBasemaps.findIndex((activeBasemap) => activeBasemap.opacityDisabled);
27
26
  if (idx >= 0) {
28
27
  activeBasemaps.splice(idx, 1);
29
28
  }
@@ -51,7 +50,7 @@ class BasemapComponent extends GirafeHTMLElement {
51
50
  }
52
51
  changeBasemapOpacity(basemap, e) {
53
52
  e.stopPropagation();
54
- if (basemap.opacity == -1) {
53
+ if (basemap.opacityDisabled) {
55
54
  console.warn(`Trying to set Opacity on Basemap '${basemap.name}' which does not allow it`);
56
55
  return;
57
56
  }
@@ -15,7 +15,7 @@ class ExternalLayersComponent extends GirafeHTMLElement {
15
15
  </style><style>
16
16
  #panel{background:var(--bkg-color);height:100%;color:var(--text-color);flex-direction:column;padding:0 1rem;display:flex}#content{flex-direction:column;flex-grow:1;margin:0;display:flex}header{background-color:var(--bkg-color);margin:0;position:sticky;top:-5px}h4{margin-bottom:.5rem}ul{margin-top:0;margin-left:0;padding-left:2rem;line-height:1.3rem;list-style-type:disc}.clearable-input{background-color:var(--bkg-color);border:1px solid #cfd6dd;border-radius:4px;flex-direction:row;margin-bottom:.1rem;display:flex}.scan-button{float:right}.layers{border-top:1px solid #ccc;flex-direction:column;flex-grow:1;margin-top:1rem;padding-top:.5rem;display:flex;overflow:hidden}ul.results{border:none;margin-bottom:.5rem;padding:0;list-style:none;overflow-y:auto}.result{height:1.6rem;display:flex}.result>button>span{text-align:left;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}#panel>div{padding:1rem}#layers li{margin-top:.3rem;margin-bottom:.3rem}#layers li button{text-align:left;overflow-wrap:anywhere;text-wrap-style:balance}#file-selector{display:grid}#file,#file-label{grid-area:1/1}#file{opacity:0;z-index:2;cursor:pointer}#file::file-selector-button{cursor:pointer}#loading-icon{flex-grow:1;align-self:center;width:3rem}.title{margin-top:0}.filter-buttons{flex-direction:row;margin:2px;display:flex}.filter-buttons span{flex-grow:1;align-self:center}.clearable-input-field{width:90%;color:var(--text-color);background:0 0;border:0;outline:none;flex:auto;padding:0;font-size:1rem}.filter-icon,.delete-icon{width:16px;color:var(--text-color);padding:0 .5rem}.delete-button{cursor:pointer;background-color:#0000;border:none;padding:0}
17
17
  </style>
18
- <div id="panel"><header class="gg-tabs"><button class="${this.selectedTab === 'wms_wmts' ? 'gg-tab active' : 'gg-tab'}" onclick="${() => this.setSelectedTab('wms_wmts')}" i18n="wms_wmts"></button> <button class="${this.selectedTab === 'file' ? 'gg-tab active' : 'gg-tab'}" onclick="${() => this.setSelectedTab('file')}" i18n="local file"></button></header><section class="${(this.selectedTab === 'wms_wmts') ? 'group' : 'hidden'}"><h4 class="title" i18n="Suggestions" ?hidden="${this.hasPredefinedWmsWmtsSources}">Suggestions</h4><ul class="suggestions" ?hidden="${this.hasPredefinedWmsWmtsSources}">${this.predefinedWmsWmtsSources.map(source => uHtmlFor(source, source.label) `<li><a href="javascript:void(0)" onclick="${() => this.scanSource(source.url, source.type)}">${source.label}</a></li>`)}</ul><h4 class="title" i18n="Custom service URL">Custom service URL</h4><div class="clearable-input"><input id="url" class="gg-input clearable-input-field" oninput="${() => this.typeUrl()}"><br><button id="clear-url" class="delete-button hidden" onclick="${() => this.clearUrl()}"><img class="delete-icon" alt="delete-button-icon" tip="Reset filter" i18n="Reset filter" src="icons/trash.svg"></button></div><button i18n="Scan Source" class="gg-button scan-button" onclick="${() => this.scanSource()}">Scan Source</button></section><section class="${(this.selectedTab === 'file') ? 'group' : 'hidden'}"><h4 class="title" i18n="Choose local file">Choose local file</h4><div id="file-selector"><label for="file" id="file-label"><button class="gg-button" i18n="Select File"></button> <span>${this.fileDescription}</span></label> <input type="file" accept=".gpx,.kml,.geojson,.json" id="file" onchange="${() => this.render()}"><br></div><button id="load" i18n="Load File" class="gg-button" onclick="${() => this.loadFile()}"></button></section><section class="${(this.selectedTab === 'wms_wmts' && this.externalLayers.length > 0) || this.loading ? 'layers' : 'hidden'}"><h4 class="title" i18n="Layers (from Capabilities)">Layers (from Capabilities)</h4><div class="clearable-input"><img class="filter-icon" alt="filter-button-icon" src="icons/filter.svg"> <input id="layer-search-field" class="gg-input clearable-input-field" placeholder="${this.context.i18nManager.getTranslation('Filter external layers...')}" i18n="Filter external layers..." autocomplete="off" autocorrect="off" oninput="${(e) => this.filterLayers(e.target.value)}"> <button class="${this.isFiltered ? 'delete-button' : 'hidden'}" onclick="${() => this.clearFilter()}"><img class="delete-icon" alt="delete-button-icon" tip="Reset filter" i18n="Reset filter" src="icons/trash.svg"></button></div><div class="filter-buttons"><span>${Object.values(this.externalLayers).filter(layer => layer.isSelected).length.toString()} ${this.context.i18nManager.getTranslation('layers selected')} </span><button class="gg-button" tip="Select all filtered layers" onclick="${() => this.selectVisible()}">+</button> <button class="gg-button" tip="Unselect all filtered layers" onclick="${() => this.deselectVisible()}">-</button> <button class="gg-button" tip="Select none" onclick="${() => this.deselectAll()}">--</button></div><img id="loading-icon" alt="loading-icon" src="icons/loading.svg" class="${this.loading ? 'gg-spin' : 'hidden'}"><ul class="results">${this.filteredLayers.map(layer => uHtmlFor(layer, layer.treeItemId) `<li class="result"><button class="${layer.isSelected ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" tip="Activate / Deactivate" onclick="${() => this.selectLayer(layer)}"><img class="${layer.isSelected ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="icons/checked-full.svg"> <img class="${!layer.isSelected ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="icons/checked-no.svg"></button> <button class="${layer.isSelected ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" onclick="${() => this.selectLayer(layer)}"><span i18n="${layer.name}" class="gg-tree-label">${layer.name}</span></button></li>`)}</ul><button id="add" tip="Add selected" i18n="Add selected" class="gg-button" onclick="${() => this.addSelectedLayers()}">Add selected</button></section></div>`;
18
+ <div id="panel"><header class="gg-tabs"><button class="${this.selectedTab === 'wms_wmts' ? 'gg-tab active' : 'gg-tab'}" onclick="${() => this.setSelectedTab('wms_wmts')}" i18n="wms_wmts"></button> <button class="${this.selectedTab === 'file' ? 'gg-tab active' : 'gg-tab'}" onclick="${() => this.setSelectedTab('file')}" i18n="local file"></button></header><section class="${(this.selectedTab === 'wms_wmts') ? 'group' : 'hidden'}"><h4 class="title" i18n="Suggestions" ?hidden="${!this.hasPredefinedWmsWmtsSources}">Suggestions</h4><ul class="suggestions" ?hidden="${!this.hasPredefinedWmsWmtsSources}">${this.predefinedWmsWmtsSources.map(source => uHtmlFor(source, source.label) `<li><a href="javascript:void(0)" onclick="${() => this.scanSource(source.url, source.type)}">${source.label}</a></li>`)}</ul><h4 class="title" i18n="Custom service URL">Custom service URL</h4><div class="clearable-input"><input id="url" class="gg-input clearable-input-field" oninput="${() => this.typeUrl()}"><br><button id="clear-url" class="delete-button hidden" onclick="${() => this.clearUrl()}"><img class="delete-icon" alt="delete-button-icon" tip="Reset filter" i18n="Reset filter" src="icons/trash.svg"></button></div><button i18n="Scan Source" class="gg-button scan-button" onclick="${() => this.scanSource()}">Scan Source</button></section><section class="${(this.selectedTab === 'file') ? 'group' : 'hidden'}"><h4 class="title" i18n="Choose local file">Choose local file</h4><div id="file-selector"><label for="file" id="file-label"><button class="gg-button" i18n="Select File"></button> <span>${this.fileDescription}</span></label> <input type="file" accept=".gpx,.kml,.geojson,.json" id="file" onchange="${() => this.render()}"><br></div><button id="load" i18n="Load File" class="gg-button" onclick="${() => this.loadFile()}"></button></section><section class="${(this.selectedTab === 'wms_wmts' && this.externalLayers.length > 0) || this.loading ? 'layers' : 'hidden'}"><h4 class="title" i18n="Layers (from Capabilities)">Layers (from Capabilities)</h4><div class="clearable-input"><img class="filter-icon" alt="filter-button-icon" src="icons/filter.svg"> <input id="layer-search-field" class="gg-input clearable-input-field" placeholder="${this.context.i18nManager.getTranslation('Filter external layers...')}" i18n="Filter external layers..." autocomplete="off" autocorrect="off" oninput="${(e) => this.filterLayers(e.target.value)}"> <button class="${this.isFiltered ? 'delete-button' : 'hidden'}" onclick="${() => this.clearFilter()}"><img class="delete-icon" alt="delete-button-icon" tip="Reset filter" i18n="Reset filter" src="icons/trash.svg"></button></div><div class="filter-buttons"><span>${Object.values(this.externalLayers).filter(layer => layer.isSelected).length.toString()} ${this.context.i18nManager.getTranslation('layers selected')} </span><button class="gg-button" tip="Select all filtered layers" onclick="${() => this.selectVisible()}">+</button> <button class="gg-button" tip="Unselect all filtered layers" onclick="${() => this.deselectVisible()}">-</button> <button class="gg-button" tip="Select none" onclick="${() => this.deselectAll()}">--</button></div><img id="loading-icon" alt="loading-icon" src="icons/loading.svg" class="${this.loading ? 'gg-spin' : 'hidden'}"><ul class="results">${this.filteredLayers.map(layer => uHtmlFor(layer, layer.treeItemId) `<li class="result"><button class="${layer.isSelected ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" tip="Activate / Deactivate" onclick="${() => this.selectLayer(layer)}"><img class="${layer.isSelected ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="icons/checked-full.svg"> <img class="${!layer.isSelected ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="icons/checked-no.svg"></button> <button class="${layer.isSelected ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" onclick="${() => this.selectLayer(layer)}"><span i18n="${layer.name}" class="gg-tree-label">${layer.name}</span></button></li>`)}</ul><button id="add" tip="Add selected" i18n="Add selected" class="gg-button" onclick="${() => this.addSelectedLayers()}">Add selected</button></section></div>`;
19
19
  };
20
20
  isPanelVisible = false;
21
21
  panelTitle = 'ext-layer-panel';
@@ -26,7 +26,7 @@ class ExternalLayersComponent extends GirafeHTMLElement {
26
26
  return this.predefinedSources.filter((source) => source.type === 'WMS' || source.type === 'WMTS');
27
27
  }
28
28
  get hasPredefinedWmsWmtsSources() {
29
- return this.predefinedWmsWmtsSources.length == 0;
29
+ return this.predefinedWmsWmtsSources.length > 0;
30
30
  }
31
31
  selectedTab = 'wms_wmts';
32
32
  wmtsManager;
@@ -885,6 +885,12 @@ export default class MapComponent extends GirafeHTMLElement {
885
885
  throw new TypeError('Unknown basemap type');
886
886
  }
887
887
  }
888
+ // Apply default opacity
889
+ for (const basemap of basemaps) {
890
+ if (!basemap.opacityDisabled) {
891
+ this.onChangeBasemapOpacity(basemap);
892
+ }
893
+ }
888
894
  }
889
895
  /**
890
896
  * This method checks for the presence of an initial selection originating from a shared state.
@@ -39,10 +39,8 @@ header{border:none;flex-direction:row-reverse;margin-top:.5rem;margin-bottom:.5r
39
39
  this.refreshRender(layer);
40
40
  this.refreshRender(layer.parent);
41
41
  });
42
- this.subscribe('treeview.renderEnabled', (_oldValue, enabled) => {
43
- if (enabled) {
44
- this.refreshRender();
45
- }
42
+ this.subscribe('treeview.renderEnabled', () => {
43
+ this.refreshRender();
46
44
  });
47
45
  }
48
46
  connectedCallback() {
@@ -8,5 +8,6 @@ declare class Basemap {
8
8
  layersList: BaseLayer[];
9
9
  constructor(elem: GMFBackgroundLayer, opacity?: number);
10
10
  get projection(): string | undefined;
11
+ get opacityDisabled(): boolean;
11
12
  }
12
13
  export default Basemap;
@@ -16,5 +16,8 @@ class Basemap {
16
16
  const layer = this.layersList.find((l) => l instanceof LayerVectorTiles);
17
17
  return layer?.projection;
18
18
  }
19
+ get opacityDisabled() {
20
+ return this.opacity === -1;
21
+ }
19
22
  }
20
23
  export default Basemap;
@@ -1,5 +1,12 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
1
7
  import LayerTimeFormatter from '../../tools/time/layertimeformatter.js';
2
8
  import Layer from './layer.js';
9
+ import { BrainIgnore } from '../../tools/state/brain/decorators.js';
3
10
  class LayerWms extends Layer {
4
11
  /**
5
12
  * This class is a used in the state of the application, which will be accessed behind a javascript proxy.
@@ -189,4 +196,7 @@ class LayerWms extends Layer {
189
196
  return opts;
190
197
  }
191
198
  }
199
+ __decorate([
200
+ BrainIgnore
201
+ ], LayerWms.prototype, "ogcServer", void 0);
192
202
  export default LayerWms;
@@ -1,3 +1,10 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { BrainIgnore } from '../../tools/state/brain/decorators.js';
1
8
  import Layer from './layer.js';
2
9
  class LayerWmts extends Layer {
3
10
  /**
@@ -115,4 +122,7 @@ class LayerWmts extends Layer {
115
122
  };
116
123
  }
117
124
  }
125
+ __decorate([
126
+ BrainIgnore
127
+ ], LayerWmts.prototype, "ogcServer", void 0);
118
128
  export default LayerWmts;
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "GeoGirafe PSC",
6
6
  "url": "https://doc.geomapfish.dev"
7
7
  },
8
- "version": "1.0.2251126674",
8
+ "version": "1.0.2259617981",
9
9
  "type": "module",
10
10
  "engines": {
11
11
  "node": ">=20.19.0"
@@ -1 +1 @@
1
- {"version":"1.0.2251126674", "build":"2251126674", "date":"08/01/2026"}
1
+ {"version":"1.0.2259617981", "build":"2259617981", "date":"13/01/2026"}
@@ -84,6 +84,7 @@ declare class GirafeConfig {
84
84
  };
85
85
  share?: {
86
86
  service: 'gmf' | 'geogirafe' | null;
87
+ preferNames: boolean;
87
88
  createUrl: string;
88
89
  getUrl?: string;
89
90
  };
@@ -162,6 +162,7 @@ class GirafeConfig {
162
162
  }
163
163
  return {
164
164
  service: config.share?.service ?? 'gmf',
165
+ preferNames: config.share?.preferNames ?? false,
165
166
  createUrl: config.share?.createUrl,
166
167
  getUrl: config.share?.getUrl
167
168
  };
package/tools/main.d.ts CHANGED
@@ -54,7 +54,7 @@ export { default as LayersConfigSerializer } from './share/serializers/layerconf
54
54
  export { default as MapPositionSerializer } from './share/serializers/mappositionserializer.js';
55
55
  export type { SharedInitialSelection } from './share/serializers/selectionserializer.js';
56
56
  export { default as SelectionSerializer } from './share/serializers/selectionserializer.js';
57
- export type { SharedFilter, SharedLayer, SharedInternalTheme, SharedInternalGroup, SharedInternalLayer, SharedExternalTheme, SharedExternalLayer } from './share/serializers/sharedtypes.js';
57
+ export type { SharedFilter, SharedLayer, SharedInternalTheme, SharedInternalGroup, SharedInternalLayer, SharedExternalTheme, SharedExternalLayer, SharedBasemap } from './share/serializers/sharedtypes.js';
58
58
  export { default as SessionManager } from './share/sessionmanager.js';
59
59
  export { default as ShareManager } from './share/sharemanager.js';
60
60
  export { default as StateSerializer } from './share/stateserializer.js';
@@ -62,11 +62,11 @@ export type { default as ISnappingConfig } from './snap/isnapconfig.js';
62
62
  export type { TProxy } from './state/brain/brain.js';
63
63
  export { default as Brain } from './state/brain/brain.js';
64
64
  export type { IBrainSerializable } from './state/brain/decorators.js';
65
- export { ignoreCloneSymbol, serializeSymbol, BrainIgnoreClone, BrainSerialize, isBrainSerializable } from './state/brain/decorators.js';
65
+ export { ignoreSymbol, ignoreCloneSymbol, serializeSymbol, BrainIgnoreClone, BrainIgnore, BrainSerialize, isBrainSerializable } from './state/brain/decorators.js';
66
66
  export { default as areEqual } from './state/brain/equality.js';
67
67
  export type { Constructor, IBrainSerializer } from './state/brain/serialize.js';
68
68
  export { default as BrainSerializer } from './state/brain/serialize.js';
69
- export { deepFreeze, deepCloneCustomizer, deepClone, isPrimitive, isFunction, isVirtualProperty, isConstructor, isIgnoredProperty, isFlagedIgnoreClone, isTypeSupported, isIterator } from './state/brain/tools.js';
69
+ export { deepFreeze, deepCloneCustomizer, deepClone, isPrimitive, isFunction, isVirtualProperty, isConstructor, isIgnoredProperty, isFlagedIgnoreClone, isFlagedIgnore, isTypeSupported, isIterator } from './state/brain/tools.js';
70
70
  export { default as ComponentManager } from './state/componentManager.js';
71
71
  export type { CameraConfig } from './state/globe.js';
72
72
  export { default as GlobeState } from './state/globe.js';
package/tools/main.js CHANGED
@@ -49,10 +49,10 @@ export { default as SessionManager } from './share/sessionmanager.js';
49
49
  export { default as ShareManager } from './share/sharemanager.js';
50
50
  export { default as StateSerializer } from './share/stateserializer.js';
51
51
  export { default as Brain } from './state/brain/brain.js';
52
- export { ignoreCloneSymbol, serializeSymbol, BrainIgnoreClone, BrainSerialize, isBrainSerializable } from './state/brain/decorators.js';
52
+ export { ignoreSymbol, ignoreCloneSymbol, serializeSymbol, BrainIgnoreClone, BrainIgnore, BrainSerialize, isBrainSerializable } from './state/brain/decorators.js';
53
53
  export { default as areEqual } from './state/brain/equality.js';
54
54
  export { default as BrainSerializer } from './state/brain/serialize.js';
55
- export { deepFreeze, deepCloneCustomizer, deepClone, isPrimitive, isFunction, isVirtualProperty, isConstructor, isIgnoredProperty, isFlagedIgnoreClone, isTypeSupported, isIterator } from './state/brain/tools.js';
55
+ export { deepFreeze, deepCloneCustomizer, deepClone, isPrimitive, isFunction, isVirtualProperty, isConstructor, isIgnoredProperty, isFlagedIgnoreClone, isFlagedIgnore, isTypeSupported, isIterator } from './state/brain/tools.js';
56
56
  export { default as ComponentManager } from './state/componentManager.js';
57
57
  export { default as GlobeState } from './state/globe.js';
58
58
  export { default as GraphicalInterface } from './state/graphicalInterface.js';
@@ -5,6 +5,9 @@ export default class ActiveBasemapsSerializer implements IBrainSerializer<Basema
5
5
  private readonly context;
6
6
  constructor(context: IGirafeContext);
7
7
  private get state();
8
- brainSerialize(basemaps: Basemap[]): string;
8
+ private get preferNames();
9
+ brainSerialize(activeBasemaps: Basemap[]): string;
9
10
  brainDeserialize(serializedString: string): void;
11
+ private serialize;
12
+ private deserialize;
10
13
  }
@@ -1,5 +1,3 @@
1
- import { applyOpacityToLayers } from '../../utils/utils.js';
2
- import { DEFAULT_OPACITY } from '../../themes/themes-config.js';
3
1
  export default class ActiveBasemapsSerializer {
4
2
  context;
5
3
  constructor(context) {
@@ -8,31 +6,43 @@ export default class ActiveBasemapsSerializer {
8
6
  get state() {
9
7
  return this.context.stateManager.state;
10
8
  }
11
- brainSerialize(basemaps) {
12
- const serializedBasemaps = basemaps
13
- .map((basemap) => `${basemap.id.toString()}=${basemap.opacity.toString()}`)
14
- .join(';');
15
- console.debug(`Serialized Basemaps to ${serializedBasemaps}`);
16
- return serializedBasemaps;
9
+ get preferNames() {
10
+ return this.context.configManager.Config.share?.preferNames ?? false;
11
+ }
12
+ brainSerialize(activeBasemaps) {
13
+ return this.serialize(activeBasemaps);
17
14
  }
18
15
  brainDeserialize(serializedString) {
19
- console.debug(`Deserializing Basemaps from ${serializedString}`);
20
- const basemapIdsWithOpacities = Object.fromEntries(serializedString.split(';').map((idAndOpacity) => idAndOpacity.split('=').map(Number)));
21
- const basemapIds = Object.keys(basemapIdsWithOpacities).map(Number);
22
- const basemaps = Object.values(this.state.basemaps).filter((b) => basemapIds.includes(b.id));
16
+ const activeBasemaps = this.deserialize(serializedString);
17
+ this.state.activeBasemaps = activeBasemaps;
18
+ }
19
+ serialize(basemaps) {
20
+ const sharedBasemaps = [];
23
21
  for (const basemap of basemaps) {
24
- basemap.opacity = basemapIdsWithOpacities[basemap.id] ?? DEFAULT_OPACITY;
25
- applyOpacityToLayers(basemap.opacity === -1 ? 1 : basemap.opacity, basemap.layersList);
26
- }
27
- if (basemaps.length > 0) {
28
- this.state.activeBasemaps = basemaps;
22
+ const sharedBasemap = {
23
+ id: basemap.id,
24
+ name: basemap.name,
25
+ opacity: basemap.opacity
26
+ };
27
+ sharedBasemaps.push(sharedBasemap);
29
28
  }
30
- else {
31
- console.debug('No Basemaps could be deserialized => not changing state.activeBasemaps');
32
- }
33
- if (basemaps.length != basemapIds.length) {
34
- const missingIds = basemapIds.filter((id) => basemaps.findIndex((basemap) => basemap.id == id) == -1); //NOSONAR(typescript:S7754) It IS about the Indexes
35
- throw new Error(`Some basemaps could not be deserialized: ${missingIds.join(',')}.`);
29
+ return JSON.stringify(sharedBasemaps);
30
+ }
31
+ deserialize(str) {
32
+ const sharedBasemaps = JSON.parse(str);
33
+ const basemaps = [];
34
+ for (const sharedBasemap of sharedBasemaps) {
35
+ const basemap = this.preferNames
36
+ ? Object.values(this.state.basemaps).find((b) => b.name === sharedBasemap.name)
37
+ : Object.values(this.state.basemaps).find((b) => b.id === sharedBasemap.id);
38
+ if (basemap) {
39
+ basemap.opacity = sharedBasemap.opacity;
40
+ basemaps.push(basemap);
41
+ }
42
+ else {
43
+ console.error(`The basemap ${sharedBasemap.name} could not be deserialized`);
44
+ }
36
45
  }
46
+ return basemaps;
37
47
  }
38
48
  }
@@ -26,7 +26,7 @@ describe('ActiveBasemapsSerializer.serialize', () => {
26
26
  })
27
27
  ];
28
28
  const serialized = serializer.brainSerialize(basemaps);
29
- expect(serialized).toBe('1=-1');
29
+ expect(serialized).toEqual('[{"id":1,"name":"OpenStreetMap","opacity":-1}]');
30
30
  });
31
31
  });
32
32
  describe('ActiveBasemapsSerializer.deserialize', () => {
@@ -40,13 +40,14 @@ describe('ActiveBasemapsSerializer.deserialize', () => {
40
40
  basemaps.forEach((basemap) => {
41
41
  context.stateManager.state.basemaps[basemap.id] = basemap;
42
42
  });
43
- serializer.brainDeserialize('1=-1');
43
+ serializer.brainDeserialize('[{"id":1,"name":"OpenStreetMap","opacity":-1}]');
44
44
  const state = context.stateManager.state;
45
45
  expect(state.activeBasemaps).toBeInstanceOf((Array));
46
46
  expect(state.activeBasemaps[0].id).toBe(basemaps[0].id);
47
47
  expect(state.activeBasemaps[0].opacity).toBe(-1);
48
+ expect(state.activeBasemaps[0].opacityDisabled).toBeTruthy();
48
49
  });
49
- it('deserializing with non existing IDs should throw an Error', () => {
50
+ it('should deserialize a valid basemap even if another one cannot be found', () => {
50
51
  const basemaps = [
51
52
  new Basemap({
52
53
  id: 1,
@@ -56,6 +57,87 @@ describe('ActiveBasemapsSerializer.deserialize', () => {
56
57
  basemaps.forEach((basemap) => {
57
58
  context.stateManager.state.basemaps[basemap.id] = basemap;
58
59
  });
59
- expect(() => serializer.brainDeserialize('1=-1;2=0;42=0.45')).toThrow(new Error(`Some basemaps could not be deserialized: 2,42.`));
60
+ serializer.brainDeserialize('[{"id":1,"name":"OpenStreetMap","opacity":-1},{"id":2,"name":"NotExisting","opacity":0.5}]');
61
+ const state = context.stateManager.state;
62
+ expect(state.activeBasemaps).toBeInstanceOf((Array));
63
+ expect(state.activeBasemaps[0].id).toBe(basemaps[0].id);
64
+ expect(state.activeBasemaps[0].opacity).toBe(-1);
65
+ expect(state.activeBasemaps[0].opacityDisabled).toBeTruthy();
66
+ });
67
+ it('should deserialize a valid basemap with the right opacity', () => {
68
+ const basemaps = [
69
+ new Basemap({
70
+ id: 1,
71
+ name: 'OpenStreetMap'
72
+ })
73
+ ];
74
+ basemaps.forEach((basemap) => {
75
+ context.stateManager.state.basemaps[basemap.id] = basemap;
76
+ });
77
+ serializer.brainDeserialize('[{"id":1,"name":"OpenStreetMap","opacity":0.45}]');
78
+ const state = context.stateManager.state;
79
+ expect(state.activeBasemaps).toBeInstanceOf((Array));
80
+ expect(state.activeBasemaps[0].id).toBe(basemaps[0].id);
81
+ expect(state.activeBasemaps[0].opacity).toBe(0.45);
82
+ expect(state.activeBasemaps[0].opacityDisabled).toBeFalsy();
83
+ });
84
+ });
85
+ describe('ActiveBasemapsSerializer.deserialize (preferNames)', () => {
86
+ beforeAll(() => {
87
+ context.configManager.Config.share.preferNames = true;
88
+ });
89
+ afterAll(() => {
90
+ context.configManager.Config.share.preferNames = false;
91
+ });
92
+ it('should deserialize a valid basemap id and set it as activeBasemap (preferNames)', () => {
93
+ const basemaps = [
94
+ new Basemap({
95
+ id: 1,
96
+ name: 'OpenStreetMap'
97
+ })
98
+ ];
99
+ basemaps.forEach((basemap) => {
100
+ context.stateManager.state.basemaps[basemap.id] = basemap;
101
+ });
102
+ serializer.brainDeserialize('[{"id":999,"name":"OpenStreetMap","opacity":-1}]');
103
+ const state = context.stateManager.state;
104
+ expect(state.activeBasemaps).toBeInstanceOf((Array));
105
+ expect(state.activeBasemaps[0].id).toBe(basemaps[0].id);
106
+ expect(state.activeBasemaps[0].opacity).toBe(-1);
107
+ expect(state.activeBasemaps[0].opacityDisabled).toBeTruthy();
108
+ });
109
+ it('should deserialize a valid basemap even if another one cannot be found (preferNames)', () => {
110
+ const basemaps = [
111
+ new Basemap({
112
+ id: 1,
113
+ name: 'OpenStreetMap'
114
+ })
115
+ ];
116
+ basemaps.forEach((basemap) => {
117
+ context.stateManager.state.basemaps[basemap.id] = basemap;
118
+ });
119
+ serializer.brainDeserialize('[{"id":999,"name":"OpenStreetMap","opacity":-1},{"id":888,"name":"NotExisting","opacity":0.5}]');
120
+ const state = context.stateManager.state;
121
+ expect(state.activeBasemaps).toBeInstanceOf((Array));
122
+ expect(state.activeBasemaps[0].id).toBe(basemaps[0].id);
123
+ expect(state.activeBasemaps[0].opacity).toBe(-1);
124
+ expect(state.activeBasemaps[0].opacityDisabled).toBeTruthy();
125
+ });
126
+ it('should deserialize a valid basemap with the right opacity (preferNames)', () => {
127
+ const basemaps = [
128
+ new Basemap({
129
+ id: 1,
130
+ name: 'OpenStreetMap'
131
+ })
132
+ ];
133
+ basemaps.forEach((basemap) => {
134
+ context.stateManager.state.basemaps[basemap.id] = basemap;
135
+ });
136
+ serializer.brainDeserialize('[{"id":999,"name":"OpenStreetMap","opacity":0.45}]');
137
+ const state = context.stateManager.state;
138
+ expect(state.activeBasemaps).toBeInstanceOf((Array));
139
+ expect(state.activeBasemaps[0].id).toBe(basemaps[0].id);
140
+ expect(state.activeBasemaps[0].opacity).toBe(0.45);
141
+ expect(state.activeBasemaps[0].opacityDisabled).toBeFalsy();
60
142
  });
61
143
  });
@@ -7,6 +7,7 @@ export default class LayersConfigSerializer implements IBrainSerializer<LayersCo
7
7
  private readonly context;
8
8
  constructor(context: IGirafeContext);
9
9
  private get state();
10
+ private get preferNames();
10
11
  brainSerialize(layersConfig: LayersConfig): string;
11
12
  protected serialize(layers: BaseLayer[]): string;
12
13
  brainDeserialize(str: string): void;
@@ -16,6 +17,7 @@ export default class LayersConfigSerializer implements IBrainSerializer<LayersCo
16
17
  private getExternalSerializedTheme;
17
18
  private getExternalSerializedLayer;
18
19
  private getInternalSerializedGroupOrTheme;
20
+ private getInternalLayerType;
19
21
  private getInternalSerializedLayer;
20
22
  private getDeserializedLayerTree;
21
23
  private deserializeInternalObject;
@@ -24,8 +26,11 @@ export default class LayersConfigSerializer implements IBrainSerializer<LayersCo
24
26
  private deserializeInternalLayer;
25
27
  private checkUnknownLayers;
26
28
  private removeUnnecessaryChilds;
29
+ private reorderChildren;
27
30
  private findBaseLayerById;
28
- private findLayerRecursive;
31
+ private findLayerRecursiveById;
32
+ private findBaseLayerByName;
33
+ private findLayerRecursiveByName;
29
34
  private deserializeExternalObject;
30
35
  private deserializeExternalTheme;
31
36
  private deserializeExternalWmsLayer;
@@ -7,6 +7,11 @@ import ThemeLayerExternal from '../../../models/layers/themelayerexternal.js';
7
7
  import LayerWmsExternal from '../../../models/layers/layerwmsexternal.js';
8
8
  import LayerWmtsExternal from '../../../models/layers/layerwmtsexternal.js';
9
9
  import ServerOgc from '../../../models/serverogc.js';
10
+ import LayerWmts from '../../../models/layers/layerwmts.js';
11
+ import LayerVectorTiles from '../../../models/layers/layervectortiles.js';
12
+ import LayerCog from '../../../models/layers/layercog.js';
13
+ import LayerOsm from '../../../models/layers/layerosm.js';
14
+ import LayerXYZ from '../../../models/layers/layerxyz.js';
10
15
  export default class LayersConfigSerializer {
11
16
  context;
12
17
  constructor(context) {
@@ -15,6 +20,9 @@ export default class LayersConfigSerializer {
15
20
  get state() {
16
21
  return this.context.stateManager.state;
17
22
  }
23
+ get preferNames() {
24
+ return this.context.configManager.Config.share?.preferNames ?? false;
25
+ }
18
26
  brainSerialize(layersConfig) {
19
27
  return this.serialize(layersConfig.layersList);
20
28
  }
@@ -104,6 +112,7 @@ export default class LayersConfigSerializer {
104
112
  const originalTheme = this.context.themesHelper.findBaseLayerById(group.id);
105
113
  const sharedChildren = [];
106
114
  const removedChildrenIds = [];
115
+ const removedChildrenNames = [];
107
116
  for (const originalChild of originalTheme.children) {
108
117
  const index = group.children.findIndex((el) => el.id === originalChild.id);
109
118
  if (index >= 0) {
@@ -114,6 +123,7 @@ export default class LayersConfigSerializer {
114
123
  else {
115
124
  // Element is not in the list any more, and therefore should not be shared or restored
116
125
  removedChildrenIds.push(originalChild.id);
126
+ removedChildrenNames.push(originalChild.name);
117
127
  }
118
128
  }
119
129
  return {
@@ -123,9 +133,39 @@ export default class LayersConfigSerializer {
123
133
  isExpanded: Number(group.isExpanded),
124
134
  timeRestriction: isTimeAwareLayer(group) ? group.timeRestriction : undefined,
125
135
  children: sharedChildren,
126
- excludedChildrenIds: removedChildrenIds
136
+ excludedChildrenIds: removedChildrenIds,
137
+ name: group.name,
138
+ type: this.getInternalLayerType(group),
139
+ excludedChildrenNames: removedChildrenNames
127
140
  };
128
141
  }
142
+ getInternalLayerType(layer) {
143
+ if (layer instanceof ThemeLayer) {
144
+ return 'theme';
145
+ }
146
+ if (layer instanceof GroupLayer) {
147
+ return 'group';
148
+ }
149
+ if (layer instanceof LayerWms) {
150
+ return 'wms';
151
+ }
152
+ if (layer instanceof LayerWmts) {
153
+ return 'wmts';
154
+ }
155
+ if (layer instanceof LayerVectorTiles) {
156
+ return 'vt';
157
+ }
158
+ if (layer instanceof LayerCog) {
159
+ return 'cog';
160
+ }
161
+ if (layer instanceof LayerOsm) {
162
+ return 'osm';
163
+ }
164
+ if (layer instanceof LayerXYZ) {
165
+ return 'xyz';
166
+ }
167
+ throw new Error(`The layer ${layer.name} has an unknown type and cannot be shared.`);
168
+ }
129
169
  getInternalSerializedLayer(layer) {
130
170
  return {
131
171
  id: layer.id,
@@ -137,7 +177,9 @@ export default class LayersConfigSerializer {
137
177
  filter: layer instanceof LayerWms && this.context.layerManager.isLayerWithFilter(layer)
138
178
  ? layer.filter
139
179
  : undefined,
140
- timeRestriction: isTimeAwareLayer(layer) ? layer.timeRestriction : undefined
180
+ timeRestriction: isTimeAwareLayer(layer) ? layer.timeRestriction : undefined,
181
+ name: layer.name,
182
+ type: this.getInternalLayerType(layer)
141
183
  };
142
184
  }
143
185
  getDeserializedLayerTree(sharedLayers) {
@@ -146,7 +188,12 @@ export default class LayersConfigSerializer {
146
188
  let layer;
147
189
  if ('id' in sharedLayer) {
148
190
  // Id attribute found => we are on an internal layer
149
- layer = this.findBaseLayerById(sharedLayer.id);
191
+ if (this.preferNames && sharedLayer.name) {
192
+ layer = this.findBaseLayerByName(sharedLayer.name, sharedLayer.type);
193
+ }
194
+ else {
195
+ layer = this.findBaseLayerById(sharedLayer.id);
196
+ }
150
197
  if (layer) {
151
198
  this.deserializeInternalObject(layer, sharedLayer);
152
199
  }
@@ -228,7 +275,9 @@ export default class LayersConfigSerializer {
228
275
  let reorder = false;
229
276
  for (let i = originalLayer.children.length - 1; i >= 0; i--) {
230
277
  const child = originalLayer.children[i];
231
- const serializedChild = sharedLayer.children.find((l) => l.id == child.id);
278
+ const serializedChild = this.preferNames
279
+ ? sharedLayer.children.find((l) => l.name == child.name)
280
+ : sharedLayer.children.find((l) => l.id == child.id);
232
281
  if (serializedChild) {
233
282
  this.deserializeInternalObject(child, serializedChild);
234
283
  }
@@ -236,7 +285,9 @@ export default class LayersConfigSerializer {
236
285
  // This child exists in the original layer, but not in the shared state.
237
286
  // => If it is present in the x list, it was explicitely removed
238
287
  // And we can remove it from the current object
239
- const explicitlyRemoved = sharedLayer.excludedChildrenIds.find((id) => id == child.id);
288
+ const explicitlyRemoved = this.preferNames
289
+ ? sharedLayer.excludedChildrenNames.find((name) => name == child.name)
290
+ : sharedLayer.excludedChildrenIds.find((id) => id == child.id);
240
291
  if (explicitlyRemoved) {
241
292
  originalLayer.children.splice(i, 1);
242
293
  console.debug(`Layer ${child.name} was removed from initial state`);
@@ -253,23 +304,26 @@ export default class LayersConfigSerializer {
253
304
  }
254
305
  }
255
306
  if (reorder) {
256
- console.debug(`Reordering childs for layer ${originalLayer.name}`);
257
- let order = 1;
258
- for (const child of originalLayer.children) {
259
- child.order = order++;
260
- }
307
+ this.reorderChildren(originalLayer);
308
+ }
309
+ }
310
+ reorderChildren(originalLayer) {
311
+ console.debug(`Reordering childs for layer ${originalLayer.name}`);
312
+ let order = 1;
313
+ for (const child of originalLayer.children) {
314
+ child.order = order++;
261
315
  }
262
316
  }
263
317
  findBaseLayerById(layerId) {
264
318
  for (const theme of Object.values(this.state.themes._allThemes)) {
265
- const layer = this.findLayerRecursive(theme, layerId);
319
+ const layer = this.findLayerRecursiveById(theme, layerId);
266
320
  if (layer) {
267
321
  return layer;
268
322
  }
269
323
  }
270
324
  return null;
271
325
  }
272
- findLayerRecursive(layer, layerId) {
326
+ findLayerRecursiveById(layer, layerId) {
273
327
  if (layer.id === layerId) {
274
328
  // When deserializing the layer, we clone it,
275
329
  // otherwise the following operation will also
@@ -280,7 +334,35 @@ export default class LayersConfigSerializer {
280
334
  // Else, we call recursively on the children
281
335
  if (layer instanceof GroupLayer || layer instanceof ThemeLayer) {
282
336
  for (const childLayer of layer.children) {
283
- const foundChild = this.findLayerRecursive(childLayer, layerId);
337
+ const foundChild = this.findLayerRecursiveById(childLayer, layerId);
338
+ if (foundChild) {
339
+ return foundChild;
340
+ }
341
+ }
342
+ }
343
+ return null;
344
+ }
345
+ findBaseLayerByName(layerName, layerType) {
346
+ for (const theme of Object.values(this.state.themes._allThemes)) {
347
+ const layer = this.findLayerRecursiveByName(theme, layerName, layerType);
348
+ if (layer) {
349
+ return layer;
350
+ }
351
+ }
352
+ return null;
353
+ }
354
+ findLayerRecursiveByName(layer, layerName, layerType) {
355
+ if (layer.name === layerName && this.getInternalLayerType(layer) === layerType) {
356
+ // When deserializing the layer, we clone it,
357
+ // otherwise the following operation will also
358
+ // affect the layer referenced in other themes
359
+ const foundLayer = layer.clone();
360
+ return foundLayer;
361
+ }
362
+ // Else, we call recursively on the children
363
+ if (layer instanceof GroupLayer || layer instanceof ThemeLayer) {
364
+ for (const childLayer of layer.children) {
365
+ const foundChild = this.findLayerRecursiveByName(childLayer, layerName, layerType);
284
366
  if (foundChild) {
285
367
  return foundChild;
286
368
  }
@@ -74,10 +74,16 @@ function getTestData(options = {}) {
74
74
  checked: options.isGroupChecked ? 1 : 0,
75
75
  isExpanded: options.isGroupExpanded ? 1 : 0,
76
76
  children: [],
77
- excludedChildrenIds: options.addSecondMissingLayer ? [22] : []
77
+ excludedChildrenIds: options.addSecondMissingLayer ? [22] : [],
78
+ name: 'Group 1',
79
+ type: 'group',
80
+ excludedChildrenNames: options.addSecondMissingLayer ? ['Layer WMTS 2'] : []
78
81
  }
79
82
  ],
80
- excludedChildrenIds: options.addSecondMissingGroup ? [12] : []
83
+ excludedChildrenIds: options.addSecondMissingGroup ? [12] : [],
84
+ name: 'test-theme',
85
+ type: 'theme',
86
+ excludedChildrenNames: options.addSecondMissingGroup ? ['Group 2'] : []
81
87
  }
82
88
  ];
83
89
  if (options.addWmtsLayer) {
@@ -87,7 +93,9 @@ function getTestData(options = {}) {
87
93
  checked: 0,
88
94
  isExpanded: 0,
89
95
  opacity: options.opacity ?? 1,
90
- swiped: options.swiped ?? 'no'
96
+ swiped: options.swiped ?? 'no',
97
+ name: 'Layer WMTS 1',
98
+ type: 'wmts'
91
99
  });
92
100
  }
93
101
  return {
@@ -269,6 +277,70 @@ describe('LayersConfigSerializer.deserialize', () => {
269
277
  expect(serialized).toEqual(data.controlValue);
270
278
  });
271
279
  });
280
+ describe('LayersConfigSerializer.deserialize (preferNames)', () => {
281
+ beforeAll(() => {
282
+ context.configManager.Config.share.preferNames = true;
283
+ });
284
+ afterAll(() => {
285
+ context.configManager.Config.share.preferNames = false;
286
+ });
287
+ it('should return serialized data for a GroupLayer (id, order) (preferNames)', () => {
288
+ const data = getTestData();
289
+ serializer.brainDeserialize(data.controlValue);
290
+ const layersConfig = context.stateManager.state.layers;
291
+ const serialized = serializer.brainSerialize(layersConfig);
292
+ expect(serialized).toEqual(data.controlValue);
293
+ });
294
+ it('should return serialized data for a GroupLayer (isExpanded) (preferNames)', () => {
295
+ const data = getTestData({ isGroupExpanded: true });
296
+ serializer.brainDeserialize(data.controlValue);
297
+ const layersConfig = context.stateManager.state.layers;
298
+ const serialized = serializer.brainSerialize(layersConfig);
299
+ expect(serialized).toEqual(data.controlValue);
300
+ });
301
+ it('should return serialized data for a GroupLayer with children (preferNames)', () => {
302
+ const data = getTestData({ addWmtsLayer: true });
303
+ serializer.brainDeserialize(data.controlValue);
304
+ const layersConfig = context.stateManager.state.layers;
305
+ const serialized = serializer.brainSerialize(layersConfig);
306
+ expect(serialized).toEqual(data.controlValue);
307
+ });
308
+ it('should return serialized data for a GroupLayer with children (opacity) (preferNames)', () => {
309
+ const data = getTestData({ addWmtsLayer: true, opacity: 0.5 });
310
+ serializer.brainDeserialize(data.controlValue);
311
+ const layersConfig = context.stateManager.state.layers;
312
+ const serialized = serializer.brainSerialize(layersConfig);
313
+ expect(serialized).toEqual(data.controlValue);
314
+ });
315
+ it('should return serialized data for a GroupLayer with children (swiped left) (preferNames)', () => {
316
+ const data = getTestData({ addWmtsLayer: true, swiped: 'left' });
317
+ serializer.brainDeserialize(data.controlValue);
318
+ const layersConfig = context.stateManager.state.layers;
319
+ const serialized = serializer.brainSerialize(layersConfig);
320
+ expect(serialized).toEqual(data.controlValue);
321
+ });
322
+ it('should return serialized data for a GroupLayer with children (swiped right) (preferNames)', () => {
323
+ const data = getTestData({ addWmtsLayer: true, swiped: 'left' });
324
+ serializer.brainDeserialize(data.controlValue);
325
+ const layersConfig = context.stateManager.state.layers;
326
+ const serialized = serializer.brainSerialize(layersConfig);
327
+ expect(serialized).toEqual(data.controlValue);
328
+ });
329
+ it('should serialize a missing original group correctly (explicitely removed) (preferNames)', () => {
330
+ const data = getTestData({ addSecondMissingGroup: true });
331
+ serializer.brainDeserialize(data.controlValue);
332
+ const layersConfig = context.stateManager.state.layers;
333
+ const serialized = serializer.brainSerialize(layersConfig);
334
+ expect(serialized).toEqual(data.controlValue);
335
+ });
336
+ it('should serialize a missing original layer correctly (explicitely removed) (preferNames)', () => {
337
+ const data = getTestData({ addWmtsLayer: true, addSecondMissingLayer: true });
338
+ serializer.brainDeserialize(data.controlValue);
339
+ const layersConfig = context.stateManager.state.layers;
340
+ const serialized = serializer.brainSerialize(layersConfig);
341
+ expect(serialized).toEqual(data.controlValue);
342
+ });
343
+ });
272
344
  describe('LayersConfigSerializer.serialize external', () => {
273
345
  it('should return serialized data for an external theme', () => {
274
346
  const data = getExternalTestData();
@@ -20,6 +20,16 @@ export type SharedInternalTheme = {
20
20
  * Or if it was explicitly removed from the user, and then we won't have to display it again
21
21
  */
22
22
  excludedChildrenIds: number[];
23
+ /**
24
+ * When configuring the share with "preferNames=true", the name of the layer will be used to generate the sahred state
25
+ * instead of the ID. But in this case, we also have to share the type os the layer, because tha layer name can be
26
+ * the same for a them, a group, a WMS-Layer, a WMTSLayer, etc...
27
+ * Also note that the "excludedChildrenNames" options has some limitations : if we have 2 layers of different types with the same name
28
+ * And if one of it is exceluded, both will be excluded.
29
+ */
30
+ name: string;
31
+ type: string;
32
+ excludedChildrenNames: string[];
23
33
  };
24
34
  export type SharedInternalGroup = {
25
35
  id: number;
@@ -29,6 +39,9 @@ export type SharedInternalGroup = {
29
39
  timeRestriction?: string;
30
40
  children: SharedInternalLayer[];
31
41
  excludedChildrenIds: number[];
42
+ name: string;
43
+ type: string;
44
+ excludedChildrenNames: string[];
32
45
  };
33
46
  export type SharedInternalLayer = {
34
47
  id: number;
@@ -39,6 +52,8 @@ export type SharedInternalLayer = {
39
52
  opacity?: number;
40
53
  swiped?: 'left' | 'right' | 'no';
41
54
  filter?: SharedFilter;
55
+ name: string;
56
+ type: string;
42
57
  };
43
58
  export type SharedExternalTheme = {
44
59
  name: string;
@@ -64,3 +79,8 @@ export type SharedExternalLayer = {
64
79
  opacity?: number;
65
80
  swiped?: 'left' | 'right' | 'no';
66
81
  };
82
+ export type SharedBasemap = {
83
+ id: number;
84
+ name: string;
85
+ opacity: number;
86
+ };
@@ -6,7 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { describe, it, expect, beforeEach } from 'vitest';
8
8
  import Brain from './brain';
9
- import { BrainIgnoreClone } from './decorators';
9
+ import { BrainIgnore, BrainIgnoreClone } from './decorators';
10
10
  let state;
11
11
  let brain;
12
12
  describe('State: base principles', () => {
@@ -377,7 +377,7 @@ describe('State: base principles', () => {
377
377
  });
378
378
  expect(counter).toBe(2);
379
379
  });
380
- it('Base principles: Do not clone objects with attribute BrainIgnoreClone', () => {
380
+ it('Base principles: Do not clone objects with decorator @BrainIgnoreClone', () => {
381
381
  class Pizza {
382
382
  key;
383
383
  owner;
@@ -406,7 +406,7 @@ describe('State: base principles', () => {
406
406
  brain.getState().objectValue = pizza2;
407
407
  expect(counter).toBe(2);
408
408
  });
409
- it('Base principles: Do not clone objects with attribute BrainIgnoreClone, but listen to changes', () => {
409
+ it('Base principles: Do not clone objects with decorator @BrainIgnoreClone, but listen to changes', () => {
410
410
  class Pizza {
411
411
  key;
412
412
  owner;
@@ -434,6 +434,35 @@ describe('State: base principles', () => {
434
434
  brain.getState().objectValue.owner.place = 'Bernwiller';
435
435
  expect(counter).toBe(2);
436
436
  });
437
+ it('Base principles: Do not create proxy for attributes with decorator @BrainIgnore', () => {
438
+ class Pizza {
439
+ key;
440
+ owner;
441
+ constructor(key, owner) {
442
+ this.key = key;
443
+ this.owner = owner;
444
+ }
445
+ }
446
+ __decorate([
447
+ BrainIgnore
448
+ ], Pizza.prototype, "owner", void 0);
449
+ const pizzeria = {
450
+ name: 'Noninna',
451
+ place: 'Aspach'
452
+ };
453
+ const pizza1 = new Pizza('pizza1', pizzeria);
454
+ state.objectValue = pizza1;
455
+ let counter = 1;
456
+ brain = new Brain(state, () => {
457
+ counter++;
458
+ });
459
+ const pizzaProxy = brain.getState().objectValue;
460
+ expect(pizzaProxy.__brainIsProxy).toBeTruthy();
461
+ const owner = pizzaProxy.owner;
462
+ expect(owner.__brainIsProxy).toBeFalsy();
463
+ owner.place = 'Bernwiller';
464
+ expect(counter).toBe(1);
465
+ });
437
466
  it('Base principles: Handles multiple parents referencing the same object', () => {
438
467
  const shared = { id: 42 };
439
468
  state.objectValue = { child: shared };
@@ -20,20 +20,20 @@ export default class Brain {
20
20
  this.externalCallback = callback;
21
21
  }
22
22
  callback(path, oldValue, newValue, parents) {
23
- if (!this.delayed) {
24
- // Immediate Callback
25
- this.externalCallback(path, oldValue, newValue, parents);
26
- }
27
- else {
23
+ if (this.delayed) {
28
24
  // Remember all callback infos
29
25
  const infos = this.delayedCallbacks.get(path);
30
- if (!infos) {
31
- this.delayedCallbacks.set(path, { oldValue: oldValue, newValue: newValue, parents: parents });
32
- }
33
- else {
26
+ if (infos) {
34
27
  // Just set the new value
35
28
  infos.newValue = newValue;
36
29
  }
30
+ else {
31
+ this.delayedCallbacks.set(path, { oldValue: oldValue, newValue: newValue, parents: parents });
32
+ }
33
+ }
34
+ else {
35
+ // Immediate Callback
36
+ this.externalCallback(path, oldValue, newValue, parents);
37
37
  }
38
38
  }
39
39
  /**
@@ -86,17 +86,21 @@ export default class Brain {
86
86
  return this.mergeMinimalPathsCache.get(cacheKey);
87
87
  }
88
88
  const minimalPaths = new Set();
89
+ const parentsPaths = [];
90
+ for (const p of parents) {
91
+ parentsPaths.push(...p.__brainFullPaths);
92
+ }
89
93
  for (const candidate of [...existingPaths, ...candidatePaths]) {
90
94
  // Already present
91
95
  if (minimalPaths.has(candidate)) {
92
96
  continue;
93
97
  }
94
98
  // If the candidate hat a minimal path as prefix
95
- if ([...minimalPaths].some((path) => candidate.startsWith(`${path}.`))) {
99
+ if (Array.from(minimalPaths).some((path) => candidate.startsWith(`${path}.`))) {
96
100
  continue;
97
101
  }
98
102
  // If not prefixed by any parent
99
- if (!this.isRightParent(candidate, parents)) {
103
+ if (!this.isRightParent(candidate, parentsPaths)) {
100
104
  continue;
101
105
  }
102
106
  minimalPaths.add(candidate);
@@ -105,30 +109,23 @@ export default class Brain {
105
109
  this.mergeMinimalPathsCache.set(cacheKey, returnValue);
106
110
  return returnValue;
107
111
  }
108
- isRightParent(candidate, parents) {
109
- const parentsPaths = [];
110
- for (const p of parents) {
111
- parentsPaths.push(...p.__brainFullPaths);
112
+ isRightParent(candidate, parentsPaths) {
113
+ if (parentsPaths.length === 0 || parentsPaths[0].length === 0) {
114
+ // On the root
115
+ return true;
112
116
  }
113
- if (parentsPaths?.length > 0 && parentsPaths[0].length > 0) {
114
- // Not on the root
115
- let circularReference = false;
116
- let rightParent = false;
117
- for (const parentPath of parentsPaths) {
118
- if (parentPath.includes(`${candidate}.`)) {
119
- circularReference = true;
120
- break;
121
- }
122
- else if (candidate.startsWith(`${parentPath}.`)) {
123
- rightParent = true;
124
- break;
125
- }
117
+ for (const parentPath of parentsPaths) {
118
+ if (parentPath.includes(`${candidate}.`)) {
119
+ // Circular reference
120
+ return true;
126
121
  }
127
- if (!circularReference && !rightParent) {
128
- return false;
122
+ if (candidate.startsWith(`${parentPath}.`)) {
123
+ // Parent found
124
+ return true;
129
125
  }
130
126
  }
131
- return true;
127
+ // Not a valid parent
128
+ return false;
132
129
  }
133
130
  getOrCreateProxyForValue(proxy, prop, value, childPaths) {
134
131
  if (isPrimitive(value)) {
@@ -150,8 +147,8 @@ export default class Brain {
150
147
  const fullPaths = valueProxy.__brainFullPaths;
151
148
  if (merged.length !== fullPaths.length || merged.some((p, i) => p !== fullPaths[i])) {
152
149
  fullPaths.splice(0, fullPaths.length, ...merged);
150
+ this.updateChildPathsRecursively(valueProxy, fullPaths);
153
151
  }
154
- this.updateChildPathsRecursively(valueProxy, fullPaths);
155
152
  }
156
153
  else {
157
154
  // Create a new proxy
@@ -197,7 +194,7 @@ export default class Brain {
197
194
  }
198
195
  recalculateChildrenForArray(proxy, target, oldValue) {
199
196
  if (!Array.isArray(target)) {
200
- throw new Error('This method is only for arrays');
197
+ throw new TypeError('This method is only for arrays');
201
198
  }
202
199
  // Clean previous childs
203
200
  proxy.__brainChildren.clear();
@@ -266,7 +263,7 @@ export default class Brain {
266
263
  }
267
264
  handleGetIgnored(target, prop) {
268
265
  const value = target[prop];
269
- if (typeof prop === 'symbol' || prop.startsWith('_')) {
266
+ if (isIgnoredProperty(target, prop)) {
270
267
  return value;
271
268
  }
272
269
  throw new Error(`Unknown ignored property: ${prop}`);
@@ -334,7 +331,7 @@ export default class Brain {
334
331
  };
335
332
  }
336
333
  getHandler(proxy, target, prop) {
337
- if (isIgnoredProperty(prop)) {
334
+ if (isIgnoredProperty(target, prop)) {
338
335
  return this.handleGetIgnored(target, prop);
339
336
  }
340
337
  if (isVirtualProperty(prop)) {
@@ -388,7 +385,7 @@ export default class Brain {
388
385
  let oldValue = target[prop];
389
386
  const childPaths = proxy.__brainFullPaths.map((path) => this.getFullPath(path, prop));
390
387
  this.cleanProxyForValue(proxy, prop, oldValue, childPaths);
391
- if (isIgnoredProperty(prop)) {
388
+ if (isIgnoredProperty(target, prop)) {
392
389
  // Ignored => No callback and no clone of the old version
393
390
  // Just set the new value
394
391
  target[prop] = newValue;
@@ -1,3 +1,7 @@
1
+ /**
2
+ * A symbol used to mark properties that should be ignored during cloning operations.
3
+ */
4
+ export declare const ignoreSymbol: unique symbol;
1
5
  /**
2
6
  * A symbol used to mark properties that should be ignored during cloning operations.
3
7
  */
@@ -13,6 +17,13 @@ export declare const serializeSymbol: unique symbol;
13
17
  * @param propertyKey - The key of the property to be ignored during cloning.
14
18
  */
15
19
  export declare function BrainIgnoreClone(target: any, propertyKey: string | symbol): void;
20
+ /**
21
+ * A decorator function that marks a property of a class to be ignored. No proxy will be created for this object.
22
+ *
23
+ * @param target - The target object (class prototype) where the property resides.
24
+ * @param propertyKey - The key of the property to be ignored.
25
+ */
26
+ export declare function BrainIgnore(target: any, propertyKey: string | symbol): void;
16
27
  /**
17
28
  * A decorator function that marks a property of a class to be serializable.
18
29
  *
@@ -1,3 +1,7 @@
1
+ /**
2
+ * A symbol used to mark properties that should be ignored during cloning operations.
3
+ */
4
+ export const ignoreSymbol = Symbol('brainIgnore');
1
5
  /**
2
6
  * A symbol used to mark properties that should be ignored during cloning operations.
3
7
  */
@@ -16,6 +20,16 @@ export function BrainIgnoreClone(target, propertyKey) {
16
20
  target[ignoreCloneSymbol] ??= [];
17
21
  target[ignoreCloneSymbol].push(propertyKey);
18
22
  }
23
+ /**
24
+ * A decorator function that marks a property of a class to be ignored. No proxy will be created for this object.
25
+ *
26
+ * @param target - The target object (class prototype) where the property resides.
27
+ * @param propertyKey - The key of the property to be ignored.
28
+ */
29
+ export function BrainIgnore(target, propertyKey) {
30
+ target[ignoreSymbol] ??= [];
31
+ target[ignoreSymbol].push(propertyKey);
32
+ }
19
33
  /**
20
34
  * A decorator function that marks a property of a class to be serializable.
21
35
  *
@@ -62,7 +62,7 @@ export declare function isConstructor(prop: string): boolean;
62
62
  * @param prop - The property name or symbol to check.
63
63
  * @returns `true` if the property should be ignored, otherwise `false`.
64
64
  */
65
- export declare function isIgnoredProperty(prop: string | symbol): boolean;
65
+ export declare function isIgnoredProperty(target: any, prop: string | symbol): boolean;
66
66
  /**
67
67
  * Checks if a property on a target object is flagged to be ignored during cloning.
68
68
  *
@@ -71,6 +71,14 @@ export declare function isIgnoredProperty(prop: string | symbol): boolean;
71
71
  * @returns `true` if the property is flagged to be ignored, otherwise `false`.
72
72
  */
73
73
  export declare function isFlagedIgnoreClone(target: any, prop?: string | number): any;
74
+ /**
75
+ * Checks if a property on a target object is flagged to be totally ignored by brain.
76
+ *
77
+ * @param target - The object containing the property.
78
+ * @param prop - The property name or index to check.
79
+ * @returns `true` if the property is flagged to be ignored, otherwise `false`.
80
+ */
81
+ export declare function isFlagedIgnore(target: any, prop?: string | number): any;
74
82
  /**
75
83
  * Determines if a value is of a supported type.
76
84
  * Unsupported types include `WeakMap`, `WeakSet`, `Map`, and `Set`.
@@ -1,5 +1,5 @@
1
1
  import { cloneDeepWith } from 'lodash-es';
2
- import { ignoreCloneSymbol } from './decorators.js';
2
+ import { ignoreCloneSymbol, ignoreSymbol } from './decorators.js';
3
3
  /**
4
4
  * Recursively freezes an object and all its properties to make it immutable.
5
5
  * This function also handles circular references by using a `WeakSet` to track visited objects.
@@ -17,15 +17,15 @@ export function deepFreeze(obj, visitedObjects = new WeakSet()) {
17
17
  }
18
18
  visitedObjects.add(obj);
19
19
  Object.freeze(obj);
20
- Object.getOwnPropertyNames(obj).forEach((prop) => {
21
- if (!isIgnoredProperty(prop) && !isFlagedIgnoreClone(obj, prop)) {
20
+ for (const prop of Object.getOwnPropertyNames(obj)) {
21
+ if (!isIgnoredProperty(obj, prop) && !isFlagedIgnoreClone(obj, prop)) {
22
22
  // Do not freeze ignored objects
23
23
  const value = obj[prop];
24
24
  if (typeof value === 'object' && value !== null) {
25
25
  deepFreeze(value, visitedObjects);
26
26
  }
27
27
  }
28
- });
28
+ }
29
29
  }
30
30
  /**
31
31
  * Customizer function for `cloneDeepWith` to handle specific cloning rules.
@@ -39,14 +39,10 @@ export function deepFreeze(obj, visitedObjects = new WeakSet()) {
39
39
  * @returns The value to use for the cloned property, or `undefined` to use the default cloning behavior.
40
40
  */
41
41
  export function deepCloneCustomizer(value, prop, target) {
42
- if (prop?.toString().startsWith('_')) {
42
+ if (isFlagedIgnore(value, prop) || isFlagedIgnoreClone(target, prop) || prop?.toString().startsWith('_')) {
43
43
  // Do not clone : just copy the reference
44
44
  return value;
45
45
  }
46
- if (isFlagedIgnoreClone(target, prop)) {
47
- // Do not clone marked properties
48
- return value;
49
- }
50
46
  // Else : do nothing special, the default cloneDeep will be used.
51
47
  }
52
48
  /**
@@ -108,8 +104,14 @@ export function isConstructor(prop) {
108
104
  * @param prop - The property name or symbol to check.
109
105
  * @returns `true` if the property should be ignored, otherwise `false`.
110
106
  */
111
- export function isIgnoredProperty(prop) {
112
- return typeof prop === 'symbol' || (prop.startsWith('_') && !isVirtualProperty(prop));
107
+ export function isIgnoredProperty(target, prop) {
108
+ if (typeof prop === 'symbol') {
109
+ return true;
110
+ }
111
+ if (prop.startsWith('_') && !isVirtualProperty(prop)) {
112
+ return true;
113
+ }
114
+ return isFlagedIgnore(target, prop);
113
115
  }
114
116
  /**
115
117
  * Checks if a property on a target object is flagged to be ignored during cloning.
@@ -121,6 +123,16 @@ export function isIgnoredProperty(prop) {
121
123
  export function isFlagedIgnoreClone(target, prop) {
122
124
  return target?.[ignoreCloneSymbol]?.includes(prop);
123
125
  }
126
+ /**
127
+ * Checks if a property on a target object is flagged to be totally ignored by brain.
128
+ *
129
+ * @param target - The object containing the property.
130
+ * @param prop - The property name or index to check.
131
+ * @returns `true` if the property is flagged to be ignored, otherwise `false`.
132
+ */
133
+ export function isFlagedIgnore(target, prop) {
134
+ return target?.[ignoreSymbol]?.includes(prop);
135
+ }
124
136
  /**
125
137
  * Determines if a value is of a supported type.
126
138
  * Unsupported types include `WeakMap`, `WeakSet`, `Map`, and `Set`.
@@ -52,7 +52,7 @@ describe('StateManager.subscribe', () => {
52
52
  controlValue = 2;
53
53
  };
54
54
  try {
55
- manager.state.ogcServers = {};
55
+ manager.state.basemaps = {};
56
56
  manager.subscribe('ogcServers', callback);
57
57
  expect(controlValue).toEqual(1);
58
58
  }