@geogirafe/lib-geoportal 1.2.0-dev.2836809138 → 1.2.0-dev.2844989778

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 (46) hide show
  1. package/api/apigeogirafeapp.js +4 -2
  2. package/api/style.css +4 -10
  3. package/assets/i18n/de.json +7 -3
  4. package/assets/i18n/en.json +4 -0
  5. package/assets/i18n/fr.json +4 -0
  6. package/assets/i18n/it.json +4 -0
  7. package/components/layout/component.d.ts +34 -3
  8. package/components/layout/component.js +66 -28
  9. package/components/layout/style.css +13 -0
  10. package/components/main.d.ts +1 -0
  11. package/components/main.js +1 -0
  12. package/components/map-2d/component.d.ts +0 -6
  13. package/components/map-2d/component.js +3 -22
  14. package/components/map-3d/component.d.ts +4 -2
  15. package/components/map-3d/component.js +7 -18
  16. package/components/map-grid/component.d.ts +70 -0
  17. package/components/map-grid/component.js +206 -0
  18. package/components/map-grid/style.css +99 -0
  19. package/components/menu-buttons-mobile/MenuMobile3dButton.js +3 -1
  20. package/package.json +1 -1
  21. package/styles/iframe.css +4 -10
  22. package/styles/index.css +4 -10
  23. package/styles/mobile.css +4 -10
  24. package/templates/iframe.html +2 -2
  25. package/templates/index.html +2 -2
  26. package/templates/mobile.html +2 -2
  27. package/templates/public/about.json +1 -1
  28. package/tools/app/geogirafeapp-mobile.js +2 -0
  29. package/tools/app/geogirafeapp.js +2 -0
  30. package/tools/configuration/girafeconfig.d.ts +4 -0
  31. package/tools/configuration/girafeconfig.js +20 -2
  32. package/tools/main.d.ts +3 -0
  33. package/tools/main.js +2 -0
  34. package/tools/share/serializers/globeserializer.d.ts +2 -0
  35. package/tools/share/serializers/globeserializer.js +26 -4
  36. package/tools/share/serializers/layoutserializer.d.ts +17 -0
  37. package/tools/share/serializers/layoutserializer.js +44 -0
  38. package/tools/share/stateserializer.js +3 -0
  39. package/tools/state/globe.d.ts +0 -1
  40. package/tools/state/globe.js +0 -1
  41. package/tools/state/layout.d.ts +62 -0
  42. package/tools/state/layout.js +67 -0
  43. package/tools/state/state.d.ts +2 -0
  44. package/tools/state/state.js +8 -0
  45. package/tools/state/statemanager.d.ts +1 -0
  46. package/tools/state/statemanager.js +8 -0
