@geogirafe/lib-geoportal 1.1.0-dev.2318696406 → 1.1.0-dev.2321895443

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 (42) hide show
  1. package/LICENSE +2 -0
  2. package/api/apicontext.d.ts +2 -0
  3. package/api/apicontext.js +4 -0
  4. package/assets/i18n/de.json +5 -1
  5. package/assets/i18n/en.json +4 -0
  6. package/assets/i18n/fr.json +4 -0
  7. package/assets/i18n/it.json +4 -0
  8. package/assets/icons/favorite-add.svg +1 -0
  9. package/assets/icons/favorite-no.svg +1 -0
  10. package/assets/icons/favorite-remove.svg +1 -0
  11. package/assets/icons/favorite-yes.svg +1 -0
  12. package/components/print/tools/MFPEncoder.js +1 -1
  13. package/components/print/tools/MFPEncoder.spec.js +6 -2
  14. package/components/print/tools/MFPLegendEncoder.js +1 -1
  15. package/components/print/tools/MFPLegendEncoder.spec.js +16 -1
  16. package/components/themes/component.d.ts +16 -4
  17. package/components/themes/component.js +65 -11
  18. package/models/gmf.d.ts +1 -1
  19. package/models/serverogc.d.ts +1 -1
  20. package/package.json +1 -1
  21. package/templates/public/about.json +1 -1
  22. package/tools/context/context.d.ts +2 -0
  23. package/tools/context/context.js +4 -0
  24. package/tools/context/icontext.d.ts +2 -0
  25. package/tools/legendhelper.d.ts +3 -2
  26. package/tools/legendhelper.js +7 -4
  27. package/tools/main.d.ts +1 -0
  28. package/tools/main.js +1 -0
  29. package/tools/ogcapi/demolayers.js +1 -1
  30. package/tools/ogcapi/ogcapiclient.spec.js +1 -1
  31. package/tools/ogcapi/ogcapifeaturesclient.spec.js +1 -1
  32. package/tools/ogcapi/ogcapifeaturesmanager.spec.js +1 -1
  33. package/tools/share/serializers/activebasemapsserializer.js +1 -2
  34. package/tools/tests/layerhelpers.d.ts +2 -0
  35. package/tools/tests/layerhelpers.js +14 -0
  36. package/tools/tests/mockcontext.d.ts +2 -0
  37. package/tools/tests/mockcontext.js +4 -0
  38. package/tools/themes/themefavoritesmanager.d.ts +17 -0
  39. package/tools/themes/themefavoritesmanager.js +53 -0
  40. package/tools/themes/themefavoritesmanager.spec.d.ts +1 -0
  41. package/tools/themes/themefavoritesmanager.spec.js +103 -0
  42. package/tools/wms/wmslegendhelper.js +3 -0
package/LICENSE CHANGED
@@ -237,6 +237,8 @@ From:
237
237
  - https://www.svgrepo.com/svg/309943/select-all-off
238
238
  - https://www.svgrepo.com/svg/310303/arrow-forward
239
239
  - https://www.svgrepo.com/svg/310306/arrow-import
240
+ - https://www.svgrepo.com/svg/310006/star
241
+ - https://www.svgrepo.com/svg/310008/star-add
240
242
 
241
243
 
242
244
  The MIT License (MIT)
@@ -30,6 +30,7 @@ import WmsManager from '../tools/wms/wmsmanager.js';
30
30
  import IGirafeContext from '../tools/context/icontext.js';
31
31
  import ApiSessionManager from './apisessionmanager.js';
32
32
  import OnBoardingManager from '../tools/onboarding/onboardingmanager.js';
