@geogirafe/lib-geoportal 1.1.0-dev.2588173445 → 1.1.0-dev.2593337676

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.
@@ -11,6 +11,7 @@ import MapCustomContextMenuComponent from '../components/context-menu/custom-con
11
11
  import SearchComponent from '../components/search/component.js';
12
12
  import SelectionWindowComponent from '../components/selectionwindow/component.js';
13
13
  import { splitTrimAndConvertToNumber } from '../tools/utils/utils.js';
14
+ import DOMPurify from 'dompurify';
14
15
  const getImageSize = (url) => {
15
16
  return new Promise((resolve, reject) => {
16
17
  const img = new Image();
@@ -120,7 +121,7 @@ export default class GeoGirafeApi extends GirafeHTMLElement {
120
121
  this.manageUserInteraction();
121
122
  }
122
123
  manageUserInteraction() {
123
- // Deacivate the preview of search results
124
+ // Deactivate the preview of search results
124
125
  this.context.configManager.Config.search.objectPreview = false;
125
126
  this.context.configManager.Config.search.layerPreview = false;
126
127
  // Force window as selection component
@@ -145,9 +146,11 @@ export default class GeoGirafeApi extends GirafeHTMLElement {
145
146
  }
146
147
  defineAndAddComponent(customElementName, customElementType) {
147
148
  if (!customElements.get(customElementName)) {
149
+ console.log(`Defining custom element: ${customElementName}`);
148
150
  customElements.define(customElementName, customElementType);
149
151
  }
150
152
  const existingElement = this.shadowRoot?.querySelector(customElementName);
153
+ console.log('existingElement', existingElement);
151
154
  if (!existingElement) {
152
155
  const component = new customElementType();
153
156
  this.shadow.appendChild(component);
@@ -246,13 +249,16 @@ export default class GeoGirafeApi extends GirafeHTMLElement {
246
249
  if (tooltip) {
247
250
  this.defineAndAddComponent('girafe-custom-context-menu', MapCustomContextMenuComponent);
248
251
  const content = tooltip.split('|');
252
+ const hasTitle = content.length > 2;
249
253
  const coords = content[0].split(',');
250
254
  const x = Number(coords[0].trim());
251
255
  const y = Number(coords[1].trim());
252
256
  if (!Number.isNaN(x) && !Number.isNaN(y)) {
253
- const text = content[1];
257
+ const title = hasTitle ? content[1] : undefined;
258
+ const text = content[hasTitle ? 2 : 1];
254
259
  this.context.stateManager.state.position.tooltip = {
255
260
  position: [x, y],
261
+ title,
256
262
  content: text
257
263
  };
258
264
  }
@@ -316,6 +322,7 @@ export default class GeoGirafeApi extends GirafeHTMLElement {
316
322
  manageMarkersFileAttribute() {
317
323
  const markersFile = this.getAttributeFromConfig('markersfile');
318
324
  if (markersFile) {
325
+ this.defineAndAddComponent('girafe-custom-context-menu', MapCustomContextMenuComponent);
319
326
  void this.readMarkersFromFile(markersFile).then((markers) => {
320
327
  this.context.stateManager.state.position.markers.push(...markers);
321
328
  });
@@ -336,18 +343,22 @@ export default class GeoGirafeApi extends GirafeHTMLElement {
336
343
  return [];
337
344
  }
338
345
  const headers = lines[0].split('\t').map((header) => header.trim());
339
- const pointIndex = headers.indexOf('point');
340
- const iconIndex = headers.indexOf('icon');
341
- const iconSizeIndex = headers.indexOf('iconSize');
342
- const iconOffsetIndex = headers.indexOf('iconOffset');
343
- if (pointIndex === -1 || iconIndex === -1) {
346
+ const columnIndexes = {
347
+ pointIndex: headers.indexOf('point'),
348
+ titleIndex: headers.indexOf('title'),
349
+ descriptionIndex: headers.indexOf('description'),
350
+ iconIndex: headers.indexOf('icon'),
351
+ iconSizeIndex: headers.indexOf('iconSize'),
352
+ iconOffsetIndex: headers.indexOf('iconOffset')
353
+ };
354
+ if (columnIndexes.pointIndex === -1 || columnIndexes.iconIndex === -1) {
344
355
  console.warn(`Invalid markers file '${fileUrl}': missing required 'point' or 'icon' column`);
345
356
  return [];
346
357
  }
347
358
  const markers = [];
348
359
  const legacy = fileUrl.includes('legacy');
349
360
  for (const line of lines.slice(1)) {
350
- await this.lineToMarker(line, pointIndex, iconIndex, iconSizeIndex, iconOffsetIndex, legacy).then((marker) => {
361
+ await this.lineToMarker(line, columnIndexes, legacy).then((marker) => {
351
362
  if (marker) {
352
363
  markers.push(marker);
353
364
  }
@@ -355,11 +366,12 @@ export default class GeoGirafeApi extends GirafeHTMLElement {
355
366
  }
356
367
  return markers;
357
368
  }
358
- async lineToMarker(line, pointIndex, iconIndex, iconSizeIndex, iconOffsetIndex, legacy) {
369
+ async lineToMarker(line, columnIndexes, legacy) {
359
370
  const columns = line.split('\t');
360
- const coords = splitTrimAndConvertToNumber(columns[pointIndex]);
361
- const imageUrl = columns[iconIndex]?.trim();
362
- if (coords.length < 2 || !imageUrl || Number.isNaN(coords[0]) || Number.isNaN(coords[1])) {
371
+ const getColumOrUndefined = (index) => index ? columns[index]?.trim() : undefined;
372
+ const coords = splitTrimAndConvertToNumber(columns[columnIndexes.pointIndex]);
373
+ const imageUrl = columnIndexes.iconIndex ? columns[columnIndexes.iconIndex]?.trim() : undefined;
374
+ if (!coords || coords.length < 2 || !imageUrl || Number.isNaN(coords[0]) || Number.isNaN(coords[1])) {
363
375
  console.warn(`Invalid marker line ': ${line}`);
364
376
  return undefined;
365
377
  }
@@ -367,12 +379,8 @@ export default class GeoGirafeApi extends GirafeHTMLElement {
367
379
  if (coords[0] < coords[1] && this.context.stateManager.state.projection === 'EPSG:2056') {
368
380
  coords.reverse();
369
381
  }
370
- const size = iconSizeIndex >= 0 && columns[iconSizeIndex]?.trim()
371
- ? splitTrimAndConvertToNumber(columns[iconSizeIndex])
372
- : undefined;
373
- let offset = iconOffsetIndex >= 0 && columns[iconOffsetIndex]?.trim()
374
- ? splitTrimAndConvertToNumber(columns[iconOffsetIndex])
375
- : undefined;
382
+ const size = splitTrimAndConvertToNumber(getColumOrUndefined(columnIndexes.iconSizeIndex));
383
+ let offset = splitTrimAndConvertToNumber(getColumOrUndefined(columnIndexes.iconOffsetIndex));
376
384
  // In the old WebGIS the Offset referred to the Size of the original Image, while now it refers to the Size of the
377
385
  // resized Image. This is to ensure compatibility with older data.
378
386
  if (offset && size && legacy) {
@@ -382,11 +390,17 @@ export default class GeoGirafeApi extends GirafeHTMLElement {
382
390
  offset = [offset[0] * scaleX, offset[1] * scaleY];
383
391
  });
384
392
  }
393
+ const tooltipTitle = getColumOrUndefined(columnIndexes.titleIndex);
394
+ const tooltipContent = getColumOrUndefined(columnIndexes.descriptionIndex);
395
+ const tooltip = tooltipTitle && tooltipContent ?
396
+ { title: tooltipTitle, content: DOMPurify.sanitize(tooltipContent) } :
397
+ undefined;
385
398
  return {
386
399
  position: [coords[0], coords[1]],
387
400
  imageUrl,
388
401
  size,
389
- offset
402
+ offset,
403
+ tooltip,
390
404
  };
391
405
  }
392
406
  async initialize() {
@@ -495,6 +495,7 @@
495
495
  "Swisstopo WMTS": "Swisstopo WMTS",
496
496
  "system": "System",
497
497
  "Take measure": "Messen",
498
+ "Test for Translation": "Test für Übersetzung",
498
499
  "Thanks for your message. If necessary, somebody will enter in contact with you as soon as possible.": "Vielen Dank für Ihre Nachricht. Falls erforderlich, wird sich so schnell wie möglich jemand mit Ihnen in Verbindung setzen.",
499
500
  "The following link will be sent with your message. It contains your current configuration and will help us to understand your problem.": "Der folgende Link wird mit Ihrer Nachricht gesendet. Er enthält Ihre aktuelle Konfiguration und hilft uns, Ihr Problem zu verstehen.",
500
501
  "The generated document may contain sensitive data.": "Das generierte Dokument kann sensible Daten enthalten.",
@@ -509,6 +510,7 @@
509
510
  "themes-type-custom": "Benutzerdefinierte",
510
511
  "themes-type-favorites": "Favoriten",
511
512
  "This instance of GeoGirafe cannot be used at the moment.": "Diese Instanz von GeoGirafe kann momentan nicht verwendet werden.",
513
+ "This Text should be translated": "Dieser Text sollte übersetzt werden",
512
514
  "Toggle all legends": "Alle Legenden umschalten",
513
515
  "Toggle legend": "Legende umschalten",
514
516
  "Tools": "Werkzeuge",
@@ -498,6 +498,7 @@
498
498
  "Swisstopo WMTS": "Swisstopo WMTS",
499
499
  "system": "System",
500
500
  "Take measure": "Take measure",
501
+ "Test for Translation": "Test for Translation",
501
502
  "Thanks for your message. If necessary, somebody will enter in contact with you as soon as possible.": "Thanks for your message. If necessary, someone will get in touch with you as soon as possible.",
502
503
  "The following link will be sent with your message. It contains your current configuration and will help us to understand your problem.": "The following link will be sent with your message. It contains your current configuration and will help us to understand your problem.",
503
504
  "The generated document may contain sensitive data.": "The generated document may contain sensitive data.",
@@ -512,6 +513,7 @@
512
513
  "themes-type-custom": "Custom Themes",
513
514
  "themes-type-favorites": "Favorite Themes",
514
515
  "This instance of GeoGirafe cannot be used at the moment.": "This instance of GeoGirafe cannot be used at the moment.",
516
+ "This Text should be translated": "This Text should be translated",
515
517
  "Toggle all legends": "Toggle all legends",
516
518
  "Toggle legend": "Toggle legend",
517
519
  "Tools": "Tools",
@@ -495,6 +495,7 @@
495
495
  "Swisstopo WMTS": "Swisstopo WMTS",
496
496
  "system": "Système",
497
497
  "Take measure": "Prendre une mesure",
498
+ "Test for Translation": "Test de traduction",
498
499
  "Thanks for your message. If necessary, somebody will enter in contact with you as soon as possible.": "Merci pour votre message. Si nécessaire, quelqu'un prendra contact avec vous dès que possible.",
499
500
  "The following link will be sent with your message. It contains your current configuration and will help us to understand your problem.": "Le lien ci-dessous sera envoyé avec votre message. Il contient votre configuration courante et nous aidera à analyser votre demande.",
500
501
  "The generated document may contain sensitive data.": "Le document généré peut contenir des données sensibles.",
@@ -509,6 +510,7 @@
509
510
  "themes-type-custom": "Personnalisés",
510
511
  "themes-type-favorites": "Favoris",
511
512
  "This instance of GeoGirafe cannot be used at the moment.": "Cette instance de GeoGirafe ne peut pas être utilisée pour le moment.",
513
+ "This Text should be translated": "Ce texte doit être traduit",
512
514
  "Toggle all legends": "Afficher / Masquer toutes les légendes",
513
515
  "Toggle legend": "Afficher / Masquer la légende",
514
516
  "Tools": "Outis",
@@ -495,6 +495,7 @@
495
495
  "Swisstopo WMTS": "Swisstopo WMTS",
496
496
  "system": "Sistema",
497
497
  "Take measure": "Prendi misura",
498
+ "Test for Translation": "Test di traduzione",
498
499
  "Thanks for your message. If necessary, somebody will enter in contact with you as soon as possible.": "Grazie per il tuo messaggio. Se necessario, qualcuno ti contatterà il prima possibile.",
499
500
  "The following link will be sent with your message. It contains your current configuration and will help us to understand your problem.": "Il seguente link verrà inviato con il tuo messaggio. Contiene la tua configurazione attuale e ci aiuterà a capire il problema.",
500
501
  "The generated document may contain sensitive data.": "Il documento generato potrebbe contenere dati sensibili.",
@@ -509,6 +510,7 @@
509
510
  "themes-type-custom": "Personalizzati",
510
511
  "themes-type-favorites": "Preferiti",
511
512
  "This instance of GeoGirafe cannot be used at the moment.": "Questa istanza di GeoGirafe non può essere utilizzata al momento.",
513
+ "This Text should be translated": "Questo testo dovrebbe essere tradotto",
512
514
  "Toggle all legends": "Attiva / Disattiva tutte le legende",
513
515
  "Toggle legend": "Attiva / Disattiva legenda",
514
516
  "Tools": "Strumenti",
@@ -4,7 +4,11 @@ declare class MapCustomContextMenuComponent extends GirafeHTMLElement {
4
4
  protected styleUrls: string[] | null;
5
5
  template: () => import("uhtml").Hole;
6
6
  private get map();
7
- private static contextMenuOverlay;
7
+ private static readonly contextMenuOverlayByMap;
8
+ private get contextMenuOverlay();
9
+ private set contextMenuOverlay(value);
10
+ get tooltipTitle(): "" | import("uhtml").Hole;
11
+ get tooltipContent(): "" | import("uhtml").Hole;
8
12
  constructor();
9
13
  render(): void;
10
14
  closeMenu(): void;
@@ -8,7 +8,7 @@ class MapCustomContextMenuComponent extends GirafeHTMLElement {
8
8
  styleUrls = null;
9
9
  template = () => {
10
10
  return uHtml `<style>
11
- .map-info-contextmenu{--contextmenu-border-width:1px;--contextmenu-triangle-width:20px;--contextmenu-triangle-height:10px;border:var(--contextmenu-border-width) solid var(--text-color-grad1);bottom:var(--contextmenu-triangle-height);border-radius:0;position:absolute;left:0;transform:translate(-50%)}
11
+ .map-info-contextmenu{--contextmenu-border-width:1px;--contextmenu-triangle-width:20px;--contextmenu-triangle-height:10px;border:var(--contextmenu-border-width) solid var(--text-color-grad1);bottom:var(--contextmenu-triangle-height);border-radius:0;position:absolute;left:0;transform:translate(-50%);& .title-container{padding-bottom:.5rem;&.no-title{padding-bottom:0}& span.title{font-weight:700}}}
12
12
  </style><style>
13
13
  *{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}
14
14
  </style><style>
@@ -17,21 +17,51 @@ class MapCustomContextMenuComponent extends GirafeHTMLElement {
17
17
  .ol-popup{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;font-size:.9em}.ol-popup .ol-popup-content{cursor:default;padding:.25em .5em;overflow:hidden}.ol-popup.hasclosebox .ol-popup-content{margin-right:1.7em}.ol-popup .ol-popup-content:after{clear:both;content:"";height:0;font-size:0;display:block}.ol-popup .anchor{pointer-events:none;background:red;width:0;height:0;margin:-11px 22px;display:block;position:absolute}.ol-popup .anchor:after,.ol-popup .anchor:before{position:absolute}.ol-popup-right .anchor:after,.ol-popup-right .anchor:before{right:0}.ol-popup-top .anchor{top:0}.ol-popup-bottom .anchor{bottom:0}.ol-popup-right .anchor{right:0}.ol-popup-left .anchor{left:0}.ol-popup-center .anchor{left:50%;margin-left:0!important}.ol-popup-middle .anchor{top:50%;margin-top:0!important}.ol-popup-center.ol-popup-middle .anchor{display:none}.ol-popup.ol-fixed{margin:0!important;inset:.5em .5em auto auto!important;transform:none!important}.ol-popup.ol-fixed .anchor{display:none}.ol-popup.ol-fixed.anim>div{animation:none}.ol-popup .ol-fix{float:right;cursor:pointer;background:#fff;width:1em;height:.9em;margin:.2em;position:relative}.ol-popup .ol-fix:before{content:"";box-sizing:border-box;border:.1em solid #666;border-right-width:.3em;width:.8em;height:.7em;margin:.1em;display:block}.ol-popup.shadow{box-shadow:2px 2px 2px 2px #00000080}.ol-popup .closeBox{color:#fff;cursor:pointer;float:right;background-color:#003c8880;border:0;border-radius:2px;width:1.4em;height:1.4em;margin:5px 5px 0 0;padding:0;font-size:.9em;font-weight:700;display:none;position:relative}.ol-popup.hasclosebox .closeBox{display:block}.ol-popup .closeBox:hover{background-color:#003c88b3}.ol-popup .closeBox:after{content:"u00d7";text-align:center;width:100%;margin:-.5em 0;font-size:1.5em;line-height:1em;position:absolute;top:50%;left:0;right:0}.ol-popup.modifytouch{background-color:#eee}.ol-popup.modifytouch .ol-popup-content{white-space:nowrap;padding:0 .25em;font-size:.85em}.ol-popup.modifytouch .ol-popup-content a{text-decoration:none}.ol-popup.tooltips{background-color:#ffa}.ol-popup.tooltips .ol-popup-content{white-space:nowrap;padding:0 .25em;font-size:.85em}.ol-popup.default>div{background-color:#fff;border:1px solid #69f;border-radius:5px}.ol-popup.default{margin:-11px 0;transform:translateY(-22px)}.ol-popup-top.ol-popup.default{margin:11px 0;transform:none}.ol-popup-left.default{margin:-11px -22px;transform:translateY(-22px)}.ol-popup-top.ol-popup-left.default{margin:11px -22px;transform:none}.ol-popup-right.default{margin:-11px 22px;transform:translate(44px,-22px)}.ol-popup-top.ol-popup-right.default{margin:11px 22px;transform:translate(44px)}.ol-popup-middle.default{margin:0 10px;transform:none}.ol-popup-middle.ol-popup-right.default{margin:0 -10px;transform:translate(-20px)}.ol-popup.default .anchor{color:#69f}.ol-popup.default .anchor:after,.ol-popup.default .anchor:before{content:"";border:11px solid;border-color:currentColor #0000;margin:0 -11px}.ol-popup.default .anchor:after{border-width:11px;border-color:#fff #0000;margin:2px -11px}.ol-popup-top.default .anchor:before,.ol-popup-top.default .anchor:after{border-top:0;top:0}.ol-popup-bottom.default .anchor:before,.ol-popup-bottom.default .anchor:after{border-bottom:0;bottom:0}.ol-popup-middle.default .anchor:before{border-color:#0000 currentColor;margin:-11px -33px}.ol-popup-middle.default .anchor:after{border-color:#0000 #fff;margin:-11px -31px}.ol-popup-middle.ol-popup-left.default .anchor:before,.ol-popup-middle.ol-popup-left.default .anchor:after{border-left:0}.ol-popup-middle.ol-popup-right.default .anchor:before,.ol-popup-middle.ol-popup-right.default .anchor:after{border-right:0}.ol-popup.placemark{color:#c00;margin:-.65em 0;transform:translateY(-1.3em)}.ol-popup.placemark>div{width:2em;height:2em;min-width:unset;box-sizing:border-box;background-color:#fff;border:0;border-radius:50%;font-size:15px;position:relative;box-shadow:inset 0 0 0 .45em}.ol-popup.placemark .ol-popup-content{cursor:default;text-align:center;width:1em;height:1em;padding:.25em 0;line-height:1em;position:absolute;top:50%;left:50%;overflow:hidden;transform:translate(-50%,-50%)}.ol-popup.placemark .anchor{margin:0}.ol-popup.placemark .anchor:before{content:"";background:0 0;border-radius:50%;width:1em;height:.5em;margin:-.5em;box-shadow:0 1em .5em #00000080}.ol-popup.placemark .anchor:after{content:"";border:.7em solid #0000;border-top:1em solid;border-bottom:0 solid;margin:-.75em -.7em;bottom:0}.ol-popup.placemark.shield>div{border-radius:.2em}.ol-popup.placemark.shield .anchor:after{border-width:.8em 1em 0;margin:-.7em -1em}.ol-popup.placemark.blazon>div{border-radius:.2em}.ol-popup.placemark.pushpin{margin:-2.2em 0;transform:translateY(-4em)}.ol-popup.placemark.pushpin>div{border-radius:0;width:1.1em;box-shadow:inset 2em 0;background:0 0!important}.ol-popup.placemark.pushpin>div:before{content:"";pointer-events:none;border:.5em solid #0000;border-top:.3em solid;border-bottom-color:currentColor;width:1.3em;height:1.5em;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.ol-popup.placemark.needle{margin:-2em 0;transform:translateY(-4em)}.ol-popup.placemark.pushpin .anchor,.ol-popup.placemark.needle .anchor{margin:-1.2em}.ol-popup.placemark.pushpin .anchor:after,.ol-popup.placemark.needle .anchor:after{border-style:solid;border-width:2em .15em 0;width:.1em;margin:-.55em -.2em}.ol-popup.placemark.pushpin .anchor:before,.ol-popup.placemark.needle .anchor:before{margin:-.75em -.5em}.ol-popup.placemark.flagv{margin:-2em 1em;transform:translateY(-4em)}.ol-popup.placemark.flagv>div{box-shadow:none;background-color:#0000;border-radius:0}.ol-popup.placemark.flagv>div:before{content:"";pointer-events:none;border:1em solid #0000;border-left:2em solid;position:absolute}.ol-popup.placemark.flagv .anchor{margin:-1.4em}.ol-popup.placemark.flag{margin:-2em 1em;transform:translateY(-4em)}.ol-popup.placemark.flag>div{border-radius:0;transform-origin:0 150%!important}.ol-popup.placemark.flag .anchor{margin:-1.4em}.ol-popup.placemark.flagv .anchor:after,.ol-popup.placemark.flag .anchor:after{border-style:solid;border-width:2em .15em 0;width:.1em;margin:-.55em -1em}.ol-popup.placemark.flagv .anchor:before,.ol-popup.placemark.flag .anchor:before{margin:-.75em -1.25em}.ol-popup.placemark.flag.finish{margin:-2em 1em}.ol-popup.placemark.flag.finish>div{background-image:linear-gradient(45deg,currentColor 25%,#0000 25% 75%,currentColor 75%,currentColor),linear-gradient(45deg,currentColor 25%,#0000 25% 75%,currentColor 75%,currentColor);background-position:.5em 0,0 .5em;background-size:1em 1em;box-shadow:inset 0 0 0 .25em}.ol-popup.black .closeBox{color:#f80;background-color:#00000080;border-radius:5px}.ol-popup.black .closeBox:hover{color:#da2;background-color:#000000b3}.ol-popup.black{margin:-20px 0;transform:translateY(-40px)}.ol-popup.black>div{color:#fff;background-color:#0009;border-radius:5px}.ol-popup-top.ol-popup.black{margin:20px 0;transform:none}.ol-popup-left.black{margin:-20px -22px;transform:translateY(-40px)}.ol-popup-top.ol-popup-left.black{margin:20px -22px;transform:none}.ol-popup-right.black{margin:-20px 22px;transform:translate(44px,-40px)}.ol-popup-top.ol-popup-right.black{margin:20px 22px;transform:translate(44px)}.ol-popup-middle.black{margin:0 11px;transform:none}.ol-popup-left.ol-popup-middle.black{transform:none}.ol-popup-right.ol-popup-middle.black{margin:0 -11px;transform:translate(-22px)}.ol-popup.black .anchor{color:#0009;margin:-20px 11px}.ol-popup.black .anchor:before{content:"";border:20px solid;border-left:11px solid #0000;border-right:11px solid #0000}.ol-popup-top.black .anchor:before{border-top:0;top:0}.ol-popup-bottom.black .anchor:before{border-bottom:0;bottom:0}.ol-popup-middle.black .anchor:before{border-color:#0000 currentColor;margin:-20px -22px}.ol-popup-middle.ol-popup-left.black .anchor:before{border-left:0}.ol-popup-middle.ol-popup-right.black .anchor:before{border-right:0}.ol-popup-center.black .anchor:before{margin:0 -10px}.ol-popup.tips .closeBox{color:#fff;background-color:red;border-radius:50%;width:1.2em;height:1.2em}.ol-popup.tips .closeBox:hover{background-color:#f40}.ol-popup.tips{margin:-20px 0;transform:translateY(-40px)}.ol-popup.tips>div{color:#333;background-color:#cea;border:5px solid #ad7;border-radius:5px}.ol-popup-top.ol-popup.tips{margin:20px 0;transform:none}.ol-popup-left.tips{margin:-20px -22px;transform:translateY(-40px)}.ol-popup-top.ol-popup-left.tips{margin:20px -22px;transform:none}.ol-popup-right.tips{margin:-20px 22px;transform:translate(44px,-40px)}.ol-popup-top.ol-popup-right.tips{margin:20px 22px;transform:translate(44px)}.ol-popup-middle.tips{margin:0;transform:none}.ol-popup-left.ol-popup-middle.tips{margin:0 22px;transform:none}.ol-popup-right.ol-popup-middle.tips{margin:0 -22px;transform:translate(-44px)}.ol-popup.tips .anchor{color:#ad7;margin:-18px 22px}.ol-popup.tips .anchor:before{content:"";border:20px solid;border-left:11px solid #0000;border-right:11px solid #0000}.ol-popup-top.tips .anchor:before{border-top:0;top:0}.ol-popup-bottom.tips .anchor:before{border-bottom:0;bottom:0}.ol-popup-center.tips .anchor:before{border-width:20px 6px;margin:0 -6px}.ol-popup-left.tips .anchor:before{border-left:0;margin-left:0}.ol-popup-right.tips .anchor:before{border-right:0;margin-right:0}.ol-popup-middle.tips .anchor:before{border-width:6px 20px;border-color:#0000 currentColor;margin:-6px -41px}.ol-popup-middle.ol-popup-left.tips .anchor:before{border-left:0}.ol-popup-middle.ol-popup-right.tips .anchor:before{border-right:0}.ol-popup.warning .closeBox{color:#fff;background-color:red;border-radius:50%;font-size:.83em}.ol-popup.warning .closeBox:hover{background-color:#f40}.ol-popup.warning{color:#900;background-color:#fd0;border:4px dashed red;border-radius:3px;margin:-28px 10px;transform:translateY(-56px)}.ol-popup-top.ol-popup.warning{margin:28px 10px;transform:none}.ol-popup-left.warning{margin:-28px -22px;transform:translateY(-56px)}.ol-popup-top.ol-popup-left.warning{margin:28px -22px;transform:none}.ol-popup-right.warning{margin:-28px 22px;transform:translate(44px,-56px)}.ol-popup-top.ol-popup-right.warning{margin:28px 22px;transform:translate(44px)}.ol-popup-middle.warning{margin:0;transform:none}.ol-popup-left.ol-popup-middle.warning{margin:0 22px;transform:none}.ol-popup-right.ol-popup-middle.warning{margin:0 -22px;transform:translate(-44px)}.ol-popup.warning .anchor{margin:-33px 7px}.ol-popup.warning .anchor:before{content:"";border:30px solid red;border-left:11px solid #0000;border-right:11px solid #0000}.ol-popup-top.warning .anchor:before{border-top:0;top:0}.ol-popup-bottom.warning .anchor:before{border-bottom:0;bottom:0}.ol-popup-center.warning .anchor:before{margin:0 -21px}.ol-popup-middle.warning .anchor:before{border-width:10px 22px;border-color:#0000 red;margin:-10px -33px}.ol-popup-middle.ol-popup-left.warning .anchor:before{border-left:0}.ol-popup-middle.ol-popup-right.warning .anchor:before{border-right:0}.ol-popup .ol-popupfeature table{width:100%}.ol-popup .ol-popupfeature table td{text-overflow:ellipsis;max-width:25em;overflow:hidden}.ol-popup .ol-popupfeature table td img{max-width:100px;max-height:100px}.ol-popup .ol-popupfeature tr:nth-child(odd){background-color:#eee}.ol-popup .ol-popupfeature .ol-zoombt{color:#003c8880;background:0 0;border:0;outline:none;width:2em;height:2em;display:inline-block;position:relative}.ol-popup .ol-popupfeature .ol-zoombt:before{content:"";box-sizing:border-box;background-color:#0000;border:.17em solid;border-radius:100%;width:1em;height:1em;position:absolute;top:.3em;left:.3em}.ol-popup .ol-popupfeature .ol-zoombt:after{content:"";box-sizing:border-box;border-style:solid;border-width:.1em .3em;border-radius:.03em;position:absolute;top:1.35em;left:1.15em;transform:rotate(45deg);box-shadow:-.2em 0 0 -.04em}.ol-popup .ol-popupfeature .ol-count{float:right;margin:.25em 0}.ol-popup .ol-popupfeature .ol-prev,.ol-popup .ol-popupfeature .ol-next{vertical-align:bottom;cursor:pointer;border:.5em solid #0000;border-left-color:#003c8880;border-right:0 solid #003c8880;margin:0 .5em;display:inline-block}.ol-popup .ol-popupfeature .ol-prev{border-width:.5em .5em .5em 0}.ol-popup.tooltips.black{background-color:#0000}.ol-popup.tooltips.black>div{background-color:#00000080;padding:.2em .5em;transform:scaleY(1.3)}.ol-popup-middle.tooltips.black .anchor:before{border-width:5px 10px;margin:-5px -21px}.ol-popup-center.ol-popup-middle,.ol-popup-top.ol-popup-left.ol-fixPopup,.ol-popup-top.ol-popup-right.ol-fixPopup,.ol-popup.ol-fixPopup{margin:0}
18
18
  </style>
19
19
  <style>${this.customStyle}</style>
20
- <div class="map-info-contextmenu"><button class="contextmenu-closer gg-icon-button gg-small" onclick="${() => this.closeMenu()}"></button><div>${this.htmlUnsafe(this.state.position.tooltip?.content ?? '')}</div><svg class="tooltip-triangle" viewBox="0 0 20 10" preserveAspectRatio="none"><polygon points="0,0 10,10 20,0" class="triangle-fill"/><path d="M0,0 L10,10 L20,0" class="triangle-border"/></svg></div>`;
20
+ <div class="map-info-contextmenu"><div class="${this.tooltipTitle ? 'title-container' : 'title-container no-title'}"><span class="title">${this.tooltipTitle}</span> <button class="contextmenu-closer gg-icon-button gg-small" onclick="${() => this.closeMenu()}"></button></div><div>${this.tooltipContent}</div><svg class="tooltip-triangle" viewBox="0 0 20 10" preserveAspectRatio="none"><polygon points="0,0 10,10 20,0" class="triangle-fill"/><path d="M0,0 L10,10 L20,0" class="triangle-border"/></svg></div>`;
21
21
  };
22
22
  get map() {
23
23
  return this.context.mapManager.getMap();
24
24
  }
25
- // We use a static property for the overlay
26
- // Because OpenLayer is recreating a new object each time when we do an addOverlay()
27
- static contextMenuOverlay;
25
+ // We use a static property/getter for the overlay, because OpenLayer is recreating a new object each time when we do
26
+ // an addOverlay(). But as multiple instances of the Map (when using multiple <geogirafe-map ... /> in API context)
27
+ // should NOT share the same overlay, we need to create a new one for each map instance (indentified by Map's custom
28
+ // UUID Property).
29
+ static contextMenuOverlayByMap = {};
30
+ get contextMenuOverlay() {
31
+ const uuid = this.map.get('uuid');
32
+ return MapCustomContextMenuComponent.contextMenuOverlayByMap[uuid];
33
+ }
34
+ set contextMenuOverlay(value) {
35
+ const uuid = this.map.get('uuid');
36
+ MapCustomContextMenuComponent.contextMenuOverlayByMap[uuid] = value;
37
+ }
38
+ get tooltipTitle() {
39
+ const title = this.state.position.tooltip?.title;
40
+ if (!title) {
41
+ return '';
42
+ }
43
+ if (title.includes('<')) {
44
+ return this.htmlUnsafe(title);
45
+ }
46
+ return this.htmlUnsafe(this.context.i18nManager.getTranslation(title));
47
+ }
48
+ get tooltipContent() {
49
+ const content = this.state.position.tooltip?.content;
50
+ if (!content) {
51
+ return '';
52
+ }
53
+ if (content.includes('<')) {
54
+ return this.htmlUnsafe(content);
55
+ }
56
+ return this.htmlUnsafe(this.context.i18nManager.getTranslation(content));
57
+ }
28
58
  constructor() {
29
59
  super('custom-map-context-menu');
30
60
  }
31
61
  render() {
32
62
  if (this.state.position.tooltip) {
33
63
  super.render();
34
- MapCustomContextMenuComponent.contextMenuOverlay.setPosition(this.state.position.tooltip.position);
64
+ this.contextMenuOverlay.setPosition(this.state.position.tooltip.position);
35
65
  }
36
66
  else {
37
67
  super.renderEmpty();
@@ -53,12 +83,12 @@ class MapCustomContextMenuComponent extends GirafeHTMLElement {
53
83
  }
54
84
  connectedCallback() {
55
85
  super.connectedCallback();
56
- if (!MapCustomContextMenuComponent.contextMenuOverlay) {
57
- MapCustomContextMenuComponent.contextMenuOverlay = new Overlay({
86
+ if (!this.contextMenuOverlay) {
87
+ this.contextMenuOverlay = new Overlay({
58
88
  element: this,
59
89
  autoPan: { animation: { duration: 250 } }
60
90
  });
61
- this.map.addOverlay(MapCustomContextMenuComponent.contextMenuOverlay);
91
+ this.map.addOverlay(this.contextMenuOverlay);
62
92
  }
63
93
  this.registerEvents();
64
94
  }
@@ -55,6 +55,7 @@ export default class MapComponent extends GirafeHTMLElement {
55
55
  geolocationSource: VectorSource;
56
56
  private readonly markerSource;
57
57
  private readonly markerLayer;
58
+ private mapMarkerUnderCursor;
58
59
  get projection(): import("ol/proj.js").Projection;
59
60
  get config(): import("../../tools/main.js").GirafeConfig;
60
61
  get onMobile(): boolean;
@@ -82,6 +82,7 @@ export default class MapComponent extends GirafeHTMLElement {
82
82
  markerLayer = new VectorLayer({
83
83
  source: this.markerSource
84
84
  });
85
+ mapMarkerUnderCursor = null;
85
86
  get projection() {
86
87
  return this.olMap.getView().getProjection();
87
88
  }
@@ -362,6 +363,17 @@ export default class MapComponent extends GirafeHTMLElement {
362
363
  if (this.registerInteractionListener('map.select', false)) {
363
364
  // Simple click for single feature selection
364
365
  this.olMap.on('singleclick', (e) => {
366
+ if (this.mapMarkerUnderCursor) {
367
+ const mapMarker = this.mapMarkerUnderCursor.get('mapMarker');
368
+ if (mapMarker) {
369
+ this.state.position.tooltip = {
370
+ title: mapMarker.tooltip?.title,
371
+ content: mapMarker.tooltip?.content ?? '',
372
+ position: mapMarker.position
373
+ };
374
+ return;
375
+ }
376
+ }
365
377
  if (this.canExecute('map.select')) {
366
378
  this.onClick(e);
367
379
  }
@@ -396,6 +408,19 @@ export default class MapComponent extends GirafeHTMLElement {
396
408
  }
397
409
  onPointerMove(e) {
398
410
  this.state.mouseCoordinates = e.coordinate;
411
+ const featuresAtPixel = this.olMap.getFeaturesAtPixel(e.pixel, { layerFilter: (layer) => layer === this.markerLayer });
412
+ let cursor = 'default';
413
+ if (featuresAtPixel.length > 0) {
414
+ this.mapMarkerUnderCursor = featuresAtPixel[0];
415
+ const mapMarker = this.mapMarkerUnderCursor.get('mapMarker');
416
+ if (mapMarker?.tooltip) {
417
+ cursor = 'pointer';
418
+ }
419
+ }
420
+ else {
421
+ this.mapMarkerUnderCursor = null;
422
+ }
423
+ this.mapTarget.style.cursor = cursor;
399
424
  }
400
425
  onMoveEnd(_e) {
401
426
  const view = this.olMap.getView();
@@ -1028,9 +1053,18 @@ export default class MapComponent extends GirafeHTMLElement {
1028
1053
  this.markerSource.clear();
1029
1054
  }
1030
1055
  addMarker(mapMarker) {
1056
+ /*
1057
+ In OpenLayers we need to calculate the anchor based on the marker size and offset as it is `fraction` by default.
1058
+ As the offset and the size are provided in pixel, we need to calculate the fraction for each axis. So, for example,
1059
+ when you have size [21,25] and offset [-10.5,-25] the anchor must be [0.5, 1] to have the bottom center of the
1060
+ marker at the given coordinates.
1061
+ */
1062
+ const anchor = mapMarker.offset && mapMarker.size
1063
+ ? [Math.abs(mapMarker.offset[0] / mapMarker.size[0]), Math.abs(mapMarker.offset[1] / mapMarker.size[1])]
1064
+ : undefined;
1031
1065
  const iconStyle = new Style({
1032
1066
  image: new Icon({
1033
- displacement: mapMarker.offset,
1067
+ anchor: anchor,
1034
1068
  src: mapMarker.imageUrl,
1035
1069
  width: mapMarker.size ? mapMarker.size[0] : undefined,
1036
1070
  height: mapMarker.size ? mapMarker.size[1] : undefined
@@ -1040,6 +1074,7 @@ export default class MapComponent extends GirafeHTMLElement {
1040
1074
  geometry: new Point(mapMarker.position)
1041
1075
  });
1042
1076
  marker.setStyle(iconStyle);
1077
+ marker.set('mapMarker', mapMarker);
1043
1078
  this.markerSource.addFeature(marker);
1044
1079
  }
1045
1080
  showCrosshair(position) {
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.2588173445",
8
+ "version": "1.1.0-dev.2593337676",
9
9
  "type": "module",
10
10
  "engines": {
11
11
  "node": ">=20.19.0"
@@ -260,7 +260,7 @@
260
260
 
261
261
  <!-- Tooltip -->
262
262
  <h2>Add a toolip somewhere on the map</h2>
263
- <p class="descr">Add a tooltip with cutom text at the defined coordinates.</p>
263
+ <p class="descr">Add a tooltip with custom text and optional title at the defined coordinates.</p>
264
264
  <section>
265
265
  <div class="row">
266
266
  <div class="left">
@@ -321,7 +321,9 @@
321
321
  </p>
322
322
  <p class="descr">Allowed column names:</p>
323
323
  <ul>
324
- <li><strong>point</strong>: coordinates where the amrker should be added.</li>
324
+ <li><strong>point</strong>: coordinates where the marker should be added.</li>
325
+ <li><strong>title</strong> (optional): the title for the popup shown when clicking on the marker.</li>
326
+ <li><strong>description</strong> (optional): the description (content) of the popup shown when clicking on the marker.</li>
325
327
  <li><strong>icon</strong>: url to the marker image</li>
326
328
  <li><strong>iconSize</strong> (optional): size if the marker should be resized</li>
327
329
  <li><strong>iconOffset</strong> (optional): offset if the marker should be shifted</li>
@@ -1 +1 @@
1
- {"version":"1.1.0-dev.2588173445", "build":"2588173445", "date":"09/06/2026"}
1
+ {"version":"1.1.0-dev.2593337676", "build":"2593337676", "date":"11/06/2026"}
@@ -1,6 +1,6 @@
1
1
  id point title description icon iconSize iconOffset
2
2
  1 2611778,1266865 Popups konfigurieren <br>Popups können via der Text-Datei beliebig mit Inhalt gefüllt werden!<br/>Die anderen zwei Beispiel-Popups zeigen weitere Beispiele.<br><img src="/static/api/apihelp/img/geoportal-bs.jpg" width="300px"> api/marker-blue.png 21,25 -10.5,-25
3
- 2 2611542,1267266 Klänge der Basler Fasnacht <br/><br/>Audio-Dateien in ein Popup einbinden:<br/><br/><audio controls><source src="/static/api/apihelp/BS_N19_Fasnacht_v1.mp3" type="audio/mp3">Your browser does not support the audio element.</audio> api/marker-green.png 21,25 -10.5,-25
3
+ 2 2611542,1267266 Klänge der Basler Fasnacht <br/><br/>Audio-Dateien in ein Popup einbinden:<br/><br/><audio controls><source src="/static/api/apihelp/BS_N19_Fasnacht_v1.mp3" type="audio/mp3">Your browser does not support the audio element.</audio> api/marker-gold.png 21,25 -10.5,-25
4
4
  3 2613089,1267566 FILM AB! Für den GEO-Beruf <br>IFrames in ein Popup einbinden: <br/><br/><iframe width="312" height="175" src="https://www.youtube.com/embed/-Mw277Rgcyc" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> api/parking.png 21,25 -10.5,-25
5
5
  4 2611738,1267123 Test for Offset/Size Test for Offset/Size api/marker-huge.png 24,38 -162,-512
6
- 5 2611738,1267123 Test for Offset/Size Test for Offset/Size api/marker-huge.png 24,38 -12,-38
6
+ 5 2611638,1267183 Test for Translation This Text should be translated api/marker-green.png 24,38 -12,-38
@@ -5,7 +5,7 @@
5
5
  "zoom": "11",
6
6
  "basemap": "ArcGIS Imagery",
7
7
  "crosshair": "1631989, 5067094",
8
- "tooltip": "1631989, 5067094 | Hello from the GeoGirafe API!",
8
+ "tooltip": "1631989, 5067094 | GeoGirafe API | Hello from the GeoGirafe API!",
9
9
  "layers": "OpenTopoMap",
10
10
  "multiLayers": "GL_AbschnittsStoergrade, SW_Jugendlichenanteil, KJ_Jugendangebot",
11
11
  "layersWithConfig": "GR_Grundwasserschutzzonen|o;0.4|f;gr_gz_kantypbez;eq;S1",
package/tools/main.d.ts CHANGED
@@ -86,7 +86,7 @@ export type { default as IGirafePanel } from './state/igirafepanel.js';
86
86
  export { isGirafePanel } from './state/igirafepanel.js';
87
87
  export { default as LayersConfig } from './state/layersConfig.js';
88
88
  export { default as MapManager } from './state/mapManager.js';
89
- export type { MapMarker } from './state/mapposition.js';
89
+ export type { MapMarkerTooltip, MapMarker } from './state/mapposition.js';
90
90
  export { default as MapPosition } from './state/mapposition.js';
91
91
  export type { InitialSelectionQuery } from './state/objectselection.js';
92
92
  export { default as ObjectSelection } from './state/objectselection.js';
@@ -123,6 +123,7 @@ export { generateQrCode } from './utils/qrcode.js';
123
123
  export { default as ServiceWorkerHelper } from './utils/swhelper.js';
124
124
  export { systemIsInDarkMode, isSafari, isFirefox, getValidIndex, minMax, hexToRgbaArray, rgbStrToRgbaArray, colorToRgbaArray, isValidEmail, applyOpacityToLayers, applyFeaturesToSelection, linkify, applyDefaultPrefixToUrl, splitTrimAndConvertToNumber } from './utils/utils.js';
125
125
  export { default as VendorSpecificOgcServerManager } from './vendorspecificogcservermanager.js';
126
+ export { CompositeCurveGML3, CompositeCurveGML32 } from './wfs/compositecurvegml.js';
126
127
  export type { WfsClientOptions, WfsClientOptionalOptions, QueryableLayerWms, GetFeatureOptionsPartial } from './wfs/wfsclient.js';
127
128
  export { default as WfsClient, WfsClientMapServer, WfsClientQgis, WfsClientGeorama, WfsClientDefault, WfsClientGeoServer } from './wfs/wfsclient.js';
128
129
  export { default as WfsFilter } from './wfs/wfsfilter.js';
package/tools/main.js CHANGED
@@ -96,6 +96,7 @@ export { generateQrCode } from './utils/qrcode.js';
96
96
  export { default as ServiceWorkerHelper } from './utils/swhelper.js';
97
97
  export { systemIsInDarkMode, isSafari, isFirefox, getValidIndex, minMax, hexToRgbaArray, rgbStrToRgbaArray, colorToRgbaArray, isValidEmail, applyOpacityToLayers, applyFeaturesToSelection, linkify, applyDefaultPrefixToUrl, splitTrimAndConvertToNumber } from './utils/utils.js';
98
98
  export { default as VendorSpecificOgcServerManager } from './vendorspecificogcservermanager.js';
99
+ export { CompositeCurveGML3, CompositeCurveGML32 } from './wfs/compositecurvegml.js';
99
100
  export { default as WfsClient, WfsClientMapServer, WfsClientQgis, WfsClientGeorama, WfsClientDefault, WfsClientGeoServer } from './wfs/wfsclient.js';
100
101
  export { default as WfsFilter } from './wfs/wfsfilter.js';
101
102
  export { default as WfsFilterCondition, isWfsOperator } from './wfs/wfsfiltercondition.js';
@@ -5,6 +5,7 @@ import { defaults as defaultControls } from 'ol/control/defaults.js';
5
5
  import { defaults as defaultInteractions } from 'ol/interaction/defaults.js';
6
6
  import { DragPan } from 'ol/interaction.js';
7
7
  import { Kinetic } from 'ol';
8
+ import { v4 as uuidv4 } from 'uuid';
8
9
  /** The singleton containing the main OpenLayers map accessible from everywhere */
9
10
  export default class MapManager extends GirafeSingleton {
10
11
  map;
@@ -26,6 +27,9 @@ export default class MapManager extends GirafeSingleton {
26
27
  interactions: interactions,
27
28
  moveTolerance: 5
28
29
  });
30
+ // Needed to identify the map instance for reusing Overlay (or not) in the context menu (see
31
+ // CustomContextMenu.contextMenuOverlayByMap)
32
+ this.map.set('uuid', uuidv4());
29
33
  }
30
34
  getMap() {
31
35
  if (!this.map) {
@@ -1,9 +1,14 @@
1
1
  import { Coordinate } from 'ol/coordinate.js';
2
+ export type MapMarkerTooltip = {
3
+ title: string;
4
+ content?: string;
5
+ };
2
6
  export type MapMarker = {
3
7
  imageUrl: string;
4
8
  position: Coordinate;
5
9
  size?: number[];
6
10
  offset?: number[];
11
+ tooltip?: MapMarkerTooltip;
7
12
  };
8
13
  declare class MapPosition {
9
14
  center: Coordinate;
@@ -12,6 +17,7 @@ declare class MapPosition {
12
17
  scale?: number;
13
18
  crosshair?: Coordinate;
14
19
  tooltip?: {
20
+ title?: string;
15
21
  content: string;
16
22
  position?: Coordinate;
17
23
  };
@@ -24,6 +24,7 @@ class MapPosition {
24
24
  position.crosshair = this.crosshair ? [...this.crosshair] : undefined;
25
25
  position.tooltip = this.tooltip
26
26
  ? {
27
+ title: this.tooltip.title,
27
28
  content: this.tooltip.content,
28
29
  position: this.tooltip.position ? [...this.tooltip.position] : undefined
29
30
  }
@@ -33,7 +34,8 @@ class MapPosition {
33
34
  imageUrl: marker.imageUrl,
34
35
  position: marker.position,
35
36
  size: marker.size,
36
- offset: marker.offset
37
+ offset: marker.offset,
38
+ tooltip: marker.tooltip,
37
39
  });
38
40
  }
39
41
  return position;
@@ -78,7 +78,8 @@ export declare const linkify: (str: string) => string;
78
78
  */
79
79
  export declare function applyDefaultPrefixToUrl(context: IGirafeContext, metadataUrl?: string): string | undefined;
80
80
  /**
81
- * Splits a String containing Numbers into an Array of Numbers. The Separator is a comma.
81
+ * Splits a String containing Numbers into an Array of Numbers. The Separator is a comma. Or returns
82
+ * <code>undefined</code> if the input is <code>undefined</code>
82
83
  * @param numbersAsString The String containing Numbers.
83
84
  */
84
- export declare const splitTrimAndConvertToNumber: (numbersAsString: string) => number[];
85
+ export declare const splitTrimAndConvertToNumber: (numbersAsString: string | undefined) => number[] | undefined;
@@ -200,12 +200,13 @@ export function applyDefaultPrefixToUrl(context, metadataUrl) {
200
200
  return metadataUrl;
201
201
  }
202
202
  /**
203
- * Splits a String containing Numbers into an Array of Numbers. The Separator is a comma.
203
+ * Splits a String containing Numbers into an Array of Numbers. The Separator is a comma. Or returns
204
+ * <code>undefined</code> if the input is <code>undefined</code>
204
205
  * @param numbersAsString The String containing Numbers.
205
206
  */
206
207
  export const splitTrimAndConvertToNumber = (numbersAsString) => {
207
- return numbersAsString
208
+ return numbersAsString ? numbersAsString
208
209
  .split(',')
209
210
  .map((e) => e.trim())
210
- .map(Number);
211
+ .map(Number) : undefined;
211
212
  };
@@ -0,0 +1,18 @@
1
+ import GML3 from 'ol/format/GML3.js';
2
+ import GML32 from 'ol/format/GML32.js';
3
+ import { Options as GmlOptions } from 'ol/format/GMLBase.js';
4
+ import LineString from 'ol/geom/LineString.js';
5
+ import MultiLineString from 'ol/geom/MultiLineString.js';
6
+ type CompositeGeometry = LineString | MultiLineString;
7
+ type ObjectStack = Array<unknown>;
8
+ export declare class CompositeCurveGML3 extends GML3 {
9
+ constructor(options?: GmlOptions);
10
+ readCompositeCurve(node: Node, objectStack: ObjectStack): CompositeGeometry;
11
+ readOrientableCurve(node: Node, objectStack: ObjectStack): CompositeGeometry;
12
+ }
13
+ export declare class CompositeCurveGML32 extends GML32 {
14
+ constructor(options?: GmlOptions);
15
+ readCompositeCurve(node: Node, objectStack: ObjectStack): CompositeGeometry;
16
+ readOrientableCurve(node: Node, objectStack: ObjectStack): CompositeGeometry;
17
+ }
18
+ export {};
@@ -0,0 +1,160 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import GML3 from 'ol/format/GML3.js';
3
+ import GML32 from 'ol/format/GML32.js';
4
+ import LineString from 'ol/geom/LineString.js';
5
+ import MultiLineString from 'ol/geom/MultiLineString.js';
6
+ import { makeReplacer } from 'ol/xml.js';
7
+ const GML31_NS = 'http://www.opengis.net/gml';
8
+ const GML32_NS = 'http://www.opengis.net/gml/3.2';
9
+ const XLINK_NS = 'http://www.w3.org/1999/xlink';
10
+ export class CompositeCurveGML3 extends GML3 {
11
+ constructor(options) {
12
+ super(options);
13
+ registerCompositeCurveParser(this, GML31_NS);
14
+ }
15
+ readCompositeCurve(node, objectStack) {
16
+ return readCompositeCurve(this, node, objectStack);
17
+ }
18
+ readOrientableCurve(node, objectStack) {
19
+ return readOrientableCurve(this, node, objectStack);
20
+ }
21
+ }
22
+ export class CompositeCurveGML32 extends GML32 {
23
+ constructor(options) {
24
+ super(options);
25
+ registerCompositeCurveParser(this, GML32_NS);
26
+ }
27
+ readCompositeCurve(node, objectStack) {
28
+ return readCompositeCurve(this, node, objectStack);
29
+ }
30
+ readOrientableCurve(node, objectStack) {
31
+ return readOrientableCurve(this, node, objectStack);
32
+ }
33
+ }
34
+ function registerCompositeCurveParser(parser, namespace) {
35
+ parser.GEOMETRY_PARSERS[namespace] = {
36
+ ...parser.GEOMETRY_PARSERS[namespace],
37
+ CompositeCurve: makeReplacer(parser.readCompositeCurve, parser)
38
+ };
39
+ parser.CURVEMEMBER_PARSERS[namespace] = {
40
+ ...parser.CURVEMEMBER_PARSERS[namespace],
41
+ CompositeCurve: makeReplacer(parser.readCompositeCurve, parser),
42
+ OrientableCurve: makeReplacer(parser.readOrientableCurve, parser)
43
+ };
44
+ }
45
+ function readCompositeCurve(parser, node, objectStack) {
46
+ const pieces = [];
47
+ for (const member of Array.from(node.children)) {
48
+ if (member.localName !== 'curveMember' && member.localName !== 'curveMembers') {
49
+ continue;
50
+ }
51
+ pieces.push(...readCompositeCurveMember(parser, member, objectStack));
52
+ }
53
+ if (pieces.length === 0) {
54
+ console.warn(`CompositeCurve at ${describeNode(node)} did not contain convertible curve members.`);
55
+ return new MultiLineString([]);
56
+ }
57
+ return buildCompositeGeometry(pieces);
58
+ }
59
+ function readCompositeCurveMember(parser, member, objectStack) {
60
+ const href = member.getAttributeNS(XLINK_NS, 'href') ?? member.getAttribute('xlink:href');
61
+ if (href) {
62
+ console.warn(`CompositeCurve member reference ${href} cannot be resolved locally.`);
63
+ return [];
64
+ }
65
+ const pieces = [];
66
+ for (const child of Array.from(member.children)) {
67
+ pieces.push(...readCurveElement(parser, child, objectStack));
68
+ }
69
+ if (pieces.length === 0) {
70
+ console.warn(`CompositeCurve member at ${describeNode(member)} did not contain a convertible curve.`);
71
+ }
72
+ return pieces;
73
+ }
74
+ function readCurveElement(parser, node, objectStack) {
75
+ if (node.localName === 'LineString') {
76
+ return lineStringToPieces(parser.readLineString(node, objectStack));
77
+ }
78
+ if (node.localName === 'Curve') {
79
+ warnIfCurveHasUnsupportedSegments(node);
80
+ return lineStringToPieces(parser.readCurve(node, objectStack));
81
+ }
82
+ if (node.localName === 'CompositeCurve') {
83
+ return geometryToPieces(parser.readCompositeCurve(node, objectStack));
84
+ }
85
+ if (node.localName === 'OrientableCurve') {
86
+ return geometryToPieces(parser.readOrientableCurve(node, objectStack));
87
+ }
88
+ console.warn(`CompositeCurve member contains unsupported ${describeNode(node)}.`);
89
+ return [];
90
+ }
91
+ function readOrientableCurve(parser, node, objectStack) {
92
+ const baseCurve = Array.from(node.children).find((child) => child.localName === 'baseCurve');
93
+ if (!baseCurve) {
94
+ console.warn(`OrientableCurve at ${describeNode(node)} does not contain baseCurve.`);
95
+ return new MultiLineString([]);
96
+ }
97
+ const pieces = Array.from(baseCurve.children).flatMap((child) => readCurveElement(parser, child, objectStack));
98
+ if (node.getAttribute('orientation') === '-') {
99
+ pieces.reverse();
100
+ for (const piece of pieces) {
101
+ piece.reverse();
102
+ }
103
+ }
104
+ return buildCompositeGeometry(pieces);
105
+ }
106
+ function warnIfCurveHasUnsupportedSegments(node) {
107
+ const segments = Array.from(node.children).find((child) => child.localName === 'segments');
108
+ if (!segments) {
109
+ return;
110
+ }
111
+ const unsupportedSegment = Array.from(segments.children).find((child) => child.localName !== 'LineStringSegment');
112
+ if (unsupportedSegment) {
113
+ console.warn(`CompositeCurve member contains unsupported ${describeNode(unsupportedSegment)} segment.`);
114
+ }
115
+ }
116
+ function lineStringToPieces(lineString) {
117
+ if (!lineString) {
118
+ return [];
119
+ }
120
+ const coordinates = lineString.getCoordinates();
121
+ return coordinates.length > 0 ? [coordinates] : [];
122
+ }
123
+ function geometryToPieces(geometry) {
124
+ if (geometry instanceof LineString) {
125
+ return lineStringToPieces(geometry);
126
+ }
127
+ return geometry.getCoordinates();
128
+ }
129
+ function buildCompositeGeometry(pieces) {
130
+ const normalizedPieces = pieces.filter((piece) => piece.length > 0);
131
+ if (normalizedPieces.length === 0) {
132
+ return new MultiLineString([]);
133
+ }
134
+ const groups = [];
135
+ for (const piece of normalizedPieces) {
136
+ const current = clonePiece(piece);
137
+ const previous = groups.at(-1);
138
+ if (previous && coordinatesEqual(previous[previous.length - 1], current[0])) {
139
+ previous.push(...current.slice(1));
140
+ }
141
+ else {
142
+ groups.push(current);
143
+ }
144
+ }
145
+ return groups.length === 1 ? new LineString(groups[0]) : new MultiLineString(groups);
146
+ }
147
+ function clonePiece(piece) {
148
+ return piece.map((coordinate) => [...coordinate]);
149
+ }
150
+ function coordinatesEqual(first, second) {
151
+ if (first.length !== second.length) {
152
+ console.warn('CompositeCurve contains members with inconsistent coordinate dimensions.');
153
+ return false;
154
+ }
155
+ return first.every((value, index) => value === second[index]);
156
+ }
157
+ function describeNode(node) {
158
+ const id = node.getAttribute('gml:id') ?? node.getAttribute('id');
159
+ return id ? `${node.localName}#${id}` : node.localName;
160
+ }
@@ -1,5 +1,6 @@
1
1
  import { WFS as OlWFS } from 'ol/format.js';
2
2
  import GML32 from 'ol/format/GML32.js';
3
+ import { CompositeCurveGML3, CompositeCurveGML32 } from './compositecurvegml.js';
3
4
  /**
4
5
  * WFS parser
5
6
  *
@@ -14,7 +15,22 @@ import GML32 from 'ol/format/GML32.js';
14
15
  */
15
16
  export default class WfsParser extends OlWFS {
16
17
  constructor(opt_options) {
17
- super(opt_options);
18
+ const options = {
19
+ gmlFormat: opt_options?.gmlFormat,
20
+ version: opt_options?.version,
21
+ featureNS: opt_options?.featureNS,
22
+ featureType: opt_options?.featureType,
23
+ schemaLocation: opt_options?.schemaLocation
24
+ };
25
+ if (!options.gmlFormat) {
26
+ if (options.version === '2.0.0') {
27
+ options.gmlFormat = new CompositeCurveGML32(options);
28
+ }
29
+ else if (!options.version || options.version === '1.1.0') {
30
+ options.gmlFormat = new CompositeCurveGML3(options);
31
+ }
32
+ }
33
+ super(options);
18
34
  }
19
35
  readFeatures(source, opt_options) {
20
36
  // @ts-expect-error gmlFormat_ is private in super class