@@ -0,0 +1,206 @@
1
+ import { html as uHtml } from 'uhtml';
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import GirafeHTMLElement from '../../base/GirafeHTMLElement.js';
4
+ import { AVAILABLE_CELL_COMPONENTS, getGridSize, MAX_CELL_SIZE, MIN_CELL_SIZE } from '../../tools/state/layout.js';
5
+ /**
6
+ * The Grid in which the Map Components are displayed.
7
+ *
8
+ * The Components are written as children of this Element in the HTML of the application
9
+ * and are moved into the Cells of the Grid, depending on <code>state.layout</code>.
10
+ * A Component that is not placed in any Cell is not slotted, and therefore not displayed.
11
+ */
12
+ class MapGridComponent extends GirafeHTMLElement {
13
+ templateUrl = null;
14
+ styleUrls = null;
15
+ template = () => {
16
+ return uHtml `<style>
17
+ .hidden{display:none!important}.gg-rotate90{transform:rotate(90deg)}.gg-rotate180{transform:rotate(180deg)}.gg-rotate270{transform:rotate(270deg)}img{filter:var(--svg-filter)}img.legend-image{filter:var(--svg-map-filter);background:var(--svg-legend-bkg)}div{scrollbar-width:thin}a,a:visited{color:var(--link-color)}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes spin-wait{0%{transform:rotate(0)}7%{transform:rotate(360deg)}to{transform:rotate(360deg)}}.gg-spin{animation-name:spin;animation-duration:2s;animation-timing-function:linear;animation-iteration-count:infinite}.gg-spin-wait{animation-name:spin-wait;animation-duration:10s;animation-timing-function:linear;animation-iteration-count:infinite}::-webkit-scrollbar{width:5px}::-webkit-scrollbar-thumb{background:#999}button,input,select,textarea{font:inherit}.gg-button,.gg-select,.gg-input,.gg-textarea{background-color:var(--bkg-color);color:var(--text-color);border:var(--app-standard-border);box-sizing:border-box;cursor:pointer;border-radius:3px;outline:0;margin:0;padding:0 0 0 .5rem;display:inline-block}.gg-label{background-color:var(--bkg-color);color:var(--text-color);border:none;align-items:center;margin:0;padding:0;display:flex}.gg-button,.gg-select,.gg-input,.gg-label{min-height:calc(var(--app-standard-height) / 1.5)}.gg-textarea{max-height:initial;resize:vertical;height:6rem;padding:.5rem;line-height:1.3rem}.gg-input{cursor:text}.gg-checkbox{accent-color:var(--text-color);cursor:pointer;width:1.2rem}.gg-range{accent-color:var(--text-color)}.gg-button{padding:0 .5rem}.gg-button.active{border:solid 1px var(--text-color-grad1);background-color:var(--bkg-color-grad2)}.gg-button:disabled{background-color:var(--bkg-color-grad1);color:var(--text-color-grad2);cursor:not-allowed;border:none}.gg-button:disabled img{filter:opacity(.6)}.gg-input:disabled,.gg-select:disabled,.gg-textarea:disabled{background-color:var(--bkg-color-grad1);color:var(--text-color-grad2);cursor:not-allowed}.gg-button>img{vertical-align:middle}.gg-icon-button{color:var(--text-color);cursor:pointer;background-color:#0000;border:none;flex-direction:column;justify-content:center;align-items:center;padding:0;display:flex}.gg-icon{justify-content:center;align-items:center;display:flex}.gg-big,.gg-big-withtext{min-width:var(--app-standard-height);min-height:var(--app-standard-height);max-height:var(--app-standard-height)}.gg-big img,.gg-big-withtext img{width:calc(var(--app-standard-height) - 1.5rem);margin:0}.gg-big-withtext span{font-variant:small-caps;padding:0 1rem;font-size:.9rem}.gg-medium,.gg-medium-withtext{min-width:calc(var(--app-standard-height) / 1.2);min-height:calc(var(--app-standard-height) / 1.2);max-height:calc(var(--app-standard-height) / 1.2);flex-direction:row}.gg-medium img{width:calc(var(--app-standard-height) / 2.4);margin:0}.gg-medium-withtext img{width:calc(var(--app-standard-height) / 2.4);margin-left:.5rem}.gg-medium-withtext span{padding:0 1rem 0 .5rem;font-size:.9rem}.gg-small,.gg-small-withtext{min-width:calc(var(--app-standard-height) / 2);min-height:calc(var(--app-standard-height) / 2);max-height:calc(var(--app-standard-height) / 2);flex-direction:row}.gg-small img{width:calc(var(--app-standard-height) / 3);margin:0}.gg-small-withtext img{width:calc(var(--app-standard-height) / 3);margin-left:.5rem}.gg-small-withtext span{padding:0 .5rem 0 .3rem;font-size:.9rem}.gg-button:hover:not(:disabled),.gg-select:hover:not(:disabled),.gg-input:hover:not(:disabled),.gg-textarea:hover:not(:disabled),.gg-icon-button:hover:not(:disabled){background-color:var(--bkg-color-grad1)}.gg-opacity{opacity:.5}.gg-opacity:hover{opacity:1;background-color:#0000}.gg-tabs{cursor:pointer;grid-auto-flow:column;padding-bottom:1rem;font-size:1rem;display:grid}.gg-tab{border:none;border-bottom:var(--app-standard-border);cursor:pointer;color:var(--text-color);background:0 0;padding:.5rem}.gg-tab.active{border-bottom:solid 1px var(--text-color)}.girafe-button-big,.girafe-button-large,.girafe-button-small,.girafe-button-tiny{color:var(--text-color);background-color:#0000;border:none;flex-direction:column;display:flex}.girafe-button-big:hover,.girafe-button-large:hover,.girafe-button-small:hover,.girafe-button-tiny:hover{background-color:var(--bkg-color-grad1);cursor:pointer}.girafe-button-big.dark,.girafe-button-large.dark,.girafe-button-small.dark,.girafe-button-tiny.dark{background-color:var(--bkg-color);filter:invert()}.girafe-button-big{width:var(--app-standard-height);height:var(--app-standard-height);align-items:center;padding:1rem}.girafe-button-big img{overflow:hidden}.girafe-button-large{flex-direction:row}.girafe-button-large img{height:2rem;margin:.3rem}.girafe-button-large span{height:2rem;margin:.3rem;line-height:2rem}.girafe-button-small{min-width:calc(var(--app-standard-height) / 2);height:calc(var(--app-standard-height) / 2);align-items:center;padding:.5rem}.girafe-button-small img{overflow:hidden}.girafe-button-small span{text-align:left;text-overflow:ellipsis;width:100%;overflow:hidden}.girafe-button-tiny{align-items:center;width:1rem;height:1rem;padding:0}.girafe-button-tiny img{overflow:hidden}.girafe-onboarding-theme{background-color:var(--bkg-color)!important;color:var(--text-color)!important}.girafe-onboarding-theme button{background-color:var(--bkg-color)!important;color:var(--text-color)!important;text-shadow:none!important}.girafe-onboarding-theme button.driver-popover-close-btn{z-index:10000}.tippy-box{background-color:var(--bkg-color)!important;border:var(--app-standard-border)!important}.tippy-content{color:var(--text-color)!important}.tippy-arrow{color:var(--bkg-color)!important}.tippy-box[data-theme~=error]{background-color:var(--error-color);white-space:pre-line}div.tippy-box[data-theme=image-popup]{border:var(--app-standard-border)!important;& div.tippy-content{background-color:var(--bkg-color)!important;& img{max-width:20rem;max-height:20rem}}& div.tippy-arrow{color:var(--bkg-color)!important}}
18
+ </style><style>
19
+ #grid{background-color:var(--bkg-color);width:100%;height:100%;display:grid;position:relative;overflow:hidden}.cell{min-width:0;min-height:0;position:relative;overflow:hidden}::slotted(*){width:100%;height:100%;display:block}.splitter{z-index:100;background:var(--bkg-color);border:var(--app-standard-border);touch-action:none;border-radius:1rem;position:absolute}.splitter-vertical{cursor:col-resize;width:6px;top:2rem;bottom:2rem;transform:translate(-50%)}.splitter-horizontal{cursor:row-resize;height:6px;left:2rem;right:2rem;transform:translateY(-50%)}.splitter:before,.splitter:after{content:"";background:var(--app-color-grey);position:absolute}.splitter-vertical:before,.splitter-vertical:after{width:1px;height:2rem;left:50%;transform:translate(-50%)}.splitter-vertical:before{height:calc(2rem + 1px);top:calc(-2rem - 1px)}.splitter-vertical:after{height:calc(2rem + 1px);bottom:calc(-2rem - 1px)}.splitter-horizontal:before,.splitter-horizontal:after{width:2rem;height:1px;top:50%;transform:translateY(-50%)}.splitter-horizontal:before{width:calc(2rem + 1px);left:calc(-2rem - 1px)}.splitter-horizontal:after{width:calc(2rem + 1px);right:calc(-2rem - 1px)}
20
+ </style>
21
+ <style>${this.customStyle}</style>
22
+ <div id="grid">${this.cellIndexes.map((index) => uHtml `<div class="cell"><slot name="${'cell-' + index}"></slot></div>`)} ${this.splitters.map((splitter) => uHtml `<div id="${this.getSplitterId(splitter)}" class="${'splitter splitter-' + splitter.orientation}" role="separator" onpointerdown="${(e) => this.onSplitterPointerDown(e, splitter)}" onpointermove="${(e) => this.onSplitterPointerMove(e, splitter)}" onpointerup="${(e) => this.onSplitterPointerUp(e, splitter)}"></div>`)}</div>
23
+ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
24
+ };
25
+ /** Index of each Cell of the current Layout, used to render the Cells and their slots. */
26
+ cellIndexes = [];
27
+ /** The splitters between the Cells of the current Layout. */
28
+ splitters = [];
29
+ /** The splitter that is currently dragged, if any. */
30
+ draggedSplitter = null;
31
+ constructor() {
32
+ super('map-grid');
33
+ }
34
+ get layout() {
35
+ return this.state.layout;
36
+ }
37
+ get grid() {
38
+ return this.getById('grid');
39
+ }
40
+ connectedCallback() {
41
+ super.connectedCallback();
42
+ this.render();
43
+ // The subscription on layout.type is called immediately, which builds the Grid.
44
+ this.registerEvents();
45
+ }
46
+ registerEvents() {
47
+ this.subscribe('layout.type', () => this.applyLayout());
48
+ this.subscribe('layout.cells', () => this.applyCells());
49
+ this.subscribe('layout.sizes.columns', () => this.applySizes());
50
+ this.subscribe('layout.sizes.rows', () => this.applySizes());
51
+ }
52
+ /**
53
+ * Rebuilds the Cells and the splitters of the Grid for the current Layout,
54
+ * then places the Components and applies the sizes.
55
+ */
56
+ applyLayout() {
57
+ const { columns, rows } = getGridSize(this.layout.type);
58
+ this.cellIndexes = Array.from({ length: columns * rows }, (_, index) => index);
59
+ this.splitters = this.createSplitters(columns, rows);
60
+ this.refreshRender();
61
+ this.applyCells();
62
+ this.applySizes();
63
+ }
64
+ /**
65
+ * Creates one splitter for each boundary between two columns and between two rows.
66
+ * The splitters go through the whole Grid, so that the columns and the rows keep
67
+ * the same sizes in every Cell of the Grid.
68
+ */
69
+ createSplitters(columns, rows) {
70
+ const splitters = [];
71
+ for (let index = 0; index < columns - 1; index++) {
72
+ splitters.push({ orientation: 'vertical', index: index, position: 0 });
73
+ }
74
+ for (let index = 0; index < rows - 1; index++) {
75
+ splitters.push({ orientation: 'horizontal', index: index, position: 0 });
76
+ }
77
+ return splitters;
78
+ }
79
+ /**
80
+ * Moves each Component into the Cell it is configured for, by setting the name of the slot
81
+ * it must be displayed in. Components that are not placed in any Cell get no slot,
82
+ * which means that they are not displayed at all.
83
+ */
84
+ applyCells() {
85
+ const cells = this.layout.cells;
86
+ for (const component of AVAILABLE_CELL_COMPONENTS) {
87
+ const element = this.querySelector(component.tagName);
88
+ if (!element) {
89
+ continue;
90
+ }
91
+ const cellIndex = cells.indexOf(component.id);
92
+ if (cellIndex >= 0) {
93
+ element.setAttribute('slot', `cell-${cellIndex}`);
94
+ }
95
+ else {
96
+ element.removeAttribute('slot');
97
+ }
98
+ }
99
+ }
100
+ /** Applies the sizes of the columns and of the rows to the Grid and to the splitters. */
101
+ applySizes() {
102
+ const grid = this.grid;
103
+ if (!grid) {
104
+ return;
105
+ }
106
+ const { columns, rows } = this.layout.sizes;
107
+ grid.style.gridTemplateColumns = columns.map((size) => `${size}%`).join(' ');
108
+ grid.style.gridTemplateRows = rows.map((size) => `${size}%`).join(' ');
109
+ for (const splitter of this.splitters) {
110
+ const sizes = splitter.orientation === 'vertical' ? columns : rows;
111
+ splitter.position = sizes.slice(0, splitter.index + 1).reduce((sum, size) => sum + size, 0);
112
+ this.applySplitterPosition(splitter);
113
+ }
114
+ }
115
+ applySplitterPosition(splitter) {
116
+ const element = this.getById(this.getSplitterId(splitter));
117
+ if (!element) {
118
+ return;
119
+ }
120
+ // When the Layout changes, uHtml reuses the Elements of the previous splitters, which can have
121
+ // the other orientation. The position of the other orientation has therefore to be removed,
122
+ // otherwise it would override the size the splitter gets from its stylesheet.
123
+ const isVertical = splitter.orientation === 'vertical';
124
+ element.style.left = isVertical ? `${splitter.position}%` : '';
125
+ element.style.top = isVertical ? '' : `${splitter.position}%`;
126
+ }
127
+ getSplitterId(splitter) {
128
+ return `splitter-${splitter.orientation}-${splitter.index}`;
129
+ }
130
+ onSplitterPointerDown(event, splitter) {
131
+ event.preventDefault();
132
+ this.draggedSplitter = splitter;
133
+ event.target.setPointerCapture(event.pointerId);
134
+ }
135
+ /**
136
+ * Resizes the two columns (or the two rows) around the dragged splitter.
137
+ * While dragging, the new sizes are only applied to the DOM: the state is updated
138
+ * at the end of the drag, to prevent flooding it with intermediate values.
139
+ */
140
+ onSplitterPointerMove(event, splitter) {
141
+ if (this.draggedSplitter !== splitter) {
142
+ return;
143
+ }
144
+ const gridBounds = this.grid.getBoundingClientRect();
145
+ const isVertical = splitter.orientation === 'vertical';
146
+ const position = isVertical
147
+ ? ((event.clientX - gridBounds.left) / gridBounds.width) * 100
148
+ : ((event.clientY - gridBounds.top) / gridBounds.height) * 100;
149
+ const sizes = this.getResizedSizes(splitter, position);
150
+ if (!sizes) {
151
+ return;
152
+ }
153
+ splitter.position = sizes.slice(0, splitter.index + 1).reduce((sum, size) => sum + size, 0);
154
+ this.applySplitterPosition(splitter);
155
+ const cssSizes = sizes.map((size) => `${size}%`).join(' ');
156
+ if (isVertical) {
157
+ this.grid.style.gridTemplateColumns = cssSizes;
158
+ }
159
+ else {
160
+ this.grid.style.gridTemplateRows = cssSizes;
161
+ }
162
+ }
163
+ onSplitterPointerUp(event, splitter) {
164
+ if (this.draggedSplitter !== splitter) {
165
+ return;
166
+ }
167
+ this.draggedSplitter = null;
168
+ event.target.releasePointerCapture(event.pointerId);
169
+ const sizes = this.getResizedSizes(splitter, splitter.position);
170
+ if (sizes) {
171
+ if (splitter.orientation === 'vertical') {
172
+ this.layout.sizes.columns = sizes;
173
+ }
174
+ else {
175
+ this.layout.sizes.rows = sizes;
176
+ }
177
+ }
178
+ }
179
+ /**
180
+ * Computes the new sizes of the columns (or of the rows) when the splitter is moved
181
+ * to the given position, in percent of the Grid. The two resized columns (or rows) are
182
+ * kept between MIN_CELL_SIZE and MAX_CELL_SIZE, the other ones keep their size.
183
+ */
184
+ getResizedSizes(splitter, position) {
185
+ const sizes = [
186
+ ...(splitter.orientation === 'vertical' ? this.layout.sizes.columns : this.layout.sizes.rows)
187
+ ];
188
+ const before = splitter.index;
189
+ const after = splitter.index + 1;
190
+ // Size the splitter can be dragged in, without taking the columns/rows that are not resized into account.
191
+ const offset = sizes.slice(0, before).reduce((sum, size) => sum + size, 0);
192
+ const available = sizes[before] + sizes[after];
193
+ let newSizeBefore = position - offset;
194
+ newSizeBefore = Math.max(MIN_CELL_SIZE, Math.min(MAX_CELL_SIZE, newSizeBefore));
195
+ newSizeBefore = Math.min(newSizeBefore, available - MIN_CELL_SIZE);
196
+ newSizeBefore = Math.max(newSizeBefore, available - MAX_CELL_SIZE);
197
+ sizes[before] = newSizeBefore;
198
+ sizes[after] = available - newSizeBefore;
199
+ return sizes;
200
+ }
201
+ render() {
202
+ super.render();
203
+ super.girafeTranslate();
204
+ }
205
+ }
206
+ export default MapGridComponent;
@@ -0,0 +1,99 @@
1
+ /* SPDX-License-Identifier: Apache-2.0 */
2
+ #grid {
3
+ display: grid;
4
+ position: relative;
5
+ width: 100%;
6
+ height: 100%;
7
+ overflow: hidden;
8
+ background-color: var(--bkg-color);
9
+ }
10
+
11
+ .cell {
12
+ position: relative;
13
+ overflow: hidden;
14
+ /* Prevents the Components from growing the Cells over the sizes defined by the Grid. */
15
+ min-width: 0;
16
+ min-height: 0;
17
+ }
18
+
19
+ ::slotted(*) {
20
+ display: block;
21
+ width: 100%;
22
+ height: 100%;
23
+ }
24
+
25
+ /* The splitters look like the gutters used to resize the panels (see #gutter of the lr-panel),
26
+ but they go through the whole Grid, with a 2rem gap at both ends,
27
+ and are centered on the boundary they can move. */
28
+ .splitter {
29
+ position: absolute;
30
+ z-index: 100;
31
+ background: var(--bkg-color);
32
+ border: var(--app-standard-border);
33
+ border-radius: 1rem;
34
+ touch-action: none;
35
+ }
36
+
37
+ .splitter-vertical {
38
+ top: 2rem;
39
+ bottom: 2rem;
40
+ width: 6px;
41
+ /* Centers the splitter on the boundary, whatever its borders add to its width. */
42
+ transform: translateX(-50%);
43
+ cursor: col-resize;
44
+ }
45
+
46
+ .splitter-horizontal {
47
+ left: 2rem;
48
+ right: 2rem;
49
+ height: 6px;
50
+ transform: translateY(-50%);
51
+ cursor: row-resize;
52
+ }
53
+
54
+ /* The 2rem at both ends that the splitter itself does not cover are filled with a thin line,
55
+ so that the boundary between the cells stays visible over the whole Grid. */
56
+ .splitter::before,
57
+ .splitter::after {
58
+ content: '';
59
+ position: absolute;
60
+ background: var(--app-color-grey);
61
+ }
62
+
63
+ .splitter-vertical::before,
64
+ .splitter-vertical::after {
65
+ left: 50%;
66
+ width: 1px;
67
+ height: 2rem;
68
+ transform: translateX(-50%);
69
+ }
70
+
71
+ /* The lines are placed against the padding box of the splitter, which is inset by its border:
72
+ they are made 1px longer, so that they reach the edge of the Grid. */
73
+ .splitter-vertical::before {
74
+ top: calc(-2rem - 1px);
75
+ height: calc(2rem + 1px);
76
+ }
77
+
78
+ .splitter-vertical::after {
79
+ bottom: calc(-2rem - 1px);
80
+ height: calc(2rem + 1px);
81
+ }
82
+
83
+ .splitter-horizontal::before,
84
+ .splitter-horizontal::after {
85
+ top: 50%;
86
+ height: 1px;
87
+ width: 2rem;
88
+ transform: translateY(-50%);
89
+ }
90
+
91
+ .splitter-horizontal::before {
92
+ left: calc(-2rem - 1px);
93
+ width: calc(2rem + 1px);
94
+ }
95
+
96
+ .splitter-horizontal::after {
97
+ right: calc(-2rem - 1px);
98
+ width: calc(2rem + 1px);
99
+ }
@@ -18,7 +18,9 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
18
18
  super('mobile-3d-button');
19
19
  }
20
20
  onPointerUp() {
21
- this.state.globe.display = this.state.globe.display === '3D' ? '2D' : '3D';
21
+ // On mobile, the Grid only has one Cell, in which the 2D and the 3D Map are exchanged.
22
+ const cells = this.state.layout.cells;
23
+ this.state.layout.cells = [cells[0] === 'map-3d' ? 'map-2d' : 'map-3d'];
22
24
  }
23
25
  connectedCallback() {
24
26
  super.connectedCallback();
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "GeoGirafe PSC",
6
6
  "url": "https://doc.geogirafe.org"
7
7
  },
8
- "version": "1.2.0-dev.2836809138",
8
+ "version": "1.2.0-dev.2844989778",
9
9
  "type": "module",
10
10
  "engines": {
11
11
  "node": ">=20.19.0"
package/styles/iframe.css CHANGED
@@ -39,27 +39,21 @@ body {
39
39
  overflow-y: hidden;
40
40
  }
41
41
 
42
- #maps {
42
+ girafe-map-grid {
43
+ display: block;
43
44
  position: relative;
44
45
  width: 100%;
45
46
  height: 100%;
46
47
  overflow: hidden;
47
48
  }
48
49
 
49
- girafe-map-2d {
50
+ girafe-map-2d,
51
+ girafe-map-3d {
50
52
  display: block;
51
53
  position: relative;
52
54
  width: 100%;
53
55
  height: 100%;
54
- }
55
-
56
- girafe-map-3d {
57
- display: none;
58
- position: absolute;
59
- top: 0;
60
- height: 100%;
61
56
  overflow: hidden;
62
- border-left: solid 3px #444;
63
57
  }
64
58
 
65
59
  girafe-basemap {
package/styles/index.css CHANGED
@@ -190,27 +190,21 @@ girafe-lr-panel > * {
190
190
  height: 100%;
191
191
  }
192
192
 
193
- #maps {
193
+ girafe-map-grid {
194
+ display: block;
194
195
  position: relative;
195
196
  width: 100%;
196
197
  flex-grow: 1;
197
198
  overflow: hidden;
198
199
  }
199
200
 
200
- girafe-map-2d {
201
+ girafe-map-2d,
202
+ girafe-map-3d {
201
203
  display: block;
202
204
  position: relative;
203
205
  width: 100%;
204
206
  height: 100%;
205
- }
206
-
207
- girafe-map-3d {
208
- display: none;
209
- position: absolute;
210
- top: 0;
211
- height: 100%;
212
207
  overflow: hidden;
213
- border-left: solid 3px #444;
214
208
  }
215
209
 
216
210
  .info-area {
package/styles/mobile.css CHANGED
@@ -91,27 +91,21 @@ girafe-search {
91
91
  right: 0;
92
92
  }
93
93
 
94
- #maps {
94
+ girafe-map-grid {
95
+ display: block;
95
96
  position: relative;
96
97
  width: 100%;
97
98
  height: 100%;
98
99
  overflow: hidden;
99
100
  }
100
101
 
101
- girafe-map-2d {
102
+ girafe-map-2d,
103
+ girafe-map-3d {
102
104
  display: block;
103
105
  position: relative;
104
106
  width: 100%;
105
107
  height: 100%;
106
- }
107
-
108
- girafe-map-3d {
109
- display: none;
110
- position: absolute;
111
- top: 0;
112
- height: 100%;
113
108
  overflow: hidden;
114
- border-left: solid 3px #444;
115
109
  }
116
110
 
117
111
  girafe-infobox {
@@ -111,10 +111,10 @@
111
111
 
112
112
  <div id="content">
113
113
  <div id="main">
114
- <div id="maps">
114
+ <girafe-map-grid>
115
115
  <girafe-map-2d></girafe-map-2d>
116
116
  <girafe-map-3d></girafe-map-3d>
117
- </div>
117
+ </girafe-map-grid>
118
118
  <girafe-basemap></girafe-basemap>
119
119
  <girafe-search id="search"></girafe-search>
120
120
  </div>
@@ -140,10 +140,10 @@
140
140
  <girafe-tree-view slot="main"></girafe-tree-view>
141
141
  </girafe-lr-panel>
142
142
  <div id="content-map">
143
- <div id="maps">
143
+ <girafe-map-grid>
144
144
  <girafe-map-2d></girafe-map-2d>
145
145
  <girafe-map-3d></girafe-map-3d>
146
- </div>
146
+ </girafe-map-grid>
147
147
  <div class="info-area">
148
148
  <girafe-basemap data-tour="basemap-selection"></girafe-basemap>
149
149
  <girafe-infobox></girafe-infobox>
@@ -92,10 +92,10 @@
92
92
  <girafe-search></girafe-search>
93
93
  <girafe-modals></girafe-modals>
94
94
  <div id="content">
95
- <div id="maps">
95
+ <girafe-map-grid>
96
96
  <girafe-map-2d></girafe-map-2d>
97
97
  <girafe-map-3d></girafe-map-3d>
98
- </div>
98
+ </girafe-map-grid>
99
99
  <girafe-infobox></girafe-infobox>
100
100
  </div>
101
101
  <girafe-swipe-up-panel>
@@ -1 +1 @@
1
- {"version":"1.2.0-dev.2836809138", "build":"2836809138", "date":"10/09/2026"}
1
+ {"version":"1.2.0-dev.2844989778", "build":"2844989778", "date":"13/09/2026"}
@@ -11,6 +11,7 @@ import InfoboxComponent from '../../components/infobox/component.js';
11
11
  import Map2dComponent from '../../components/map-2d/component.js';
12
12
  import DeprecatedMapComponent from '../../components/map-2d/deprecatedmapcomponent.js';
13
13
  import Map3dComponent from '../../components/map-3d/component.js';
14
+ import MapGridComponent from '../../components/map-grid/component.js';
14
15
  import MenuMobile3dButton from '../../components/menu-buttons-mobile/MenuMobile3dButton.js';
15
16
  import MenuMobileDrawingButton from '../../components/menu-buttons-mobile/MenuMobileDrawingButton.js';
16
17
  import MenuMobileOfflineButton from '../../components/menu-buttons-mobile/MenuMobileOfflineButton.js';
@@ -48,6 +49,7 @@ export default class GeoGirafeAppMobile extends GeoGirafeApp {
48
49
  customElements.define('girafe-infobox', InfoboxComponent);
49
50
  customElements.define('girafe-map-2d', Map2dComponent);
50
51
  customElements.define('girafe-map-3d', Map3dComponent);
52
+ customElements.define('girafe-map-grid', MapGridComponent);
51
53
  // Deprecated alias of <girafe-map-2d>, kept for the applications that were not migrated yet.
52
54
  customElements.define('girafe-map', DeprecatedMapComponent);
53
55
  customElements.define('girafe-search', MobileSearchComponent);
@@ -20,6 +20,7 @@ import LRPanelComponent from '../../components/lr-panel/component.js';
20
20
  import Map2dComponent from '../../components/map-2d/component.js';
21
21
  import DeprecatedMapComponent from '../../components/map-2d/deprecatedmapcomponent.js';
22
22
  import Map3dComponent from '../../components/map-3d/component.js';
23
+ import MapGridComponent from '../../components/map-grid/component.js';
23
24
  import MenuButtonComponent from '../../components/menubutton/component.js';
24
25
  import ModalsComponent from '../../components/modals/component.js';
25
26
  import NavigationComponent from '../../components/navigation/component.js';
@@ -121,6 +122,7 @@ export default class GeoGirafeApp {
121
122
  customElements.define('girafe-lr-panel', LRPanelComponent);
122
123
  customElements.define('girafe-map-2d', Map2dComponent);
123
124
  customElements.define('girafe-map-3d', Map3dComponent);
125
+ customElements.define('girafe-map-grid', MapGridComponent);
124
126
  // Deprecated alias of <girafe-map-2d>, kept for the applications that were not migrated yet.
125
127
  customElements.define('girafe-map', DeprecatedMapComponent);
126
128
  customElements.define('girafe-menu-button', MenuButtonComponent);
@@ -3,6 +3,7 @@ export declare enum HighlightMode {
3
3
  HighlightPerLayer = "highlight-per-layer"
4
4
  }
5
5
  import { Config as SanitizeConfig } from 'dompurify';
6
+ import { LayoutType } from '../state/layout.js';
6
7
  declare class GirafeConfig {
7
8
  general: {
8
9
  locale: string;
@@ -20,6 +21,8 @@ declare class GirafeConfig {
20
21
  defaultSelectionComponent: string;
21
22
  darkFrontendMode: boolean | undefined;
22
23
  darkMapMode: boolean;
24
+ layouts: LayoutType[];
25
+ defaultLayout: LayoutType;
23
26
  };
24
27
  themes: {
25
28
  url: string;
@@ -315,6 +318,7 @@ declare class GirafeConfig {
315
318
  private initConfigThemes;
316
319
  private initConfigLanguages;
317
320
  private initConfigInterface;
321
+ private initConfigLayouts;
318
322
  private initConfigGeneral;
319
323
  private initGmfOauth;
320
324
  private initConfigOauth;
@@ -5,6 +5,7 @@ export var HighlightMode;
5
5
  HighlightMode["HighlightPerLayer"] = "highlight-per-layer";
6
6
  })(HighlightMode || (HighlightMode = {}));
7
7
  import { defaultNewsSanitizeConfig, defaultPermalinkSanitizeConfig, defaultQuerySanitizeConfig } from './sanitizeconfig-defaults.js';
8
+ import { isLayoutType, LAYOUT_TYPES } from '../state/layout.js';
8
9
  class GirafeConfig {
9
10
  general;
10
11
  languages;
@@ -348,12 +349,29 @@ class GirafeConfig {
348
349
  const defaultConfig = {
349
350
  defaultSelectionComponent: 'window',
350
351
  darkFrontendMode: undefined,
351
- darkMapMode: false
352
+ darkMapMode: false,
353
+ layouts: [...LAYOUT_TYPES],
354
+ defaultLayout: LAYOUT_TYPES[0]
352
355
  };
353
- return {
356
+ const interfaceConfig = {
354
357
  ...defaultConfig,
355
358
  ...config.interface
356
359
  };
360
+ interfaceConfig.layouts = this.initConfigLayouts(interfaceConfig.layouts);
361
+ if (!interfaceConfig.layouts.includes(interfaceConfig.defaultLayout)) {
362
+ interfaceConfig.defaultLayout = interfaceConfig.layouts[0];
363
+ }
364
+ return interfaceConfig;
365
+ }
366
+ initConfigLayouts(layouts) {
367
+ const validLayouts = layouts.filter((layout) => {
368
+ if (isLayoutType(layout)) {
369
+ return true;
370
+ }
371
+ console.warn(`${layout} is not a valid layout and will be ignored. Valid layouts are: ${LAYOUT_TYPES.join(', ')}.`);
372
+ return false;
373
+ });
374
+ return validLayouts.length > 0 ? validLayouts : [...LAYOUT_TYPES];
357
375
  }
358
376
  initConfigGeneral(config) {
359
377
  return {
package/tools/main.d.ts CHANGED
@@ -73,6 +73,7 @@ export { default as CustomLayersSerializer } from './share/serializers/customlay
73
73
  export { default as GlobeSerializer } from './share/serializers/globeserializer.js';
74
74
  export { default as InterfaceSerializer } from './share/serializers/interfaceserializer.js';
75
75
  export { default as LayersConfigSerializer } from './share/serializers/layerconfigserializer.js';
76
+ export { default as LayoutSerializer } from './share/serializers/layoutserializer.js';
76
77
  export { default as MapPositionSerializer } from './share/serializers/mappositionserializer.js';
77
78
  export type { SharedInitialSelection } from './share/serializers/selectionserializer.js';
78
79
  export { default as SelectionSerializer } from './share/serializers/selectionserializer.js';
@@ -97,6 +98,8 @@ export { default as GraphicalInterface } from './state/graphicalInterface.js';
97
98
  export type { default as IGirafePanel } from './state/igirafepanel.js';
98
99
  export { isGirafePanel } from './state/igirafepanel.js';
99
100
  export { default as LayersConfig } from './state/layersConfig.js';
101
+ export type { LayoutType, CellComponent, CellContent } from './state/layout.js';
102
+ export { default as LayoutState, LAYOUT_TYPES, MIN_CELL_SIZE, MAX_CELL_SIZE, AVAILABLE_CELL_COMPONENTS, isLayoutType, isCellComponent, getGridSize, getDefaultSizes, distributeCells } from './state/layout.js';
100
103
  export { default as MapManager } from './state/mapManager.js';
101
104
  export type { MapMarkerTooltip, MapMarker } from './state/mapposition.js';
102
105
  export { default as MapPosition } from './state/mapposition.js';
package/tools/main.js CHANGED
@@ -57,6 +57,7 @@ export { default as CustomLayersSerializer } from './share/serializers/customlay
57
57
  export { default as GlobeSerializer } from './share/serializers/globeserializer.js';
58
58
  export { default as InterfaceSerializer } from './share/serializers/interfaceserializer.js';
59
59
  export { default as LayersConfigSerializer } from './share/serializers/layerconfigserializer.js';
60
+ export { default as LayoutSerializer } from './share/serializers/layoutserializer.js';
60
61
  export { default as MapPositionSerializer } from './share/serializers/mappositionserializer.js';
61
62
  export { default as SelectionSerializer } from './share/serializers/selectionserializer.js';
62
63
  export { serializeWfsFilter, deserializeWfsFilter } from './share/serializers/wfsfilterserializer.js';
@@ -73,6 +74,7 @@ export { default as GlobeState } from './state/globe.js';
73
74
  export { default as GraphicalInterface } from './state/graphicalInterface.js';
74
75
  export { isGirafePanel } from './state/igirafepanel.js';
75
76
  export { default as LayersConfig } from './state/layersConfig.js';
77
+ export { default as LayoutState, LAYOUT_TYPES, MIN_CELL_SIZE, MAX_CELL_SIZE, AVAILABLE_CELL_COMPONENTS, isLayoutType, isCellComponent, getGridSize, getDefaultSizes, distributeCells } from './state/layout.js';
76
78
  export { default as MapManager } from './state/mapManager.js';
77
79
  export { default as MapPosition } from './state/mapposition.js';
78
80
  export { default as ObjectSelection } from './state/objectselection.js';
@@ -7,4 +7,6 @@ export default class GlobeSerializer implements IBrainSerializer<GlobeState> {
7
7
  private get globe();
8
8
  brainSerialize(globe: GlobeState): string;
9
9
  brainDeserialize(str: string): void;
10
+ /** Applies the layout a display mode of the previous versions corresponds to. */
11
+ private deserializeLegacyDisplay;
10
12
  }