33
+ import ThemeFavoritesManager from '../tools/themes/themefavoritesmanager.js';
33
34
  export default class GirafeApiContext implements IGirafeContext {
34
35
  readonly userDataManager: UserDataManager;
35
36
  readonly configManager: ConfigManager;
@@ -62,6 +63,7 @@ export default class GirafeApiContext implements IGirafeContext {
62
63
  readonly ogcApiFeaturesManager: OgcApiFeaturesManager;
63
64
  readonly localFileManager: LocalFileManager;
64
65
  readonly onBoardingManager: OnBoardingManager;
66
+ readonly themeFavoritesManager: ThemeFavoritesManager;
65
67
  constructor();
66
68
  initialize(): Promise<void>;
67
69
  }
package/api/apicontext.js CHANGED
@@ -29,6 +29,7 @@ import WfsManager from '../tools/wfs/wfsmanager.js';
29
29
  import WmsManager from '../tools/wms/wmsmanager.js';
30
30
  import ApiSessionManager from './apisessionmanager.js';
31
31
  import OnBoardingManager from '../tools/onboarding/onboardingmanager.js';
32
+ import ThemeFavoritesManager from '../tools/themes/themefavoritesmanager.js';
32
33
  export default class GirafeApiContext {
33
34
  userDataManager;
34
35
  configManager;
@@ -61,6 +62,7 @@ export default class GirafeApiContext {
61
62
  ogcApiFeaturesManager;
62
63
  localFileManager;
63
64
  onBoardingManager;
65
+ themeFavoritesManager;
64
66
  constructor() {
65
67
  this.componentManager = new ComponentManager(this);
66
68
  this.userDataManager = new UserDataManager(this);
@@ -93,6 +95,7 @@ export default class GirafeApiContext {
93
95
  this.localFileManager = new LocalFileManager(this);
94
96
  this.ogcApiFeaturesManager = new OgcApiFeaturesManager(this);
95
97
  this.onBoardingManager = new OnBoardingManager(this);
98
+ this.themeFavoritesManager = new ThemeFavoritesManager(this);
96
99
  }
97
100
  async initialize() {
98
101
  // NOTE : This initialization order is important, because some singleton will need other ones !
@@ -129,5 +132,6 @@ export default class GirafeApiContext {
129
132
  this.localFileManager.initializeSingleton();
130
133
  this.ogcApiFeaturesManager.initializeSingleton();
131
134
  this.onBoardingManager.initializeSingleton();
135
+ this.themeFavoritesManager.initializeSingleton();
132
136
  }
133
137
  }
@@ -54,7 +54,7 @@
54
54
  "Control opacity": "Deckkraft steuern",
55
55
  "Copy ShortUrl": "Kurz-URL kopieren",
56
56
  "Copy value": "Wert kopieren",
57
- "Create custom theme": "Benutzerdefiniertes Thema erstellen",
57
+ "Create custom theme": "Thema erstellen",
58
58
  "cross-section-settings": "Querschnitt",
59
59
  "CSV export": "CSV-Export",
60
60
  "Current Feature": "Aktuelles Objekt",
@@ -415,6 +415,10 @@
415
415
  "Theme selection": "Themenauswahl",
416
416
  "Themes": "Themen",
417
417
  "themes": "themen",
418
+ "themes-add-remove-favorites": "Zu Favoriten hinzufügen/entfernen",
419
+ "themes-type-all": "Alle Themen",
420
+ "themes-type-custom": "Benutzerdefinierte",
421
+ "themes-type-favorites": "Favoriten",
418
422
  "This instance of GeoGirafe cannot be used at the moment.": "Diese Instanz von GeoGirafe kann momentan nicht verwendet werden.",
419
423
  "Toggle all legends": "Alle Legenden umschalten",
420
424
  "Toggle legend": "Legende umschalten",
@@ -417,6 +417,10 @@
417
417
  "Theme selection": "Theme selection",
418
418
  "Themes": "Themes",
419
419
  "themes": "themes",
420
+ "themes-add-remove-favorites": "Add/Remove to Favorites",
421
+ "themes-type-all": "All Themes",
422
+ "themes-type-custom": "Custom Themes",
423
+ "themes-type-favorites": "Favorite Themes",
420
424
  "This instance of GeoGirafe cannot be used at the moment.": "This instance of GeoGirafe cannot be used at the moment.",
421
425
  "Toggle all legends": "Toggle all legends",
422
426
  "Toggle legend": "Toggle legend",
@@ -415,6 +415,10 @@
415
415
  "Theme selection": "Sélection du thème",
416
416
  "Themes": "Thèmes",
417
417
  "themes": "thèmes",
418
+ "themes-add-remove-favorites": "Ajouter aux favoris / retirer des favoris",
419
+ "themes-type-all": "Tous les thèmes",
420
+ "themes-type-custom": "Personnalisés",
421
+ "themes-type-favorites": "Favoris",
418
422
  "This instance of GeoGirafe cannot be used at the moment.": "Cette instance de GeoGirafe ne peut pas être utilisée pour le moment.",
419
423
  "Toggle all legends": "Afficher / Masquer toutes les légendes",
420
424
  "Toggle legend": "Afficher / Masquer la légende",
@@ -415,6 +415,10 @@
415
415
  "Theme selection": "Selezione del tema",
416
416
  "Themes": "Temi",
417
417
  "themes": "temi",
418
+ "themes-add-remove-favorites": "Aggiungi ai preferiti / rimuovi dai preferiti",
419
+ "themes-type-all": "Tutti i temi",
420
+ "themes-type-custom": "Personalizzati",
421
+ "themes-type-favorites": "Preferiti",
418
422
  "This instance of GeoGirafe cannot be used at the moment.": "Questa istanza di GeoGirafe non può essere utilizzata al momento.",
419
423
  "Toggle all legends": "Attiva / Disattiva tutte le legende",
420
424
  "Toggle legend": "Attiva / Disattiva legenda",
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" viewBox="0 0 24 24"><title>ic_fluent_star_add_24_regular</title><path fill="#212121" fill-rule="nonzero" d="M17.5 12a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11m-4.827-9.24 2.683 5.448 6.011.869a.75.75 0 0 1 .416 1.279l-1.368 1.333a6.5 6.5 0 0 0-1.583-.552l.815-.793-4.896-.708a.75.75 0 0 1-.566-.41L12 4.787 9.815 9.225a.75.75 0 0 1-.566.411l-4.895.707 3.545 3.45a.75.75 0 0 1 .216.665l-.84 4.873L11 17.37a6.6 6.6 0 0 0 .167 1.607l-4.541 2.392a.75.75 0 0 1-1.09-.79l1.032-5.986-4.352-4.236a.75.75 0 0 1 .416-1.28l6.01-.868 2.684-5.448a.75.75 0 0 1 1.346 0M17.5 14l-.09.007a.5.5 0 0 0-.402.402L17 14.5V17L14.498 17l-.09.008a.5.5 0 0 0-.402.402l-.008.09.008.09a.5.5 0 0 0 .402.402l.09.008H17v2.503l.008.09a.5.5 0 0 0 .402.402l.09.008.09-.008a.5.5 0 0 0 .402-.402l.008-.09V18l2.504.001.09-.008a.5.5 0 0 0 .402-.402l.008-.09-.008-.09a.5.5 0 0 0-.403-.402l-.09-.008H18v-2.5l-.008-.09a.5.5 0 0 0-.402-.403z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" viewBox="0 0 28 28"><title>ic_fluent_star_28_regular</title><path fill="#212121" fill-rule="nonzero" d="M14.437 3.103a1 1 0 0 1 .455.455l2.923 5.924 6.538.95a1 1 0 0 1 .555 1.706l-4.731 4.611 1.116 6.512a1 1 0 0 1-1.45 1.054l-5.848-3.074-5.848 3.074a1 1 0 0 1-1.45-1.054l1.116-6.512-4.731-4.611a1 1 0 0 1 .554-1.706l6.538-.95 2.924-5.924a1 1 0 0 1 1.34-.455m-3.267 7.75-6.316.918 4.57 4.455-1.078 6.29 5.649-2.97 5.649 2.97-1.08-6.29 4.571-4.455-6.316-.918-2.824-5.723z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" fill="#eab308" viewBox="0 0 24 24"><title>ic_fluent_star_add_24_regular</title><g fill="#212121" fill-rule="nonzero"><path d="M14.437 3.103a1 1 0 0 1 .455.455l2.923 5.924 6.538.95a1 1 0 0 1 .555 1.706l-4.731 4.611c-6.224 4.484-.045.03-6.182 4.492l-5.848 3.074a1 1 0 0 1-1.45-1.054l1.116-6.512-4.731-4.611a1 1 0 0 1 .554-1.706l6.538-.95 2.924-5.924a1 1 0 0 1 1.34-.455" class="UnoptimicedTransforms" style="display:inline;fill:#eab308;fill-opacity:1;stroke:none;stroke-opacity:1" transform="matrix(.82693 0 0 .81171 .481 .565)"/><path d="M17.5 12a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11m-4.827-9.24 2.683 5.448 6.011.869a.75.75 0 0 1 .416 1.279l-1.368 1.333a6.5 6.5 0 0 0-1.583-.552l.815-.793-4.896-.708a.75.75 0 0 1-.566-.41L12 4.787 9.815 9.225a.75.75 0 0 1-.566.411l-4.895.707 3.545 3.45a.75.75 0 0 1 .216.665l-.84 4.873L11 17.37a6.6 6.6 0 0 0 .167 1.607l-4.541 2.392a.75.75 0 0 1-1.09-.79l1.032-5.986-4.352-4.236a.75.75 0 0 1 .416-1.28l6.01-.868 2.684-5.448a.75.75 0 0 1 1.346 0M14.5 17a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1Z" style="display:inline"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" viewBox="0 0 28 28"><title>ic_fluent_star_28_regular</title><g fill="#212121" fill-rule="nonzero"><path d="M14.437 3.103a1 1 0 0 1 .455.455l2.923 5.924 6.538.95a1 1 0 0 1 .555 1.706l-4.731 4.611 1.116 6.512a1 1 0 0 1-1.45 1.054l-5.848-3.074-5.848 3.074a1 1 0 0 1-1.45-1.054l1.116-6.512-4.731-4.611a1 1 0 0 1 .554-1.706l6.538-.95 2.924-5.924a1 1 0 0 1 1.34-.455" class="UnoptimicedTransforms" style="display:inline;fill:#eab308;fill-opacity:1;stroke:none;stroke-opacity:1"/><path d="M14.437 3.103a1 1 0 0 1 .455.455l2.923 5.924 6.538.95a1 1 0 0 1 .555 1.706l-4.731 4.611 1.116 6.512a1 1 0 0 1-1.45 1.054l-5.848-3.074-5.848 3.074a1 1 0 0 1-1.45-1.054l1.116-6.512-4.731-4.611a1 1 0 0 1 .554-1.706l6.538-.95 2.924-5.924a1 1 0 0 1 1.34-.455m-3.267 7.75-6.316.918 4.57 4.455-1.078 6.29 5.649-2.97 5.649 2.97-1.08-6.29 4.571-4.455-6.316-.918-2.824-5.723z" class="UnoptimicedTransforms" style="display:inline"/><path d="M14.437 3.103a1 1 0 0 1 .455.455l2.923 5.924 6.538.95a1 1 0 0 1 .555 1.706l-4.731 4.611 1.116 6.512a1 1 0 0 1-1.45 1.054l-5.848-3.074-5.848 3.074a1 1 0 0 1-1.45-1.054l1.116-6.512-4.731-4.611a1 1 0 0 1 .554-1.706l6.538-.95 2.924-5.924a1 1 0 0 1 1.34-.455m-3.267 7.75-6.316.918 4.57 4.455-1.078 6.29 5.649-2.97 5.649 2.97-1.08-6.29 4.571-4.455-6.316-.918-2.824-5.723z"/></g></svg>
@@ -66,7 +66,7 @@ export default class MFPEncoder {
66
66
  // If all the children have the same ogcServer and the group
67
67
  // has no grandchildren, we keep the layers together
68
68
  if (group.isMixed === false && group.hasGrandChildren === false) {
69
- layers.push(group);
69
+ layers.push(...group.children);
70
70
  }
71
71
  else {
72
72
  const resultLayers = this.getFlatLayers(group.children);
@@ -87,10 +87,14 @@ describe('MFPEncoder', () => {
87
87
  expect(result.dpi).toBe(defaultOptions.dpi);
88
88
  expect(result.scale).toBe(defaultOptions.scale);
89
89
  expect(result.rotation).toBe(180);
90
- expect(result.layers.length).toBe(5);
90
+ expect(result.layers.length).toBe(6);
91
91
  // Test order
92
92
  expect(result.layers[0].name).toBe('Test Vector layer');
93
- expect(result.layers[4].name).toBe('basemap-below');
93
+ expect(result.layers[1].layers[0]).toBe('wms-2');
94
+ expect(result.layers[2].name).toBe('testWmts');
95
+ expect(result.layers[3].layers[0]).toBe('wms-0');
96
+ expect(result.layers[4].layers[0]).toBe('wms-1');
97
+ expect(result.layers[5].name).toBe('basemap-below');
94
98
  });
95
99
  });
96
100
  describe('encodeWmsLayer method', () => {
@@ -132,7 +132,7 @@ export class MFPLegendEncoder {
132
132
  }
133
133
  const layerNames = layerWms.layers?.split(',') ?? [];
134
134
  layerNames.forEach((name) => {
135
- const url = LegendHelper.getWMSLegendURL(layerWms.ogcServer.url, name, {
135
+ const url = LegendHelper.getWMSLegendURL(layerWms.ogcServer, name, {
136
136
  dpi,
137
137
  serverType,
138
138
  scale: this.options?.scale,
@@ -1,6 +1,6 @@
1
1
  import { it, describe, expect, beforeEach, afterAll } from 'vitest';
2
2
  import { MFPLegendEncoder } from './MFPLegendEncoder';
3
- import { createTestGroupLayer, createTestLayerWms, createTestLayerWmts, createTestOgcServer } from '../../../tools/tests/layerhelpers';
3
+ import { createTestGroupLayer, createTestLayerWms, createTestLayerWmsQGis, createTestLayerWmts, createTestOgcServer } from '../../../tools/tests/layerhelpers';
4
4
  import MockHelper from '../../../tools/tests/mockhelper';
5
5
  import { createOlWmtsLayer } from '../../../tools/tests/olhelpers';
6
6
  describe('MFPLegendEncoder', () => {
@@ -199,6 +199,21 @@ describe('MFPLegendEncoder', () => {
199
199
  });
200
200
  });
201
201
  });
202
+ describe('encodeLayerWmsLegendClasses QGis', () => {
203
+ let layer;
204
+ beforeEach(() => {
205
+ layer = createTestLayerWmsQGis();
206
+ });
207
+ it('should return MFPLegendClass for QGisServer (including LAYERTITLE)', () => {
208
+ layer.layers = 'foo';
209
+ const result = encoder.encodeLayerWmsLegendClasses(layer);
210
+ expect(result?.name).toEqual('testWmsQGis');
211
+ // One layer = no subclass, complete directly the current class.
212
+ expect(result?.icons).toEqual([
213
+ 'https://ogc.test.url?FORMAT=image%2Fpng&TRANSPARENT=TRUE&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetLegendGraphic&LAYER=foo&LAYERTITLE=False&SCALE=10000'
214
+ ]);
215
+ });
216
+ });
202
217
  it('getLegendClassForWMS', () => {
203
218
  const layerName = 'LayerName';
204
219
  const icon_dpi = {
@@ -1,22 +1,34 @@
1
1
  import GirafeHTMLElement from '../../base/GirafeHTMLElement.js';
2
2
  import ThemeLayer from '../../models/layers/themelayer.js';
3
- import Theme from '../../models/theme.js';
4
3
  import CustomTheme from '../../models/customtheme.js';
4
+ declare const ThemeTypes: readonly ["all", "favorites", "custom"];
5
+ type ThemeType = (typeof ThemeTypes)[number];
5
6
  declare class ThemeComponent extends GirafeHTMLElement {
6
7
  template: () => import("uhtml").Hole;
7
8
  newIcon: string;
8
9
  menuOpen: boolean;
9
10
  openedOnce: boolean;
11
+ activeThemeType: ThemeType;
12
+ clickOutsideContainer: HTMLElement | null;
10
13
  get customThemes(): CustomTheme[];
14
+ get allThemes(): Record<number, ThemeLayer>;
15
+ get favoriteThemes(): (ThemeLayer | CustomTheme)[];
16
+ get activeThemes(): Record<number, ThemeLayer> | (ThemeLayer | CustomTheme)[];
11
17
  constructor();
12
18
  registerEvents(): void;
19
+ protected render(): void;
13
20
  onBlur(): void;
14
21
  toggleThemesList(): void;
15
- onThemeChanged(theme: ThemeLayer): void;
16
- isThemeActive(theme: Theme): boolean;
22
+ activateThemeType(themeType: ThemeType): void;
23
+ onThemeChanged(theme: ThemeLayer | CustomTheme): void;
24
+ isThemeActive(theme: ThemeLayer | CustomTheme): boolean;
25
+ isThemeFavorite(theme: ThemeLayer | CustomTheme): boolean;
26
+ onThemeFavoriteChanged(theme: ThemeLayer | CustomTheme, e: Event): void;
27
+ isThemeTypeActive(themeType: ThemeType): boolean;
28
+ isCustomTheme(theme: ThemeLayer | CustomTheme): theme is CustomTheme;
17
29
  onCustomThemeChanged(customTheme: CustomTheme): void;
18
30
  onAddCustomTheme(): Promise<void>;
19
- onDeleteCustomTheme(themelayer: CustomTheme, e: Event): Promise<void>;
31
+ onDeleteCustomTheme(customTheme: CustomTheme, e: Event): Promise<void>;
20
32
  protected connectedCallback(): void;
21
33
  }
22
34
  export default ThemeComponent;
@@ -1,48 +1,86 @@
1
1
  import { htmlFor as uHtmlFor } from 'uhtml/keyed';
2
2
  import { html as uHtml } from 'uhtml';
3
3
  import GirafeHTMLElement from '../../base/GirafeHTMLElement.js';
4
+ import ThemeLayer from '../../models/layers/themelayer.js';
4
5
  import NewIcon from './images/new.svg';
6
+ import CustomTheme from '../../models/customtheme.js';
7
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
+ const ThemeTypes = ['all', 'favorites', 'custom'];
5
9
  class ThemeComponent extends GirafeHTMLElement {
6
10
  template = () => {
7
11
  return uHtml `<style>
8
12
  *{font-family:Arial,sans-serif}.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}.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);width:1.2rem}.gg-range{accent-color:var(--text-color)}.gg-button{padding:0 .5rem}.gg-button.active{border:solid 1px var(--text-color-grad2);background-color:var(--text-color-grad2);color:var(--bkg-color)}.gg-button:disabled{color:gray;cursor:not-allowed;background-color:#d3d3d3;border:none}.gg-input:disabled,.gg-select:disabled,.gg-textarea:disabled{color:gray;cursor:not-allowed;background-color:#d3d3d3}.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,.gg-select:hover,.gg-input:hover,.gg-textarea:hover,.gg-icon-button:hover{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}
9
13
  </style><style>
10
- .themes{background-color:var(--bkg-color);border:1px solid #444;border-radius:3px;flex-wrap:wrap;width:55rem;max-height:70vh;margin-top:0;padding:1rem;display:flex;position:absolute;overflow-y:auto}.theme,.custom-theme,.new-theme{cursor:pointer;background-color:#0000;border:none;border-radius:.5rem;align-content:flex-end;width:8rem;margin:.5rem;padding:0;position:relative}.themes>button>span,.custom-theme>button>span,.new-theme>button>span{text-align:center;width:95%;color:var(--text-color);display:inline-block}.themes>button>span,.custom-theme>button>span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.theme>img,.custom-theme>button>img,.new-theme>img{max-width:100%}button.select{cursor:pointer;background-color:#0000;border:none;width:100%;padding:0}button.select>img{height:3rem;margin-bottom:1rem}button.delete{position:absolute;top:0;right:0}.theme.selected,.custom-theme.selected{background-color:var(--bkg-color-grad2)}.theme-icon{--svg-filter:none}
14
+ .themes{background-color:var(--bkg-color);border:1px solid #444;border-radius:3px;flex-flow:column;width:58rem;max-height:70vh;margin-top:0;padding:1rem;display:flex;position:absolute;overflow-y:auto}#close-themes-menu{z-index:-1;background:0 0;width:100vw;height:100vh;display:none;position:absolute;top:0;left:0}#active-themes{flex-flow:wrap;flex:1;justify-content:flex-start;display:flex}.theme-card{border:var(--app-standard-border);background-color:#0000;border-radius:6px;flex-direction:column;align-items:center;width:7rem;height:9rem;margin:.25rem;padding:.25rem;display:flex;&:hover{cursor:pointer;background-color:var(--bkg-color-grad2)}& img.theme-favorite{content:url(icons/favorite-no.svg);align-self:flex-start;width:1.5rem;height:1.5rem;&:hover{content:url(icons/favorite-add.svg)}&.yes{content:url(icons/favorite-yes.svg);&:hover{content:url(icons/favorite-remove.svg)}}&.placeholder{content:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=)}}& img.delete-icon{content:url(icons/close.svg);align-self:flex-end;width:1.5rem;height:1.5rem;margin-top:-1.5rem;&.placeholder{content:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=)}}& img.theme-icon{max-width:5rem;max-height:2.5rem;margin-top:1.25rem;margin-bottom:1.25rem}&.selected{background-color:var(--bkg-color-grad1);border:solid 1px var(--text-color)}& span.theme-name{text-align:center;word-break:break-word;max-width:6rem;color:var(--text-color);max-height:2.2rem;line-height:1.1rem;overflow:hidden}}.theme,.custom-theme,.new-theme{cursor:pointer;background-color:#0000;border:none;border-radius:.5rem;align-content:flex-end;width:8rem;margin:.5rem;padding:0;position:relative}.themes>button>span,.custom-theme>button>span,.new-theme>button>span{text-align:center;width:95%;color:var(--text-color);display:inline-block}.themes>button>span,.custom-theme>button>span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.theme>img,.custom-theme>button>img,.new-theme>img{max-width:100%}button.select{cursor:pointer;background-color:#0000;border:none;width:100%;padding:0}button.select>img{height:3rem;margin-bottom:1rem}button.delete{position:absolute;top:0;right:0}.theme.selected,.custom-theme.selected{background-color:var(--bkg-color-grad2)}.theme-icon{--svg-filter:none}#themes-type-chooser{flex:1;justify-content:space-between;display:flex}.gg-tabs{flex-grow:1;margin:0}
11
15
  </style>
12
- <button class="gg-icon-button gg-big-withtext" tip="Theme selection" onclick="${() => this.toggleThemesList()}" onblur="${() => this.onBlur()}"><img alt="menu-icon" src="icons/themes.svg" class="${(!this.state.loading ? (this.openedOnce ? '' : 'gg-spin-wait') : 'hidden')}"> <img alt="loading-icon" src="icons/loading.svg" class="${(this.state.loading ? 'gg-spin' : 'hidden')}"> <span i18n="Themes">Themes</span></button><div class="${(this.menuOpen ? 'themes' : 'hidden')}">${Object.values(this.state.themes._allThemes ?? {}).map(theme => uHtmlFor(theme, theme.id) ` <button class="${this.isThemeActive(theme) ? 'theme selected' : 'theme'}" onmousedown="${() => this.onThemeChanged(theme)}"><img class="theme-icon" alt="${'Icon for ' + theme.name}" src="${theme.icon}"> <span i18n="${theme.name}">${theme.name}</span></button> `)} ${Object.values(this.customThemes ?? {}).map(theme => uHtmlFor(theme, theme.id) `<div class="${this.isThemeActive(theme) ? 'custom-theme selected' : 'custom-theme'}"><button class="select" onmousedown="${() => this.onCustomThemeChanged(theme)}"><img alt="${'Icon for ' + theme.name}" src="${theme.icon}"> <span i18n="${theme.name}">${theme.name}</span></button> <button class="girafe-button-small delete" onmousedown="${(e) => this.onDeleteCustomTheme(theme, e)}"><img alt="delete-icon" src="icons/close.svg"></button></div>`)}<div class="new-theme"><button class="select" onmousedown="${() => this.onAddCustomTheme()}"><img alt="Create custom theme" src="${this.newIcon}" tip="Save the current layer configuration as new theme"> <span i18n="Create custom theme">Create custom theme</span></button></div></div>`;
16
+ <button class="gg-icon-button gg-big-withtext" tip="Theme selection" onclick="${() => this.toggleThemesList()}"><img alt="menu-icon" src="icons/themes.svg" class="${(!this.state.loading ? (this.openedOnce ? '' : 'gg-spin-wait') : 'hidden')}"> <img alt="loading-icon" src="icons/loading.svg" class="${(this.state.loading ? 'gg-spin' : 'hidden')}"> <span i18n="Themes">Themes</span></button><div class="${(this.menuOpen ? 'themes' : 'hidden')}"><div id="themes-type-chooser"><div class="gg-tabs">${Object.values(ThemeTypes).map((themeType) => uHtml ` <button id="${'choose-themes-type-' + themeType}" i18n="${'themes-type-' + themeType}" class="${this.isThemeTypeActive(themeType) ? 'gg-tab active' : 'gg-tab'}" .active="${this.isThemeTypeActive(themeType)}" onclick="${() => this.activateThemeType(themeType)}"></button> `)}</div></div><div id="active-themes">${Object.values(this.activeThemes).map(theme => uHtmlFor(theme, theme.id) `<div role="button" tabindex="0" class="${this.isThemeActive(theme) ? 'theme-card selected' : 'theme-card'}" onkeyup="${() => this.onThemeChanged(theme)}" onclick="${() => this.onThemeChanged(theme)}"><img class="${this.isThemeFavorite(theme) ? 'theme-favorite yes' : 'theme-favorite'}" tip="themes-add-remove-favorites" alt="themes-add-remove-favorites" src="" onclick="${(e) => this.onThemeFavoriteChanged(theme, e)}" onkeyup="${(e) => this.onThemeFavoriteChanged(theme, e)}"> <img class="${this.isCustomTheme(theme) ? 'delete-icon' : 'delete-icon placeholder'}" onclick="${(e) => this.onDeleteCustomTheme(theme, e)}" onkeyup="${(e) => this.onDeleteCustomTheme(theme, e)}" alt="delete-icon" src=""> <img class="theme-icon" alt="${'Icon for ' + theme.name}" src="${theme.icon}"> <span i18n="${theme.name}" class="theme-name">${theme.name}</span></div>`)}<div role="button" tabindex="0" class="${this.isThemeTypeActive('custom') ? 'theme-card' : 'hidden'}" onclick="${() => this.onAddCustomTheme()}" onkeyup="${() => this.onAddCustomTheme()}"><img class="theme-favorite placeholder" src="" alt="" disabled="disabled"> <img class="theme-icon" alt="Create custom theme" src="${this.newIcon}" tip="Save the current layer configuration as new theme"> <span i18n="Create custom theme" class="theme-name">Create custom theme</span></div></div></div><div id="close-themes-menu" onclick="${() => this.onBlur()}"></div>`;
13
17
  };
14
18
  newIcon = NewIcon;
15
19
  menuOpen = false;
16
20
  openedOnce = false;
21
+ activeThemeType = 'all';
22
+ clickOutsideContainer = null;
17
23
  get customThemes() {
18
24
  return this.context.customThemesManager.customThemes;
19
25
  }
26
+ get allThemes() {
27
+ return this.state.themes._allThemes ?? {};
28
+ }
29
+ get favoriteThemes() {
30
+ return [
31
+ ...Object.values(this.allThemes).filter((theme) => this.context.themeFavoritesManager.isThemeInFavorites(theme)),
32
+ ...Object.values(this.customThemes).filter((theme) => this.context.themeFavoritesManager.isThemeInFavorites(theme))
33
+ ];
34
+ }
35
+ get activeThemes() {
36
+ switch (this.activeThemeType) {
37
+ case 'all':
38
+ return this.allThemes;
39
+ case 'favorites':
40
+ return this.favoriteThemes;
41
+ case 'custom':
42
+ return this.customThemes;
43
+ default:
44
+ return {};
45
+ }
46
+ }
20
47
  constructor() {
21
48
  super('themes');
22
49
  }
23
50
  registerEvents() {
24
- this.subscribe('loading', () => super.render());
51
+ this.subscribe('loading', () => this.render());
25
52
  this.subscribe('themes.isLoaded', () => {
26
53
  if (this.state.themes.isLoaded) {
27
- super.render();
54
+ this.render();
28
55
  super.girafeTranslate();
29
56
  }
30
57
  });
31
58
  }
59
+ render() {
60
+ super.render();
61
+ if (this.clickOutsideContainer)
62
+ this.clickOutsideContainer.style.display = this.menuOpen ? 'block' : 'none';
63
+ }
32
64
  onBlur() {
33
65
  this.menuOpen = false;
34
- super.render();
66
+ this.render();
35
67
  }
36
68
  toggleThemesList() {
37
69
  this.openedOnce = true;
38
70
  this.menuOpen = !this.menuOpen;
39
- super.render();
71
+ this.render();
72
+ }
73
+ activateThemeType(themeType) {
74
+ this.activeThemeType = themeType;
75
+ this.render();
40
76
  }
41
77
  onThemeChanged(theme) {
42
78
  if (this.context.configManager.Config.themes.selectionMode === 'add') {
43
79
  this.state.themes.lastSelectedTheme = null;
44
80
  }
45
81
  this.state.themes.lastSelectedTheme = theme;
82
+ if (theme instanceof CustomTheme)
83
+ return;
46
84
  if (theme.disclaimer) {
47
85
  this.state.infobox.elements.push({
48
86
  id: theme.treeItemId,
@@ -64,7 +102,22 @@ class ThemeComponent extends GirafeHTMLElement {
64
102
  if (this.context.configManager.Config.themes.selectionMode === 'replace') {
65
103
  return theme.id === this.state.themes.lastSelectedTheme?.id;
66
104
  }
67
- return false;
105
+ return this.state.layers.layersList.some((layer) => layer instanceof ThemeLayer && layer.id === theme.id);
106
+ }
107
+ isThemeFavorite(theme) {
108
+ return this.context.themeFavoritesManager.isThemeInFavorites(theme);
109
+ }
110
+ onThemeFavoriteChanged(theme, e) {
111
+ e.preventDefault();
112
+ e.stopPropagation();
113
+ this.context.themeFavoritesManager.addOrRemoveThemeFromFavorites(theme);
114
+ this.render();
115
+ }
116
+ isThemeTypeActive(themeType) {
117
+ return (this.activeThemeType ?? 'all') === themeType;
118
+ }
119
+ isCustomTheme(theme) {
120
+ return theme instanceof CustomTheme;
68
121
  }
69
122
  onCustomThemeChanged(customTheme) {
70
123
  this.state.themes.lastSelectedTheme = customTheme;
@@ -74,15 +127,15 @@ class ThemeComponent extends GirafeHTMLElement {
74
127
  const themeName = await window.gPrompt('Enter a name for your custom theme', 'Create custom theme', 'Enter a name...');
75
128
  if (themeName !== false && themeName.trim().length > 0) {
76
129
  this.context.customThemesManager.addTheme(themeName, this.state.layers.layersList);
77
- super.render();
130
+ this.render();
78
131
  }
79
132
  }
80
- async onDeleteCustomTheme(themelayer, e) {
133
+ async onDeleteCustomTheme(customTheme, e) {
81
134
  e.stopPropagation();
82
135
  const confirm = await window.gConfirm('Do you want to delete this theme?', 'Delete Theme');
83
136
  if (confirm) {
84
- this.context.customThemesManager.deleteTheme(themelayer);
85
- super.render();
137
+ this.context.customThemesManager.deleteTheme(customTheme);
138
+ this.render();
86
139
  }
87
140
  }
88
141
  connectedCallback() {
@@ -90,6 +143,7 @@ class ThemeComponent extends GirafeHTMLElement {
90
143
  super.render();
91
144
  this.registerEvents();
92
145
  this.girafeTranslate();
146
+ this.clickOutsideContainer = this.shadow.getElementById('close-themes-menu');
93
147
  }
94
148
  }
95
149
  export default ThemeComponent;
package/models/gmf.d.ts CHANGED
@@ -79,7 +79,7 @@ export interface GMFServerOgc {
79
79
  urlWfs?: string;
80
80
  oapifSupport?: boolean;
81
81
  urlOapif?: string;
82
- type: string;
82
+ type: 'mapserver' | 'qgisserver' | 'georama' | 'geoserver' | 'arcgis' | 'other';
83
83
  imageType: string;
84
84
  attributes?: GMFServerOgcAttributes;
85
85
  }
@@ -6,7 +6,7 @@ export default class ServerOgc {
6
6
  urlWfs?: string;
7
7
  oapifSupport: boolean;
8
8
  urlOapif?: string;
9
- type: string;
9
+ type: 'mapserver' | 'qgisserver' | 'georama' | 'geoserver' | 'arcgis' | 'other';
10
10
  imageType: string;
11
11
  aliases: Record<string, string>;
12
12
  constructor(name: string, elem: GMFServerOgc);
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.1.0-dev.2318696406",
8
+ "version": "1.1.0-dev.2321895443",
9
9
  "type": "module",
10
10
  "engines": {
11
11
  "node": ">=20.19.0"
@@ -1 +1 @@
1
- {"version":"1.1.0-dev.2318696406", "build":"2318696406", "date":"11/02/2026"}
1
+ {"version":"1.1.0-dev.2321895443", "build":"2321895443", "date":"12/02/2026"}
@@ -30,6 +30,7 @@ import UserDataManager from '../userdata/userdatamanager.js';
30
30
  import WfsManager from '../wfs/wfsmanager.js';
31
31
  import WmsManager from '../wms/wmsmanager.js';
32
32
  import IGirafeContext from './icontext.js';
33
+ import ThemeFavoritesManager from '../themes/themefavoritesmanager.js';
33
34
  export default class GirafeContext implements IGirafeContext {
34
35
  readonly userDataManager: UserDataManager;
35
36
  readonly configManager: ConfigManager;
@@ -62,6 +63,7 @@ export default class GirafeContext implements IGirafeContext {
62
63
  readonly ogcApiFeaturesManager: OgcApiFeaturesManager;
63
64
  readonly localFileManager: LocalFileManager;
64
65
  readonly onBoardingManager: OnBoardingManager;
66
+ readonly themeFavoritesManager: ThemeFavoritesManager;
65
67
  constructor();
66
68
  initialize(): Promise<void>;
67
69
  }
@@ -29,6 +29,7 @@ import UrlManager from '../url/urlmanager.js';
29
29
  import UserDataManager from '../userdata/userdatamanager.js';
30
30
  import WfsManager from '../wfs/wfsmanager.js';
31
31
  import WmsManager from '../wms/wmsmanager.js';
32
+ import ThemeFavoritesManager from '../themes/themefavoritesmanager.js';
32
33
  export default class GirafeContext {
33
34
  userDataManager;
34
35
  configManager;
@@ -61,6 +62,7 @@ export default class GirafeContext {
61
62
  ogcApiFeaturesManager;
62
63
  localFileManager;
63
64
  onBoardingManager;
65
+ themeFavoritesManager;
64
66
  constructor() {
65
67
  this.componentManager = new ComponentManager(this);
66
68
  this.userDataManager = new UserDataManager(this);
@@ -93,6 +95,7 @@ export default class GirafeContext {
93
95
  this.localFileManager = new LocalFileManager(this);
94
96
  this.ogcApiFeaturesManager = new OgcApiFeaturesManager(this);
95
97
  this.onBoardingManager = new OnBoardingManager(this);
98
+ this.themeFavoritesManager = new ThemeFavoritesManager(this);
96
99
  }
97
100
  async initialize() {
98
101
  // NOTE : This initialization order is important, because some singleton will need other ones !
@@ -129,5 +132,6 @@ export default class GirafeContext {
129
132
  this.localFileManager.initializeSingleton();
130
133
  this.ogcApiFeaturesManager.initializeSingleton();
131
134
  this.onBoardingManager.initializeSingleton();
135
+ this.themeFavoritesManager.initializeSingleton();
132
136
  }
133
137
  }
@@ -29,6 +29,7 @@ import UrlManager from '../url/urlmanager.js';
29
29
  import UserDataManager from '../userdata/userdatamanager.js';
30
30
  import WfsManager from '../wfs/wfsmanager.js';
31
31
  import WmsManager from '../wms/wmsmanager.js';
32
+ import ThemeFavoritesManager from '../themes/themefavoritesmanager.js';
32
33
  export default interface IGirafeContext {
33
34
  readonly userDataManager: UserDataManager;
34
35
  readonly configManager: ConfigManager;
@@ -61,5 +62,6 @@ export default interface IGirafeContext {
61
62
  readonly ogcApiFeaturesManager: OgcApiFeaturesManager;
62
63
  readonly localFileManager: LocalFileManager;
63
64
  readonly onBoardingManager: OnBoardingManager;
65
+ readonly themeFavoritesManager: ThemeFavoritesManager;
64
66
  initialize(): Promise<void>;
65
67
  }
@@ -1,14 +1,15 @@
1
1
  import TileLayer from 'ol/layer/Tile.js';
2
2
  import WMTS from 'ol/source/WMTS.js';
3
+ import ServerOgc from '../models/serverogc.js';
3
4
  declare class LegendHelper {
4
5
  /**
5
6
  * Get the WMS legend URL.
6
- * @param url The base url of the wms service.
7
+ * @param serverOgc The base url of the wms service.
7
8
  * @param layerName The name of a wms layer.
8
9
  * @param options to create the legend url.
9
10
  * @returns The legend URL or undefined.
10
11
  */
11
- static readonly getWMSLegendURL: (url: string | undefined, layerName: string, options?: WMSLegendURLOptions) => string | undefined;
12
+ static readonly getWMSLegendURL: (serverOgc: ServerOgc, layerName: string, options?: WMSLegendURLOptions) => string | undefined;
12
13
  /**
13
14
  * Retrieves the legend URL for a given WMTS tile layer.
14
15
  * @param {TileLayer<WMTS>} olayer - The OpenLayers tile layer object.
@@ -2,13 +2,13 @@ import { appendParams } from 'ol/uri.js';
2
2
  class LegendHelper {
3
3
  /**
4
4
  * Get the WMS legend URL.
5
- * @param url The base url of the wms service.
5
+ * @param serverOgc The base url of the wms service.
6
6
  * @param layerName The name of a wms layer.
7
7
  * @param options to create the legend url.
8
8
  * @returns The legend URL or undefined.
9
9
  */
10
- static getWMSLegendURL = (url, layerName, options) => {
11
- if (!url) {
10
+ static getWMSLegendURL = (serverOgc, layerName, options) => {
11
+ if (!serverOgc.url) {
12
12
  return undefined;
13
13
  }
14
14
  const queryString = {
@@ -19,6 +19,9 @@ class LegendHelper {
19
19
  REQUEST: 'GetLegendGraphic',
20
20
  LAYER: layerName
21
21
  };
22
+ if (serverOgc.type === 'qgisserver') {
23
+ queryString.LAYERTITLE = 'False';
24
+ }
22
25
  const scale = options?.scale;
23
26
  const legendRule = options?.legendRule;
24
27
  const legendWidth = options?.legendWidth;
@@ -55,7 +58,7 @@ class LegendHelper {
55
58
  if (additionalQueryString) {
56
59
  Object.assign(queryString, additionalQueryString);
57
60
  }
58
- return appendParams(url, queryString);
61
+ return appendParams(serverOgc.url, queryString);
59
62
  };
60
63
  /**
61
64
  * Retrieves the legend URL for a given WMTS tile layer.
package/tools/main.d.ts CHANGED
@@ -90,6 +90,7 @@ export { default as UserInteractionManager } from './state/userInteractionManage
90
90
  export type { GgUserInteractionEvent } from './state/userinteractionevent.js';
91
91
  export { gGEventDependencies, isPrimaryPointerAction, isAlternateMouseClick, isMouseWheelClick } from './state/userinteractionevent.js';
92
92
  export { default as CustomThemesManager } from './themes/customthemesmanager.js';
93
+ export { default as ThemeFavoritesManager } from './themes/themefavoritesmanager.js';
93
94
  export { DEFAULT_OPACITY, OPACITY_FOR_DEFAULT_BASEMAP } from './themes/themes-config.js';
94
95
  export type { LayerTreeChanges } from './themes/themeshelper.js';
95
96
  export { default as ThemesHelper } from './themes/themeshelper.js';
package/tools/main.js CHANGED
@@ -68,6 +68,7 @@ export { default as StateManager } from './state/statemanager.js';
68
68
  export { default as UserInteractionManager } from './state/userInteractionManager.js';
69
69
  export { gGEventDependencies, isPrimaryPointerAction, isAlternateMouseClick, isMouseWheelClick } from './state/userinteractionevent.js';
70
70
  export { default as CustomThemesManager } from './themes/customthemesmanager.js';
71
+ export { default as ThemeFavoritesManager } from './themes/themefavoritesmanager.js';
71
72
  export { DEFAULT_OPACITY, OPACITY_FOR_DEFAULT_BASEMAP } from './themes/themes-config.js';
72
73
  export { default as ThemesHelper } from './themes/themeshelper.js';
73
74
  export { default as ThemesManager } from './themes/themesmanager.js';
@@ -33,7 +33,7 @@ export const DEMO_LAYERS = {
33
33
  urlWfs: '',
34
34
  oapifSupport: true,
35
35
  urlOapif: 'https://geomapfish-demo-2-9.camptocamp.com/mapserv_proxy/QGIS_Server/wfs3',
36
- type: 'gmf',
36
+ type: 'qgisserver',
37
37
  imageType: ''
38
38
  })
39
39
  }
@@ -11,7 +11,7 @@ const server = new ServerOgc('test', {
11
11
  urlWfs: '',
12
12
  oapifSupport: true,
13
13
  urlOapif: mockUrl,
14
- type: 'default',
14
+ type: 'qgisserver',
15
15
  imageType: ''
16
16
  });
17
17
  let context;
@@ -12,7 +12,7 @@ const server = new ServerOgc('test', {
12
12
  urlWfs: '',
13
13
  oapifSupport: true,
14
14
  urlOapif: mockUrl,
15
- type: 'default',
15
+ type: 'qgisserver',
16
16
  imageType: ''
17
17
  });
18
18
  const collectionId = 'test-collection';
@@ -29,7 +29,7 @@ describe('OgcApiFeaturesManager', () => {
29
29
  urlWfs: '',
30
30
  oapifSupport: true,
31
31
  urlOapif: "'https://testUrl.com",
32
- type: 'default',
32
+ type: 'qgisserver',
33
33
  imageType: ''
34
34
  })
35
35
  };
@@ -13,8 +13,7 @@ export default class ActiveBasemapsSerializer {
13
13
  return this.serialize(activeBasemaps);
14
14
  }
15
15
  brainDeserialize(serializedString) {
16
- const activeBasemaps = this.deserialize(serializedString);
17
- this.state.activeBasemaps = activeBasemaps;
16
+ this.state.activeBasemaps = this.deserialize(serializedString);
18
17
  }
19
18
  serialize(basemaps) {
20
19
  const sharedBasemaps = [];
@@ -4,7 +4,9 @@ import GroupLayer, { GroupLayerOptions } from '../../models/layers/grouplayer';
4
4
  import ServerOgc from '../../models/serverogc';
5
5
  import Basemap from '../../models/basemaps/basemap';
6
6
  export declare function createTestOgcServer(): ServerOgc;
7
+ export declare function createTestOgcServerQGis(): ServerOgc;
7
8
  export declare function createTestLayerWms(options?: LayerWmsOptions): LayerWms;
9
+ export declare function createTestLayerWmsQGis(options?: LayerWmsOptions): LayerWms;
8
10
  export declare function createTestLayerWmts(options?: LayerWmtsOptions, ogcServer?: ServerOgc): LayerWmts;
9
11
  export declare function createTestGroupLayer(options?: GroupLayerOptions): GroupLayer;
10
12
  export declare function createTestBasemap(): Basemap;
@@ -11,12 +11,26 @@ export function createTestOgcServer() {
11
11
  imageType: 'image/png'
12
12
  });
13
13
  }
14
+ export function createTestOgcServerQGis() {
15
+ return new ServerOgc('testOgcServer', {
16
+ url: 'https://ogc.test.url',
17
+ wfsSupport: false,
18
+ type: 'qgisserver',
19
+ imageType: 'image/png'
20
+ });
21
+ }
14
22
  export function createTestLayerWms(options) {
15
23
  const ogcServer = createTestOgcServer();
16
24
  const layerWms = new LayerWms(1, 'testWms', 1, ogcServer, options);
17
25
  layerWms.activeState = 'on';
18
26
  return layerWms;
19
27
  }
28
+ export function createTestLayerWmsQGis(options) {
29
+ const ogcServer = createTestOgcServerQGis();
30
+ const layerWms = new LayerWms(1, 'testWmsQGis', 1, ogcServer, options);
31
+ layerWms.activeState = 'on';
32
+ return layerWms;
33
+ }
20
34
  export function createTestLayerWmts(options, ogcServer) {
21
35
  const layerWmts = new LayerWmts(1, 'testWmts', 1, 'https://test.ch', 'testWmts', options, ogcServer);
22
36
  layerWmts.activeState = 'on';
@@ -30,6 +30,7 @@ import UserDataManager from '../userdata/userdatamanager';
30
30
  import WfsManager from '../wfs/wfsmanager';
31
31
  import WmsManager from '../wms/wmsmanager';
32
32
  import OnBoardingManager from '../onboarding/onboardingmanager';
33
+ import ThemeFavoritesManager from '../themes/themefavoritesmanager';
33
34
  export default class MockGirafeContext implements IGirafeContext {
34
35
  readonly userDataManager: UserDataManager;
35
36
  readonly configManager: ConfigManager;
@@ -62,6 +63,7 @@ export default class MockGirafeContext implements IGirafeContext {
62
63
  readonly ogcApiFeaturesManager: OgcApiFeaturesManager;
63
64
  readonly localFileManager: LocalFileManager;
64
65
  readonly onBoardingManager: OnBoardingManager;
66
+ readonly themeFavoritesManager: ThemeFavoritesManager;
65
67
  constructor();
66
68
  initialize(): Promise<void>;
67
69
  }
@@ -31,6 +31,7 @@ import WfsManager from '../wfs/wfsmanager';
31
31
  import WmsManager from '../wms/wmsmanager';
32
32
  import { MockConfig } from './mockconfig';
33
33
  import OnBoardingManager from '../onboarding/onboardingmanager';
34
+ import ThemeFavoritesManager from '../themes/themefavoritesmanager';
34
35
  export default class MockGirafeContext {
35
36
  userDataManager;
36
37
  configManager;
@@ -63,6 +64,7 @@ export default class MockGirafeContext {
63
64
  ogcApiFeaturesManager;
64
65
  localFileManager;
65
66
  onBoardingManager;
67
+ themeFavoritesManager;
66
68
  constructor() {
67
69
  this.componentManager = new ComponentManager(this);
68
70
  this.userDataManager = new UserDataManager(this);
@@ -95,6 +97,7 @@ export default class MockGirafeContext {
95
97
  this.localFileManager = new LocalFileManager(this);
96
98
  this.ogcApiFeaturesManager = new OgcApiFeaturesManager(this);
97
99
  this.onBoardingManager = new OnBoardingManager(this);
100
+ this.themeFavoritesManager = new ThemeFavoritesManager(this);
98
101
  this.initialize();
99
102
  }
100
103
  async initialize() {
@@ -137,5 +140,6 @@ export default class MockGirafeContext {
137
140
  this.localFileManager.initializeSingleton();
138
141
  this.ogcApiFeaturesManager.initializeSingleton();
139
142
  this.onBoardingManager.initializeSingleton();
143
+ this.themeFavoritesManager.initializeSingleton();
140
144
  }
141
145
  }
@@ -0,0 +1,17 @@
1
+ import GirafeSingleton from '../../base/GirafeSingleton.js';
2
+ import CustomTheme from '../../models/customtheme.js';
3
+ import ThemeLayer from '../../models/layers/themelayer.js';
4
+ declare class ThemeFavoritesManager extends GirafeSingleton {
5
+ private themeFavorites;
6
+ private readonly storagePath;
7
+ initializeSingleton(): void;
8
+ private saveThemeFavorites;
9
+ private loadThemeFavorites;
10
+ addThemeToFavorites(theme: ThemeLayer | CustomTheme): void;
11
+ removeThemeFromFavorites(theme: ThemeLayer | CustomTheme): void;
12
+ addOrRemoveThemeFromFavorites(theme: ThemeLayer | CustomTheme): void;
13
+ clearThemeFavorites(): void;
14
+ isThemeInFavorites(theme: ThemeLayer | CustomTheme): boolean;
15
+ getThemeFavorites(): Array<number | string>;
16
+ }
17
+ export default ThemeFavoritesManager;
@@ -0,0 +1,53 @@
1
+ import GirafeSingleton from '../../base/GirafeSingleton.js';
2
+ import ThemeLayer from '../../models/layers/themelayer.js';
3
+ class ThemeFavoritesManager extends GirafeSingleton {
4
+ themeFavorites = [];
5
+ storagePath = 'themeFavorites';
6
+ initializeSingleton() {
7
+ super.initializeSingleton();
8
+ this.loadThemeFavorites();
9
+ }
10
+ saveThemeFavorites() {
11
+ const serializedThemeFavorites = JSON.stringify(this.themeFavorites);
12
+ this.context.userDataManager.saveUserData(this.storagePath, serializedThemeFavorites);
13
+ }
14
+ loadThemeFavorites() {
15
+ const serializedThemeFavorites = this.context.userDataManager.getUserData(this.storagePath);
16
+ if (serializedThemeFavorites) {
17
+ this.themeFavorites = JSON.parse(serializedThemeFavorites);
18
+ }
19
+ }
20
+ addThemeToFavorites(theme) {
21
+ this.themeFavorites.push(theme instanceof ThemeLayer ? theme.id : theme.name);
22
+ this.saveThemeFavorites();
23
+ }
24
+ removeThemeFromFavorites(theme) {
25
+ const index = this.themeFavorites.findIndex((themeIdOrName) => theme instanceof ThemeLayer ? themeIdOrName === theme.id : themeIdOrName === theme.name);
26
+ if (index >= 0) {
27
+ this.themeFavorites.splice(index, 1);
28
+ this.saveThemeFavorites();
29
+ }
30
+ else {
31
+ throw new Error('The theme to be removed cannot be found in the list of favorites');
32
+ }
33
+ }
34
+ addOrRemoveThemeFromFavorites(theme) {
35
+ if (this.isThemeInFavorites(theme)) {
36
+ this.removeThemeFromFavorites(theme);
37
+ }
38
+ else {
39
+ this.addThemeToFavorites(theme);
40
+ }
41
+ }
42
+ clearThemeFavorites() {
43
+ this.themeFavorites = [];
44
+ this.saveThemeFavorites();
45
+ }
46
+ isThemeInFavorites(theme) {
47
+ return this.themeFavorites.includes(theme instanceof ThemeLayer ? theme.id : theme.name);
48
+ }
49
+ getThemeFavorites() {
50
+ return this.themeFavorites;
51
+ }
52
+ }
53
+ export default ThemeFavoritesManager;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,103 @@
1
+ import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
+ import MockHelper from '../tests/mockhelper';
3
+ import ThemeLayer from '../../models/layers/themelayer';
4
+ import CustomTheme from '../../models/customtheme';
5
+ let themeFavoritesManager;
6
+ let context;
7
+ const storagePath = 'themeFavorites';
8
+ const themeLayer = new ThemeLayer(42, "ThemeLayer", 1);
9
+ const customTheme = new CustomTheme("CustomTheme");
10
+ beforeAll(() => {
11
+ context = MockHelper.startMocking();
12
+ themeFavoritesManager = context.themeFavoritesManager;
13
+ });
14
+ beforeEach(() => {
15
+ context.userDataManager.deleteAllUserData();
16
+ themeFavoritesManager.clearThemeFavorites();
17
+ });
18
+ describe('ThemeFavoritesManager.addThemeToFavorites', () => {
19
+ it('adding a ThemeLayer should result in an number array', () => {
20
+ themeFavoritesManager.addThemeToFavorites(themeLayer);
21
+ expect(context.userDataManager.getUserData(storagePath)).toEqual(`[${themeLayer.id}]`);
22
+ });
23
+ it('adding a CustomTheme should result in an string array', () => {
24
+ themeFavoritesManager.addThemeToFavorites(customTheme);
25
+ expect(context.userDataManager.getUserData(storagePath)).toEqual(`["${customTheme.name}"]`);
26
+ });
27
+ it('adding a ThemeLayer and a CustomTheme should result in an mixed array', () => {
28
+ themeFavoritesManager.addThemeToFavorites(themeLayer);
29
+ themeFavoritesManager.addThemeToFavorites(customTheme);
30
+ expect(context.userDataManager.getUserData(storagePath)).toEqual(`[${themeLayer.id},"${customTheme.name}"]`);
31
+ });
32
+ });
33
+ describe('ThemeFavoritesManager.initializeSingleton', () => {
34
+ it('starting without persisted favorites should result in an empty array', () => {
35
+ themeFavoritesManager.initializeSingleton();
36
+ expect(themeFavoritesManager.getThemeFavorites()).toEqual([]);
37
+ });
38
+ it('starting with an empty string as persisted data should result in an empty array', () => {
39
+ context.userDataManager.saveUserData(storagePath, '');
40
+ themeFavoritesManager.initializeSingleton();
41
+ expect(themeFavoritesManager.getThemeFavorites()).toEqual([]);
42
+ });
43
+ it('starting with "[42]" as persisted data should result in [42]', () => {
44
+ context.userDataManager.saveUserData(storagePath, '[42]');
45
+ themeFavoritesManager.initializeSingleton();
46
+ expect(themeFavoritesManager.getThemeFavorites()).toEqual([42]);
47
+ });
48
+ it('starting with "["CustomTheme""]" as persisted data should result in ["CustomTheme"]', () => {
49
+ context.userDataManager.saveUserData(storagePath, '["CustomTheme"]');
50
+ themeFavoritesManager.initializeSingleton();
51
+ expect(themeFavoritesManager.getThemeFavorites()).toEqual(['CustomTheme']);
52
+ });
53
+ it('starting with "[42,"CustomTheme""]" as persisted data should result in [42,"CustomTheme"]', () => {
54
+ context.userDataManager.saveUserData(storagePath, '[42,"CustomTheme"]');
55
+ themeFavoritesManager.initializeSingleton();
56
+ expect(themeFavoritesManager.getThemeFavorites()).toEqual([42, 'CustomTheme']);
57
+ });
58
+ });
59
+ describe('ThemeFavoritesManager.isThemeInFavorites', () => {
60
+ it('adding ThemeLayer should result in ThemeLayer being a favorite', () => {
61
+ themeFavoritesManager.addThemeToFavorites(themeLayer);
62
+ expect(themeFavoritesManager.isThemeInFavorites(themeLayer)).toBeTruthy();
63
+ });
64
+ it('adding CustomTheme should result in CustomTheme being a favorite', () => {
65
+ themeFavoritesManager.addThemeToFavorites(customTheme);
66
+ expect(themeFavoritesManager.isThemeInFavorites(customTheme)).toBeTruthy();
67
+ });
68
+ it('adding CustomTheme should result in ThemeLayer being not a favorite', () => {
69
+ themeFavoritesManager.addThemeToFavorites(customTheme);
70
+ expect(themeFavoritesManager.isThemeInFavorites(themeLayer)).toBeFalsy();
71
+ });
72
+ it('adding ThemeLayer should result in CustomTheme being not a favorite', () => {
73
+ themeFavoritesManager.addThemeToFavorites(themeLayer);
74
+ expect(themeFavoritesManager.isThemeInFavorites(customTheme)).toBeFalsy();
75
+ });
76
+ it('adding ThemeLayer and CustomTheme should result in both being a favorite', () => {
77
+ themeFavoritesManager.addThemeToFavorites(themeLayer);
78
+ themeFavoritesManager.addThemeToFavorites(customTheme);
79
+ expect(themeFavoritesManager.isThemeInFavorites(themeLayer)).toBeTruthy();
80
+ expect(themeFavoritesManager.isThemeInFavorites(customTheme)).toBeTruthy();
81
+ });
82
+ });
83
+ describe('ThemeFavoritesManager.clearThemeFavorites', () => {
84
+ it('clearing favorites should result in an empty array', () => {
85
+ context.userDataManager.saveUserData(storagePath, '[42,"CustomTheme"]');
86
+ themeFavoritesManager.initializeSingleton();
87
+ expect(themeFavoritesManager.getThemeFavorites().length).toEqual(2);
88
+ themeFavoritesManager.clearThemeFavorites();
89
+ expect(themeFavoritesManager.getThemeFavorites().length).toEqual(0);
90
+ });
91
+ });
92
+ describe('ThemeFavoritesManager.addOrRemoveThemeFromFavorites', () => {
93
+ it('not already being a favorite should result in being a favorite afterwards ', () => {
94
+ themeFavoritesManager.addOrRemoveThemeFromFavorites(themeLayer);
95
+ expect(themeFavoritesManager.isThemeInFavorites(themeLayer)).toBeTruthy();
96
+ });
97
+ it('already being a favorite should result in no longer being a favorite afterwards ', () => {
98
+ context.userDataManager.saveUserData(storagePath, '[42]');
99
+ themeFavoritesManager.initializeSingleton();
100
+ themeFavoritesManager.addOrRemoveThemeFromFavorites(themeLayer);
101
+ expect(themeFavoritesManager.isThemeInFavorites(themeLayer)).toBeFalsy();
102
+ });
103
+ });
@@ -58,6 +58,9 @@ export default class WmsLegendHelper {
58
58
  graphicUrl += '&HEIGHT=' + this.context.configManager.Config.treeview.defaultIconSize.height;
59
59
  graphicUrl += '&WIDTH=' + this.context.configManager.Config.treeview.defaultIconSize.width;
60
60
  }
61
+ if (layer.ogcServer.type === 'qgisserver') {
62
+ graphicUrl += '&LAYERTITLE=False';
63
+ }
61
64
  legends[l] = graphicUrl;
62
65
  }
63
66
  return legends;