@geogirafe/lib-geoportal 1.0.2170130456 → 1.0.2178444470

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.
@@ -4,8 +4,10 @@ declare class InfoboxComponent extends GirafeHTMLElement {
4
4
  template: () => import("uhtml").Hole;
5
5
  infos: InfoBoxContent[];
6
6
  urlRegExp: RegExp;
7
+ private readonly autoCloseTimers;
7
8
  constructor();
8
9
  registerEvents(): void;
10
+ private setupAutoClose;
9
11
  closeMessage(info: InfoBoxContent): void;
10
12
  linkify(str: string): string;
11
13
  connectedCallback(): void;
@@ -11,14 +11,45 @@ class InfoboxComponent extends GirafeHTMLElement {
11
11
  };
12
12
  infos = [];
13
13
  urlRegExp = /(https?:\/\/[^"<]*?(?=\s|$|<\/[^a]>))/gi;
14
+ // Each message can have a seperate timer or be persistent (until closed by user)
15
+ autoCloseTimers = new Map();
14
16
  constructor() {
15
17
  super('infobox');
16
18
  }
17
19
  registerEvents() {
18
- this.subscribe('infobox.elements', () => super.refreshRender());
20
+ this.subscribe('infobox.elements', () => {
21
+ super.refreshRender();
22
+ this.setupAutoClose();
23
+ });
24
+ }
25
+ setupAutoClose() {
26
+ const elements = this.state.infobox.elements ?? [];
27
+ elements.forEach((info) => {
28
+ const duration = info.duration;
29
+ // No duration specified (persistent)
30
+ if (!duration) {
31
+ return;
32
+ }
33
+ if (this.autoCloseTimers.has(info.id)) {
34
+ return;
35
+ }
36
+ const timerId = globalThis.setTimeout(() => {
37
+ this.autoCloseTimers.delete(info.id);
38
+ this.closeMessage(info);
39
+ }, duration);
40
+ this.autoCloseTimers.set(info.id, timerId);
41
+ });
19
42
  }
20
43
  closeMessage(info) {
21
- this.state.infobox.elements.splice(this.state.infobox.elements.findIndex((el) => el.id === info.id), 1);
44
+ const timerId = this.autoCloseTimers.get(info.id);
45
+ if (timerId !== undefined) {
46
+ clearTimeout(timerId);
47
+ this.autoCloseTimers.delete(info.id);
48
+ }
49
+ const index = this.state.infobox.elements.findIndex((el) => el.id === info.id);
50
+ if (index !== -1) {
51
+ this.state.infobox.elements.splice(index, 1);
52
+ }
22
53
  }
23
54
  linkify(str) {
24
55
  if (this.urlRegExp.test(str)) {
@@ -1,4 +1,5 @@
1
- import type { BaseCustomizer, MFPLayer, MFPImageLayer, MFPMap, MFPWmtsLayer } from '@geoblocks/mapfishprint';
1
+ import type { BaseCustomizer, MFPLayer, MFPMap, MFPWmtsLayer, MFPWmsLayer } from '@geoblocks/mapfishprint';
2
+ import GroupLayer from '../../../models/layers/grouplayer';
2
3
  import type BaseLayer from '../../../models/layers/baselayer';
3
4
  import MapManager from '../../../tools/state/mapManager';
4
5
  import State from '../../../tools/state/state';
@@ -53,16 +54,25 @@ export default class MFPEncoder {
53
54
  * @returns A promise that resolves to an array of MFP layers, a single MFP layer, or null.
54
55
  */
55
56
  encodeLayer(layer: BaseLayer): MFPLayer[] | MFPLayer | null;
57
+ /**
58
+ * Only non mixed groups of WMS layers are supported.
59
+ *
60
+ * Will convert a group of WMS layers sharing the same OGC server into a single MFPWmsLayer
61
+ * where its 'layers' property is a concatenation of all child layers' 'layers' property.
62
+ *
63
+ * @param groupLayer
64
+ */
65
+ encodeGroupLayer(groupLayer: GroupLayer): MFPWmsLayer | null;
56
66
  /**
57
67
  * Encodes an image layer from a WMS layer object.
58
68
  * @returns The encoded image layer or null if the layer is not visible.
59
69
  */
60
- encodeImageLayer(layerWms: LayerWms): MFPImageLayer | null;
70
+ encodeWmsLayer(layerWms: LayerWms): MFPWmsLayer | null;
61
71
  /**
62
- * Encodes a WMTS layer into a MFPWmtsLayer or MFPImageLayer object.
72
+ * Encodes a WMTS layer into a MFPWmtsLayer or MFPWmsLayer object.
63
73
  * @returns The encoded layer object, or null if the layer is not visible.
64
74
  */
65
- encodeTileWmtsLayer(layerWmts: LayerWmts): MFPWmtsLayer | MFPImageLayer | null;
75
+ encodeTileWmtsLayer(layerWmts: LayerWmts): MFPWmtsLayer | MFPWmsLayer | null;
66
76
  /**
67
77
  * Encodes a local file layer.
68
78
  * @returns The encoded local file layer or null if the layer is not visible.
@@ -72,5 +82,5 @@ export default class MFPEncoder {
72
82
  * Encodes a WMS layer from a WMTS layer.
73
83
  * @returns The encoded WMS layer or null if the ogcServer is missing.
74
84
  */
75
- encodeWmsFromWmtsLayer(layerWmts: LayerWmts): MFPImageLayer | null;
85
+ encodeWmsFromWmtsLayer(layerWmts: LayerWmts): MFPWmsLayer | null;
76
86
  }
@@ -1,3 +1,4 @@
1
+ import GroupLayer from '../../../models/layers/grouplayer';
1
2
  import { getAbsoluteUrl, getWmtsMatrices, getWmtsUrl, MFPVectorEncoder } from '@geoblocks/mapfishprint';
2
3
  import { toDegrees } from 'ol/math';
3
4
  import LayerLocalFile from '../../../models/layers/layerlocalfile';
@@ -59,9 +60,18 @@ export default class MFPEncoder {
59
60
  */
60
61
  getFlatLayers(baseLayers) {
61
62
  return baseLayers.reduce((layers, layer) => {
62
- if (layer.children) {
63
- const resultLayers = this.getFlatLayers(layer.children);
64
- layers.push(...resultLayers);
63
+ const children = layer.children;
64
+ if (children) {
65
+ const group = layer;
66
+ // If all the children have the same ogcServer and the group
67
+ // has no grandchildren, we keep the layers together
68
+ if (group.isMixed === false && group.hasGrandChildren === false) {
69
+ layers.push(group);
70
+ }
71
+ else {
72
+ const resultLayers = this.getFlatLayers(group.children);
73
+ layers.push(...resultLayers);
74
+ }
65
75
  }
66
76
  else {
67
77
  layers.push(layer);
@@ -113,22 +123,52 @@ export default class MFPEncoder {
113
123
  * @returns A promise that resolves to an array of MFP layers, a single MFP layer, or null.
114
124
  */
115
125
  encodeLayer(layer) {
116
- if (layer.className === LayerWms.name) {
117
- return this.encodeImageLayer(layer);
126
+ switch (layer.className) {
127
+ case GroupLayer.name:
128
+ return this.encodeGroupLayer(layer);
129
+ case LayerWms.name:
130
+ return this.encodeWmsLayer(layer);
131
+ case LayerWmts.name:
132
+ return this.encodeTileWmtsLayer(layer);
133
+ case LayerLocalFile.name:
134
+ return this.encodeLocalFileLayer(layer);
135
+ default:
136
+ console.warn('Unsupported layer type for encoding:', layer.className);
137
+ return null;
138
+ }
139
+ }
140
+ /**
141
+ * Only non mixed groups of WMS layers are supported.
142
+ *
143
+ * Will convert a group of WMS layers sharing the same OGC server into a single MFPWmsLayer
144
+ * where its 'layers' property is a concatenation of all child layers' 'layers' property.
145
+ *
146
+ * @param groupLayer
147
+ */
148
+ encodeGroupLayer(groupLayer) {
149
+ if (groupLayer.isMixed) {
150
+ console.error("A mixed group layer is passed to print and shouldn't", groupLayer);
151
+ return null;
152
+ }
153
+ if (groupLayer.hasGrandChildren) {
154
+ console.error("A group layer that has grandchildren is passed to print and shouldn't", groupLayer);
155
+ return null;
118
156
  }
119
- if (layer.className === LayerWmts.name) {
120
- return this.encodeTileWmtsLayer(layer);
157
+ const firstWmsLayer = groupLayer.children.shift();
158
+ const spec = this.encodeWmsLayer(firstWmsLayer);
159
+ if (!spec) {
160
+ return this.encodeGroupLayer(groupLayer);
121
161
  }
122
- if (layer.className === LayerLocalFile.name) {
123
- return this.encodeLocalFileLayer(layer);
162
+ for (const childLayer of groupLayer.children) {
163
+ spec.layers.push(childLayer.layers || '');
124
164
  }
125
- return null;
165
+ return spec;
126
166
  }
127
167
  /**
128
168
  * Encodes an image layer from a WMS layer object.
129
169
  * @returns The encoded image layer or null if the layer is not visible.
130
170
  */
131
- encodeImageLayer(layerWms) {
171
+ encodeWmsLayer(layerWms) {
132
172
  if (!isLayerVisible(layerWms, this.options?.printResolution)) {
133
173
  return null;
134
174
  }
@@ -170,7 +210,7 @@ export default class MFPEncoder {
170
210
  return object;
171
211
  }
172
212
  /**
173
- * Encodes a WMTS layer into a MFPWmtsLayer or MFPImageLayer object.
213
+ * Encodes a WMTS layer into a MFPWmtsLayer or MFPWmsLayer object.
174
214
  * @returns The encoded layer object, or null if the layer is not visible.
175
215
  */
176
216
  encodeTileWmtsLayer(layerWmts) {
@@ -236,6 +276,6 @@ export default class MFPEncoder {
236
276
  const printLayers = layerWmts.printLayers ?? layerWmts.wmsLayers ?? '';
237
277
  const layerWms = new LayerWms(0, printLayers, 0, layerWmts.ogcServer, { layers: printLayers });
238
278
  layerWms.opacity = layerWmts.opacity;
239
- return this.encodeImageLayer(layerWms);
279
+ return this.encodeWmsLayer(layerWms);
240
280
  }
241
281
  }
@@ -52,10 +52,20 @@ describe('MFPEncoder', () => {
52
52
  groupLayers[2].children = [layers[3], groupLayers[0]];
53
53
  groupLayers[3].children = [groupLayers[1], groupLayers[2]];
54
54
  topLevelGroup = [groupLayers[3]];
55
+ // Structure:
56
+ // topLevelGroup
57
+ // └─ groupLayers[3]
58
+ // ├─ groupLayers[1]
59
+ // │ └─ layers[2] (wms-2)
60
+ // └─ groupLayers[2]
61
+ // ├─ layers[3] (wmts)
62
+ // └─ groupLayers[0]
63
+ // ├─ layers[0] (wms-0)
64
+ // └─ layers[1] (wms-1)
55
65
  });
56
66
  it('getFlatLayersGroupLayers', () => {
57
67
  const result = encoder.getFlatLayers(topLevelGroup);
58
- expect(result.length).toBe(4); // 4 wms layers
68
+ expect(result.length).toBe(4);
59
69
  });
60
70
  it('encode the map', () => {
61
71
  context.stateManager.state.layers.layersList = topLevelGroup;
@@ -77,21 +87,21 @@ describe('MFPEncoder', () => {
77
87
  expect(result.dpi).toBe(defaultOptions.dpi);
78
88
  expect(result.scale).toBe(defaultOptions.scale);
79
89
  expect(result.rotation).toBe(180);
80
- expect(result.layers.length).toBe(6);
90
+ expect(result.layers.length).toBe(5);
81
91
  // Test order
82
92
  expect(result.layers[0].name).toBe('Test Vector layer');
83
- expect(result.layers[5].name).toBe('basemap-below');
93
+ expect(result.layers[4].name).toBe('basemap-below');
84
94
  });
85
95
  });
86
- describe('encodeImageLayer method', () => {
96
+ describe('encodeWmsLayer method', () => {
87
97
  it('should return null for not visible layer', () => {
88
98
  const layer = createTestLayerWms({ opacity: 0 });
89
- const result = encoder.encodeImageLayer(layer);
99
+ const result = encoder.encodeWmsLayer(layer);
90
100
  expect(result).toEqual(null);
91
101
  });
92
102
  it('should encode a wms layer', () => {
93
103
  const layer = createTestLayerWms({ layers: 'tree,plant', opacity: 0.6 });
94
- const result = encoder.encodeImageLayer(layer);
104
+ const result = encoder.encodeWmsLayer(layer);
95
105
  expect(result).toEqual({
96
106
  baseURL: 'https://ogc.test.url/',
97
107
  customParams: {
@@ -107,6 +117,32 @@ describe('MFPEncoder', () => {
107
117
  });
108
118
  });
109
119
  });
120
+ describe('encodeGroupLayer with same ogcServer', () => {
121
+ it('should encode a group layer', () => {
122
+ const ogcServer = createTestOgcServer();
123
+ context.stateManager.state.ogcServers = { [ogcServer.name]: ogcServer };
124
+ const layer1 = createTestLayerWms({ layers: 'tree', opacity: 0.6 });
125
+ const layer2 = createTestLayerWms({ layers: 'plant', opacity: 0.6 });
126
+ layer1.ogcServer = ogcServer;
127
+ layer2.ogcServer = ogcServer;
128
+ const groupLayer = createTestGroupLayer();
129
+ groupLayer.children = [layer1, layer2];
130
+ const result = encoder.encodeGroupLayer(groupLayer);
131
+ expect(result).toEqual({
132
+ baseURL: 'https://ogc.test.url/',
133
+ customParams: {
134
+ TRANSPARENT: 'TRUE'
135
+ },
136
+ imageFormat: 'image/png',
137
+ layers: ['tree', 'plant'],
138
+ opacity: 0.6,
139
+ serverType: 'mapserver',
140
+ styles: [''],
141
+ type: 'wms',
142
+ useNativeAngle: undefined
143
+ });
144
+ });
145
+ });
110
146
  describe('encodeTileWmtsLayer', () => {
111
147
  it('Testing encodeTileWmtsLayer method with inactive layer', () => {
112
148
  const layer = createTestLayerWmts({ opacity: 0 });
package/models/gmf.d.ts CHANGED
@@ -39,6 +39,7 @@ export interface GMFTreeItem {
39
39
  id: number;
40
40
  name: string;
41
41
  metadata?: GMFMetadata;
42
+ mixed?: boolean;
42
43
  ogcServer?: string;
43
44
  children?: GMFTreeItem[];
44
45
  type?: 'OSM' | 'WMS' | 'WMTS' | 'VectorTiles' | 'COG' | 'XYZ';
@@ -7,6 +7,7 @@ export type GroupLayerOptions = {
7
7
  metadataUrl?: string;
8
8
  isDefaultExpanded?: boolean;
9
9
  isExclusiveGroup?: boolean;
10
+ isMixed?: boolean;
10
11
  time?: ITimeOptions;
11
12
  timeAttribute?: string;
12
13
  };
@@ -20,6 +21,7 @@ declare class GroupLayer extends BaseLayer implements ILayerWithTime {
20
21
  */
21
22
  isExclusiveGroup: boolean;
22
23
  isExpanded: boolean;
24
+ _isMixed?: boolean;
23
25
  activeState: 'on' | 'off' | 'semi';
24
26
  timeOptions?: ITimeOptions;
25
27
  timeAttribute?: string;
@@ -31,6 +33,12 @@ declare class GroupLayer extends BaseLayer implements ILayerWithTime {
31
33
  get inactive(): boolean;
32
34
  get semiActive(): boolean;
33
35
  get hasTimeRestriction(): boolean;
36
+ get hasGrandChildren(): boolean;
34
37
  setDefaultTimeRestriction(): void;
38
+ /**
39
+ * Checks if all the children are LayerWms with the same ogcServer, opacity
40
+ * time
41
+ */
42
+ get isMixed(): boolean;
35
43
  }
36
44
  export default GroupLayer;
@@ -1,5 +1,6 @@
1
1
  import BaseLayer from './baselayer';
2
2
  import LayerTimeFormatter from '../../tools/time/layertimeformatter';
3
+ import LayerWms from './layerwms';
3
4
  class GroupLayer extends BaseLayer {
4
5
  /**
5
6
  * This class is a used in the state of the application, which will be accessed behind a javascript proxy.
@@ -10,6 +11,7 @@ class GroupLayer extends BaseLayer {
10
11
  */
11
12
  isExclusiveGroup;
12
13
  isExpanded;
14
+ _isMixed;
13
15
  activeState = 'off';
14
16
  timeOptions;
15
17
  timeAttribute;
@@ -19,6 +21,7 @@ class GroupLayer extends BaseLayer {
19
21
  super(id, name, order, options);
20
22
  this.isExpanded = options?.isDefaultExpanded || false;
21
23
  this.isExclusiveGroup = options?.isExclusiveGroup ?? false;
24
+ this._isMixed = options?.isMixed;
22
25
  this.timeOptions = options?.time;
23
26
  this.timeAttribute = options?.timeAttribute;
24
27
  this.setDefaultTimeRestriction();
@@ -30,6 +33,7 @@ class GroupLayer extends BaseLayer {
30
33
  disclaimer: this.disclaimer,
31
34
  isDefaultExpanded: this.isExpanded,
32
35
  isExclusiveGroup: this.isExclusiveGroup,
36
+ isMixed: this._isMixed,
33
37
  time: this.timeOptions,
34
38
  timeAttribute: this.timeAttribute
35
39
  };
@@ -56,11 +60,62 @@ class GroupLayer extends BaseLayer {
56
60
  get hasTimeRestriction() {
57
61
  return !!this.timeRestriction;
58
62
  }
63
+ get hasGrandChildren() {
64
+ for (const child of this.children) {
65
+ if (child.children?.length) {
66
+ return true;
67
+ }
68
+ }
69
+ return false;
70
+ }
59
71
  setDefaultTimeRestriction() {
60
72
  if (this.timeOptions) {
61
73
  const timeFormatter = new LayerTimeFormatter(this.timeOptions);
62
74
  this.timeRestriction = timeFormatter.getFormattedDefault();
63
75
  }
64
76
  }
77
+ /**
78
+ * Checks if all the children are LayerWms with the same ogcServer, opacity
79
+ * time
80
+ */
81
+ get isMixed() {
82
+ // It was explicitly set to true, it can't be unmixed
83
+ if (this._isMixed) {
84
+ return this._isMixed;
85
+ }
86
+ // If any child is not a GroupLayer or LayerWms, return true immediately
87
+ for (const child of this.children) {
88
+ if (!(child instanceof GroupLayer) && !(child instanceof LayerWms)) {
89
+ return true;
90
+ }
91
+ }
92
+ // Collect all LayerWms children and grandchildren recursively
93
+ const layerWmsList = [];
94
+ function collectLayerWms(layer) {
95
+ if (layer instanceof LayerWms) {
96
+ layerWmsList.push(layer);
97
+ }
98
+ else if (layer instanceof GroupLayer) {
99
+ for (const child of layer.children) {
100
+ collectLayerWms(child);
101
+ }
102
+ }
103
+ }
104
+ for (const child of this.children) {
105
+ collectLayerWms(child);
106
+ }
107
+ // Check if all LayerWms have the same properties
108
+ if (layerWmsList.length > 0) {
109
+ const first = layerWmsList[0];
110
+ const allSame = layerWmsList.every((lw) => lw.opacity === first.opacity &&
111
+ lw.ogcServer === first.ogcServer &&
112
+ lw.filter === first.filter &&
113
+ lw.timeRestriction === first.timeRestriction);
114
+ if (allSame) {
115
+ return false;
116
+ }
117
+ }
118
+ return true;
119
+ }
65
120
  }
66
121
  export default GroupLayer;
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.2170130456",
8
+ "version": "1.0.2178444470",
9
9
  "type": "module",
10
10
  "engines": {
11
11
  "node": ">=20.19.0"
@@ -1 +1 @@
1
- {"version":"1.0.2170130456", "build":"2170130456", "date":"20/11/2025"}
1
+ {"version":"1.0.2178444470", "build":"2178444470", "date":"25/11/2025"}
@@ -30,6 +30,7 @@ export type InfoBoxContent = {
30
30
  id: string;
31
31
  text: string;
32
32
  type: 'info' | 'warning' | 'error';
33
+ duration?: number;
33
34
  };
34
35
  export type Lidar = {
35
36
  line: OlGeomLineString | null;
@@ -294,6 +294,7 @@ class ThemesManager extends GirafeSingleton {
294
294
  disclaimer: elem.metadata?.disclaimer,
295
295
  isDefaultExpanded: elem.metadata?.isExpanded,
296
296
  isExclusiveGroup: elem.metadata?.exclusiveGroup,
297
+ isMixed: elem.mixed,
297
298
  time: elem.time
298
299
  };
299
300
  if (options.metadataUrl) {
@@ -181,6 +181,8 @@ export default class WmsClient {
181
181
  }
182
182
  const isBasemapLayer = layerWms.treeItemId in this.basemapLayers;
183
183
  const isLayerIndependent = layerWms.treeItemId in this.independentLayers;
184
+ // TODO SMS: if all layers share the same opacity, then mustBeIndependent can be false even if isTransparent is true.
185
+ // This will be changed in the future.
184
186
  const mustBeIndependent = layerWms.hasFilter || layerWms.hasTimeRestriction || layerWms.isTransparent || layerWms.swiped !== 'no';
185
187
  if (!isBasemapLayer) {
186
188
  if (isLayerIndependent && !mustBeIndependent) {