@geogirafe/lib-geoportal 1.2.0-dev.2763713312 → 1.2.0-dev.2766641185

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.
@@ -54,6 +54,7 @@
54
54
  "Connecting to the print server, please wait.": "Verbindung zum Druckserver wird hergestellt, bitte warten.",
55
55
  "contact-panel": "Kontaktformular",
56
56
  "Control opacity": "Deckkraft steuern",
57
+ "Convert to drawing": "In Zeichnung umwandeln",
57
58
  "Copy ShortUrl": "Kurz-URL kopieren",
58
59
  "Copy to Clipboard": "In die Zwischenablage kopieren",
59
60
  "Copy value": "Wert kopieren",
@@ -188,6 +189,7 @@
188
189
  "feedback-thankyou-default": "Vielen Dank für Ihr Feedback!",
189
190
  "File _fileName_ is not supported": "Datei _fileName_ wird nicht unterstützt",
190
191
  "File from": "Datei vom",
192
+ "File loaded in the layer tree": "✓ Die Datei wurde in den Layerbaum geladen",
191
193
  "Files _fileNames_ are not supported": "Dateien '_fileNames_' werden nicht unterstützt",
192
194
  "Fill color": "Füllfarbe",
193
195
  "Filter": "Filter",
@@ -352,6 +354,7 @@
352
354
  "onboarding-treeview-filter-selection-description": "Sie können die in der Baumansicht vorhandenen Layer filtern",
353
355
  "onboarding-treeview-filter-selection-title": "Layer filtern",
354
356
  "Only WMTS layers are saved": "Nur WMTS-Layer werden gespeichert",
357
+ "only-n-features-loaded": "Nur _loadedCount_ von _totalCount_ Objekten konnten geladen werden. Überprüfen Sie, ob diese Objekte innerhalb des in Ihrer Anwendung konfigurierten maximalen Ausschnitts angezeigt werden können.",
355
358
  "Open advanced filter tool": "Erweiterte Filter öffnen",
356
359
  "open-buttonbar-menu": "Menü öffnen",
357
360
  "OpenStreetMap": "OpenStreetMap",
@@ -55,6 +55,7 @@
55
55
  "contact-panel": "Contact us",
56
56
  "contains": "contains",
57
57
  "Control opacity": "Control opacity",
58
+ "Convert to drawing": "Convert to drawing",
58
59
  "Copy ShortUrl": "Copy ShortUrl",
59
60
  "Copy to Clipboard": "Copy to Clipboard",
60
61
  "Copy value": "Copier value",
@@ -190,6 +191,7 @@
190
191
  "feedback-thankyou-default": "Thank you for your feedback!",
191
192
  "File _fileName_ is not supported": "File _fileName_ is not supported",
192
193
  "File from": "File from",
194
+ "File loaded in the layer tree": "✓ File loaded in the layer tree",
193
195
  "Files _fileNames_ are not supported": "Files _fileNames_ are not supported",
194
196
  "Fill color": "Fill color",
195
197
  "Filter": "Filter",
@@ -354,6 +356,7 @@
354
356
  "onboarding-treeview-filter-selection-description": "You have the possibility to filter the layers present in the treeview",
355
357
  "onboarding-treeview-filter-selection-title": "Filter layer",
356
358
  "Only WMTS layers are saved": "Only WMTS layers are saved",
359
+ "only-n-features-loaded": "Only _loadedCount_ features among _totalCount_ could be loaded. Verify that those features can be displayed within the maximal extent configured in your application.",
357
360
  "Open advanced filter tool": "Open advanced filter tool",
358
361
  "open-buttonbar-menu": "Open Menu",
359
362
  "OpenStreetMap": "OpenStreetMap",
@@ -54,6 +54,7 @@
54
54
  "Connecting to the print server, please wait.": "Connexion au serveur d'impression, veuillez patienter.",
55
55
  "contact-panel": "Nous contacter",
56
56
  "Control opacity": "Contrôler l'opacité",
57
+ "Convert to drawing": "Convertir en dessin",
57
58
  "Copy ShortUrl": "Copier le lien court",
58
59
  "Copy to Clipboard": "Copier dans le presse-papiers",
59
60
  "Copy value": "Copier la valeur",
@@ -188,6 +189,7 @@
188
189
  "feedback-thankyou-default": "Merci pour votre commentaire !",
189
190
  "File _fileName_ is not supported": "Le fichier _fileName_ n'est pas supporté",
190
191
  "File from": "Fichier de",
192
+ "File loaded in the layer tree": "✓ Le fichier a été chargé dans l'arbre des couches",
191
193
  "Files _fileNames_ are not supported": "Les fichiers _fileNames_ ne sont pas supportés",
192
194
  "Fill color": "Couleur de remplissage",
193
195
  "Filter": "Filtrer",
@@ -352,6 +354,7 @@
352
354
  "onboarding-treeview-filter-selection-description": "Vous pouvez filtrer les couches présentes dans l'arborescence",
353
355
  "onboarding-treeview-filter-selection-title": "Filtrer les couches",
354
356
  "Only WMTS layers are saved": "Seules les couches WMTS sont sauvegardées",
357
+ "only-n-features-loaded": "Seules _loadedCount_ entités sur _totalCount_ ont pu être chargées. Vérifiez que ces entités peuvent être affichées dans l'étendue maximale configurée dans votre application.",
355
358
  "Open advanced filter tool": "Ouvrir l'outil de filtre avancé",
356
359
  "open-buttonbar-menu": "Ouvrir le menu",
357
360
  "OpenStreetMap": "OpenStreetMap",
@@ -54,6 +54,7 @@
54
54
  "Connecting to the print server, please wait.": "Connessione al server di stampa, attendere prego.",
55
55
  "contact-panel": "Contattaci",
56
56
  "Control opacity": "Controlla opacità",
57
+ "Convert to drawing": "Converti in disegno",
57
58
  "Copy ShortUrl": "Copia ShortUrl",
58
59
  "Copy to Clipboard": "Copia negli appunti",
59
60
  "Copy value": "Copia il valore",
@@ -188,6 +189,7 @@
188
189
  "feedback-thankyou-default": "Grazie per il tuo feedback!",
189
190
  "File _fileName_ is not supported": "Il file _fileName_ non è supportato",
190
191
  "File from": "File da",
192
+ "File loaded in the layer tree": "✓ Il file è stato caricato nell'albero dei layer",
191
193
  "Files _fileNames_ are not supported": "I file _fileNames_ non sono supportati",
192
194
  "Fill color": "Colore di riempimento",
193
195
  "Filter": "Filtra",
@@ -352,6 +354,7 @@
352
354
  "onboarding-treeview-filter-selection-description": "Puoi filtrare i livelli presenti nella struttura ad albero",
353
355
  "onboarding-treeview-filter-selection-title": "Filtra i livelli",
354
356
  "Only WMTS layers are saved": "Solo i layer WMTS vengono salvati",
357
+ "only-n-features-loaded": "Solo _loadedCount_ elementi su _totalCount_ hanno potuto essere caricati. Verificare che questi elementi possano essere visualizzati all'interno dell'estensione massima configurata nella tua applicazione.",
355
358
  "Open advanced filter tool": "Apri lo strumento di filtro avanzato",
356
359
  "open-buttonbar-menu": "Apri menu",
357
360
  "OpenStreetMap": "OpenStreetMap",
@@ -17,6 +17,7 @@ declare class ExternalLayersComponent extends GirafeHTMLElement implements IGira
17
17
  panelTitle: string;
18
18
  panelTogglePath: string;
19
19
  loading: boolean;
20
+ fileLoaded: boolean;
20
21
  predefinedSources: PredefinedSource[];
21
22
  get predefinedWmsWmtsSources(): PredefinedSource[];
22
23
  get hasPredefinedWmsWmtsSources(): boolean;
@@ -43,7 +44,6 @@ declare class ExternalLayersComponent extends GirafeHTMLElement implements IGira
43
44
  deselectVisible(): void;
44
45
  deselectAll(): void;
45
46
  addSelectedLayers(): void;
46
- get fileDescription(): string;
47
47
  loadFile(): Promise<void>;
48
48
  protected connectedCallback(): void;
49
49
  }
@@ -16,16 +16,17 @@ class ExternalLayersComponent extends GirafeHTMLElement {
16
16
  return uHtml `<style>
17
17
  .hidden{display:none!important}.gg-rotate90{transform:rotate(90deg)}.gg-rotate180{transform:rotate(180deg)}.gg-rotate270{transform:rotate(270deg)}img{filter:var(--svg-filter)}img.legend-image{filter:var(--svg-map-filter);background:var(--svg-legend-bkg)}div{scrollbar-width:thin}a,a:visited{color:var(--link-color)}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes spin-wait{0%{transform:rotate(0)}7%{transform:rotate(360deg)}to{transform:rotate(360deg)}}.gg-spin{animation-name:spin;animation-duration:2s;animation-timing-function:linear;animation-iteration-count:infinite}.gg-spin-wait{animation-name:spin-wait;animation-duration:10s;animation-timing-function:linear;animation-iteration-count:infinite}::-webkit-scrollbar{width:5px}::-webkit-scrollbar-thumb{background:#999}button,input,select,textarea{font:inherit}.gg-button,.gg-select,.gg-input,.gg-textarea{background-color:var(--bkg-color);color:var(--text-color);border:var(--app-standard-border);box-sizing:border-box;cursor:pointer;border-radius:3px;outline:0;margin:0;padding:0 0 0 .5rem;display:inline-block}.gg-label{background-color:var(--bkg-color);color:var(--text-color);border:none;align-items:center;margin:0;padding:0;display:flex}.gg-button,.gg-select,.gg-input,.gg-label{min-height:calc(var(--app-standard-height) / 1.5)}.gg-textarea{max-height:initial;resize:vertical;height:6rem;padding:.5rem;line-height:1.3rem}.gg-input{cursor:text}.gg-checkbox{accent-color:var(--text-color);width:1.2rem}.gg-range{accent-color:var(--text-color)}.gg-button{padding:0 .5rem}.gg-button.active{border:solid 1px var(--text-color-grad1);background-color:var(--bkg-color-grad2)}.gg-button:disabled{background-color:var(--bkg-color-grad1);color:var(--text-color-grad2);cursor:not-allowed;border:none}.gg-button:disabled img{filter:opacity(.6)}.gg-input:disabled,.gg-select:disabled,.gg-textarea:disabled{background-color:var(--bkg-color-grad1);color:var(--text-color-grad2);cursor:not-allowed}.gg-button>img{vertical-align:middle}.gg-icon-button{color:var(--text-color);cursor:pointer;background-color:#0000;border:none;flex-direction:column;justify-content:center;align-items:center;padding:0;display:flex}.gg-icon{justify-content:center;align-items:center;display:flex}.gg-big,.gg-big-withtext{min-width:var(--app-standard-height);min-height:var(--app-standard-height);max-height:var(--app-standard-height)}.gg-big img,.gg-big-withtext img{width:calc(var(--app-standard-height) - 1.5rem);margin:0}.gg-big-withtext span{font-variant:small-caps;padding:0 1rem;font-size:.9rem}.gg-medium,.gg-medium-withtext{min-width:calc(var(--app-standard-height) / 1.2);min-height:calc(var(--app-standard-height) / 1.2);max-height:calc(var(--app-standard-height) / 1.2);flex-direction:row}.gg-medium img{width:calc(var(--app-standard-height) / 2.4);margin:0}.gg-medium-withtext img{width:calc(var(--app-standard-height) / 2.4);margin-left:.5rem}.gg-medium-withtext span{padding:0 1rem 0 .5rem;font-size:.9rem}.gg-small,.gg-small-withtext{min-width:calc(var(--app-standard-height) / 2);min-height:calc(var(--app-standard-height) / 2);max-height:calc(var(--app-standard-height) / 2);flex-direction:row}.gg-small img{width:calc(var(--app-standard-height) / 3);margin:0}.gg-small-withtext img{width:calc(var(--app-standard-height) / 3);margin-left:.5rem}.gg-small-withtext span{padding:0 .5rem 0 .3rem;font-size:.9rem}.gg-button:hover:not(:disabled),.gg-select:hover:not(:disabled),.gg-input:hover:not(:disabled),.gg-textarea:hover:not(:disabled),.gg-icon-button:hover:not(:disabled){background-color:var(--bkg-color-grad1)}.gg-opacity{opacity:.5}.gg-opacity:hover{opacity:1;background-color:#0000}.gg-tabs{cursor:pointer;grid-auto-flow:column;padding-bottom:1rem;font-size:1rem;display:grid}.gg-tab{border:none;border-bottom:var(--app-standard-border);cursor:pointer;color:var(--text-color);background:0 0;padding:.5rem}.gg-tab.active{border-bottom:solid 1px var(--text-color)}.girafe-button-big,.girafe-button-large,.girafe-button-small,.girafe-button-tiny{color:var(--text-color);background-color:#0000;border:none;flex-direction:column;display:flex}.girafe-button-big:hover,.girafe-button-large:hover,.girafe-button-small:hover,.girafe-button-tiny:hover{background-color:var(--bkg-color-grad1);cursor:pointer}.girafe-button-big.dark,.girafe-button-large.dark,.girafe-button-small.dark,.girafe-button-tiny.dark{background-color:var(--bkg-color);filter:invert()}.girafe-button-big{width:var(--app-standard-height);height:var(--app-standard-height);align-items:center;padding:1rem}.girafe-button-big img{overflow:hidden}.girafe-button-large{flex-direction:row}.girafe-button-large img{height:2rem;margin:.3rem}.girafe-button-large span{height:2rem;margin:.3rem;line-height:2rem}.girafe-button-small{min-width:calc(var(--app-standard-height) / 2);height:calc(var(--app-standard-height) / 2);align-items:center;padding:.5rem}.girafe-button-small img{overflow:hidden}.girafe-button-small span{text-align:left;text-overflow:ellipsis;width:100%;overflow:hidden}.girafe-button-tiny{align-items:center;width:1rem;height:1rem;padding:0}.girafe-button-tiny img{overflow:hidden}.girafe-onboarding-theme{background-color:var(--bkg-color)!important;color:var(--text-color)!important}.girafe-onboarding-theme button{background-color:var(--bkg-color)!important;color:var(--text-color)!important;text-shadow:none!important}.girafe-onboarding-theme button.driver-popover-close-btn{z-index:10000}.tippy-box{background-color:var(--bkg-color)!important;border:var(--app-standard-border)!important}.tippy-content{color:var(--text-color)!important}.tippy-arrow{color:var(--bkg-color)!important}.tippy-box[data-theme~=error]{background-color:var(--error-color);white-space:pre-line}div.tippy-box[data-theme=image-popup]{border:var(--app-standard-border)!important;& div.tippy-content{background-color:var(--bkg-color)!important;& img{max-width:20rem;max-height:20rem}}& div.tippy-arrow{color:var(--bkg-color)!important}}
18
18
  </style><style>
19
- #panel{background:var(--bkg-color);height:100%;color:var(--text-color);flex-direction:column;padding:0 1rem;display:flex}#content{flex-direction:column;flex-grow:1;margin:0;display:flex}header{background-color:var(--bkg-color);margin:0;position:sticky;top:-5px}h4{margin-bottom:.5rem}ul{margin-top:0;margin-left:0;padding-left:2rem;line-height:1.3rem;list-style-type:disc}.clearable-input{background-color:var(--bkg-color);border:1px solid #cfd6dd;border-radius:4px;flex-direction:row;margin-bottom:.1rem;display:flex}.scan-button{float:right}.layers{border-top:1px solid #ccc;flex-direction:column;flex-grow:1;margin-top:1rem;padding-top:.5rem;display:flex;overflow:hidden}ul.results{border:none;margin-bottom:.5rem;padding:0;list-style:none;overflow-y:auto}.result{height:1.6rem;display:flex}.result>button>span{text-align:left;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}#panel>div{padding:1rem}#layers li{margin-top:.3rem;margin-bottom:.3rem}#layers li button{text-align:left;overflow-wrap:anywhere;text-wrap-style:balance}#file-selector{display:grid}#file,#file-label{grid-area:1/1}#file{opacity:0;z-index:2;cursor:pointer}#file::file-selector-button{cursor:pointer}#loading-icon{flex-grow:1;align-self:center;width:3rem}.title{margin-top:0}.filter-buttons{flex-direction:row;margin:2px;display:flex}.filter-buttons span{flex-grow:1;align-self:center}.clearable-input-field{width:90%;color:var(--text-color);background:0 0;border:0;outline:none;flex:auto;padding:0;font-size:1rem}.filter-icon,.delete-icon{width:16px;color:var(--text-color);padding:0 .5rem}.delete-button{cursor:pointer;background-color:#0000;border:none;padding:0}
19
+ #panel{background:var(--bkg-color);height:100%;color:var(--text-color);flex-direction:column;padding:0 1rem;display:flex}#content{flex-direction:column;flex-grow:1;margin:0;display:flex}header{background-color:var(--bkg-color);margin:0;position:sticky;top:-5px}h4{margin-bottom:.5rem}ul{margin-top:0;margin-left:0;padding-left:2rem;line-height:1.3rem;list-style-type:disc}.clearable-input{background-color:var(--bkg-color);border:1px solid #cfd6dd;border-radius:4px;flex-direction:row;margin-bottom:.1rem;display:flex}.scan-button{float:right}.layers{border-top:1px solid #ccc;flex-direction:column;flex-grow:1;margin-top:1rem;padding-top:.5rem;display:flex;overflow:hidden}ul.results{border:none;margin-bottom:.5rem;padding:0;list-style:none;overflow-y:auto}.result{height:1.6rem;display:flex}.result>button>span{text-align:left;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}#panel>div{padding:1rem}#layers li{margin-top:.3rem;margin-bottom:.3rem}#layers li button{text-align:left;overflow-wrap:anywhere;text-wrap-style:balance}#loading-icon{flex-grow:1;align-self:center;width:3rem}.title{margin-top:0}.filter-buttons{flex-direction:row;margin:2px;display:flex}.filter-buttons span{flex-grow:1;align-self:center}.clearable-input-field{width:90%;color:var(--text-color);background:0 0;border:0;outline:none;flex:auto;padding:0;font-size:1rem}.filter-icon,.delete-icon{width:16px;color:var(--text-color);padding:0 .5rem}.delete-button{cursor:pointer;background-color:#0000;border:none;padding:0}#file-selector{flex-direction:column;display:flex;position:relative}#file{color:var(--text-color);cursor:pointer}#file::file-selector-button{background-color:var(--bkg-color);color:var(--text-color);border:var(--app-standard-border);min-height:calc(var(--app-standard-height) / 1.5);font:inherit;cursor:pointer;border-radius:.25em;outline:0;margin:0 .5rem 0 0;padding:0 .5rem}#file::file-selector-button:hover{background-color:var(--bkg-color-grad1)}.file-loaded-message{box-sizing:border-box;background-color:var(--success-color);border-radius:.25em;width:100%;margin:.5rem 0 0;padding:.5em;position:absolute;top:100%;left:0}
20
20
  </style>
21
21
  <style>${this.customStyle}</style>
22
- <div id="panel"><header class="gg-tabs"><button class="${this.selectedTab === 'wms_wmts' ? 'gg-tab active' : 'gg-tab'}" onclick="${() => this.setSelectedTab('wms_wmts')}" i18n="wms_wmts"></button> <button class="${this.selectedTab === 'file' ? 'gg-tab active' : 'gg-tab'}" onclick="${() => this.setSelectedTab('file')}" i18n="local file"></button></header><section class="${(this.selectedTab === 'wms_wmts') ? 'group' : 'hidden'}"><h4 class="title" i18n="Suggestions" ?hidden="${!this.hasPredefinedWmsWmtsSources}">Suggestions</h4><ul class="suggestions" ?hidden="${!this.hasPredefinedWmsWmtsSources}">${this.predefinedWmsWmtsSources.map(source => uHtmlFor(source, source.label) `<li><a href="javascript:void(0)" onclick="${() => this.scanSource(source.url, source.type)}">${source.label}</a></li>`)}</ul><h4 class="title" i18n="Custom service URL">Custom service URL</h4><div class="clearable-input"><input id="url" class="gg-input clearable-input-field" oninput="${() => this.typeUrl()}"><br><button id="clear-url" class="delete-button hidden" onclick="${() => this.clearUrl()}"><img class="delete-icon" alt="delete-button-icon" tip="Reset filter" i18n="Reset filter" src="icons/trash.svg"></button></div><button i18n="Scan Source" class="gg-button scan-button" onclick="${() => this.scanSource()}">Scan Source</button></section><section class="${(this.selectedTab === 'file') ? 'group' : 'hidden'}"><h4 class="title" i18n="Choose local file">Choose local file</h4><div id="file-selector"><label for="file" id="file-label"><button class="gg-button" i18n="Select File"></button> <span>${this.fileDescription}</span></label> <input type="file" accept=".gpx,.kml,.geojson,.json" id="file" onchange="${() => this.render()}"><br></div><button id="load" i18n="Load File" class="gg-button" onclick="${() => this.loadFile()}"></button></section><section class="${(this.selectedTab === 'wms_wmts' && this.externalLayers.length > 0) || this.loading ? 'layers' : 'hidden'}"><h4 class="title" i18n="Layers (from Capabilities)">Layers (from Capabilities)</h4><div class="clearable-input"><img class="filter-icon" alt="filter-button-icon" src="icons/filter.svg"> <input id="layer-search-field" class="gg-input clearable-input-field" placeholder="${this.context.i18nManager.getTranslation('Filter external layers...')}" i18n="Filter external layers..." autocomplete="off" autocorrect="off" oninput="${(e) => this.filterLayers(e.target.value)}"> <button class="${this.isFiltered ? 'delete-button' : 'hidden'}" onclick="${() => this.clearFilter()}"><img class="delete-icon" alt="delete-button-icon" tip="Reset filter" i18n="Reset filter" src="icons/trash.svg"></button></div><div class="filter-buttons"><span>${Object.values(this.externalLayers).filter(layer => layer.isSelected).length.toString()} ${this.context.i18nManager.getTranslation('layers selected')} </span><button class="gg-button" tip="Select all filtered layers" onclick="${() => this.selectVisible()}">+</button> <button class="gg-button" tip="Unselect all filtered layers" onclick="${() => this.deselectVisible()}">-</button> <button class="gg-button" tip="Select none" onclick="${() => this.deselectAll()}">--</button></div><img id="loading-icon" alt="loading-icon" src="icons/loading.svg" class="${this.loading ? 'gg-spin' : 'hidden'}"><ul class="results">${this.filteredLayers.map(layer => uHtmlFor(layer, layer.treeItemId) `<li class="result"><button class="${layer.isSelected ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" tip="Activate / Deactivate" onclick="${() => this.selectLayer(layer)}"><img class="${layer.isSelected ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="icons/checked-full.svg"> <img class="${!layer.isSelected ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="icons/checked-no.svg"></button> <button class="${layer.isSelected ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" onclick="${() => this.selectLayer(layer)}"><span i18n="${layer.name}" class="gg-tree-label">${layer.name}</span></button></li>`)}</ul><button id="add" tip="Add selected" i18n="Add selected" class="gg-button" onclick="${() => this.addSelectedLayers()}">Add selected</button></section></div>
22
+ <div id="panel"><header class="gg-tabs"><button class="${this.selectedTab === 'wms_wmts' ? 'gg-tab active' : 'gg-tab'}" onclick="${() => this.setSelectedTab('wms_wmts')}" i18n="wms_wmts"></button> <button class="${this.selectedTab === 'file' ? 'gg-tab active' : 'gg-tab'}" onclick="${() => this.setSelectedTab('file')}" i18n="local file"></button></header><section class="${(this.selectedTab === 'wms_wmts') ? 'group' : 'hidden'}"><h4 class="title" i18n="Suggestions" ?hidden="${!this.hasPredefinedWmsWmtsSources}">Suggestions</h4><ul class="suggestions" ?hidden="${!this.hasPredefinedWmsWmtsSources}">${this.predefinedWmsWmtsSources.map(source => uHtmlFor(source, source.label) `<li><a href="javascript:void(0)" onclick="${() => this.scanSource(source.url, source.type)}">${source.label}</a></li>`)}</ul><h4 class="title" i18n="Custom service URL">Custom service URL</h4><div class="clearable-input"><input id="url" class="gg-input clearable-input-field" oninput="${() => this.typeUrl()}"><br><button id="clear-url" class="delete-button hidden" onclick="${() => this.clearUrl()}"><img class="delete-icon" alt="delete-button-icon" tip="Reset filter" i18n="Reset filter" src="icons/trash.svg"></button></div><button i18n="Scan Source" class="gg-button scan-button" onclick="${() => this.scanSource()}">Scan Source</button></section><section class="${(this.selectedTab === 'file') ? 'group' : 'hidden'}"><div id="file-selector"><label for="file" class="gg-label" i18n="Choose local file">Choose local file</label> <input type="file" accept=".gpx,.kml,.geojson,.json" id="file" onchange="${() => this.loadFile()}"><p class="${this.fileLoaded ? 'file-loaded-message' : 'hidden'}" i18n="File loaded in the layer tree">File loaded in the layer tree</div></section><section class="${(this.selectedTab === 'wms_wmts' && this.externalLayers.length > 0) || this.loading ? 'layers' : 'hidden'}"><h4 class="title" i18n="Layers (from Capabilities)">Layers (from Capabilities)</h4><div class="clearable-input"><img class="filter-icon" alt="filter-button-icon" src="icons/filter.svg"> <input id="layer-search-field" class="gg-input clearable-input-field" placeholder="${this.context.i18nManager.getTranslation('Filter external layers...')}" i18n="Filter external layers..." autocomplete="off" autocorrect="off" oninput="${(e) => this.filterLayers(e.target.value)}"> <button class="${this.isFiltered ? 'delete-button' : 'hidden'}" onclick="${() => this.clearFilter()}"><img class="delete-icon" alt="delete-button-icon" tip="Reset filter" i18n="Reset filter" src="icons/trash.svg"></button></div><div class="filter-buttons"><span>${Object.values(this.externalLayers).filter(layer => layer.isSelected).length.toString()} ${this.context.i18nManager.getTranslation('layers selected')} </span><button class="gg-button" tip="Select all filtered layers" onclick="${() => this.selectVisible()}">+</button> <button class="gg-button" tip="Unselect all filtered layers" onclick="${() => this.deselectVisible()}">-</button> <button class="gg-button" tip="Select none" onclick="${() => this.deselectAll()}">--</button></div><img id="loading-icon" alt="loading-icon" src="icons/loading.svg" class="${this.loading ? 'gg-spin' : 'hidden'}"><ul class="results">${this.filteredLayers.map(layer => uHtmlFor(layer, layer.treeItemId) `<li class="result"><button class="${layer.isSelected ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" tip="Activate / Deactivate" onclick="${() => this.selectLayer(layer)}"><img class="${layer.isSelected ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="icons/checked-full.svg"> <img class="${!layer.isSelected ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="icons/checked-no.svg"></button> <button class="${layer.isSelected ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" onclick="${() => this.selectLayer(layer)}"><span i18n="${layer.name}" class="gg-tree-label">${layer.name}</span></button></li>`)}</ul><button id="add" tip="Add selected" i18n="Add selected" class="gg-button" onclick="${() => this.addSelectedLayers()}">Add selected</button></section></div>
23
23
  ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
24
24
  };
25
25
  isPanelVisible = false;
26
26
  panelTitle = 'ext-layer-panel';
27
27
  panelTogglePath = 'interface.extLayerPanelVisible';
28
28
  loading = false;
29
+ fileLoaded = false;
29
30
  predefinedSources = [];
30
31
  get predefinedWmsWmtsSources() {
31
32
  return this.predefinedSources.filter((source) => source.type === 'WMS' || source.type === 'WMTS');
@@ -211,18 +212,15 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
211
212
  this.state.layers.layersList.push(theme);
212
213
  });
213
214
  }
214
- get fileDescription() {
215
- const selectedFiles = this.getById('file')?.files;
216
- if (!selectedFiles || selectedFiles.length == 0) {
217
- return '';
218
- }
219
- return `${selectedFiles[0].name} (${selectedFiles[0].size}b)`;
220
- }
221
215
  async loadFile() {
222
216
  const selectedFiles = this.getById('file').files;
223
217
  if (selectedFiles && selectedFiles.length > 0) {
224
218
  const selectedFile = selectedFiles[0];
219
+ this.fileLoaded = false;
220
+ this.refreshRender();
225
221
  await this.context.localFileManager.loadLocalFile(selectedFile);
222
+ this.fileLoaded = true;
223
+ this.refreshRender();
226
224
  }
227
225
  }
228
226
  connectedCallback() {
@@ -92,26 +92,6 @@ ul.results {
92
92
  text-wrap-style: balance;
93
93
  }
94
94
 
95
- #file-selector {
96
- display: grid;
97
- }
98
-
99
- #file,
100
- #file-label {
101
- grid-row: 1;
102
- grid-column: 1;
103
- }
104
-
105
- #file {
106
- opacity: 0;
107
- z-index: 2;
108
- cursor: pointer;
109
- }
110
-
111
- #file::file-selector-button {
112
- cursor: pointer;
113
- }
114
-
115
95
  #loading-icon {
116
96
  width: 3rem;
117
97
  flex-grow: 1;
@@ -157,3 +137,47 @@ ul.results {
157
137
  background-color: transparent;
158
138
  cursor: pointer;
159
139
  }
140
+
141
+ #file-selector {
142
+ display: flex;
143
+ flex-direction: column;
144
+ /* Positioned so the message below can be taken out of flow without
145
+ growing #panel, which is sized with width: fit-content (see .panel-right) */
146
+ position: relative;
147
+ }
148
+
149
+ #file {
150
+ color: var(--text-color);
151
+ cursor: pointer;
152
+ }
153
+
154
+ #file::file-selector-button {
155
+ background-color: var(--bkg-color);
156
+ color: var(--text-color);
157
+ outline: 0;
158
+ border-radius: 0.25em;
159
+ border: var(--app-standard-border);
160
+ padding: 0 0.5rem;
161
+ margin: 0 0.5rem 0 0;
162
+ min-height: calc(var(--app-standard-height) / 1.5);
163
+ font: inherit;
164
+ cursor: pointer;
165
+ }
166
+
167
+ #file::file-selector-button:hover {
168
+ background-color: var(--bkg-color-grad1);
169
+ }
170
+
171
+ .file-loaded-message {
172
+ /* Absolutely positioned so its width doesn't feed into the ancestor
173
+ panel's fit-content sizing and grow/shrink the whole right panel */
174
+ position: absolute;
175
+ top: 100%;
176
+ left: 0;
177
+ width: 100%;
178
+ box-sizing: border-box;
179
+ padding: 0.5em;
180
+ border-radius: 0.25em;
181
+ background-color: var(--success-color);
182
+ margin: 0.5rem 0 0;
183
+ }
@@ -26,6 +26,7 @@ declare class TreeViewItemComponent extends TreeViewElement {
26
26
  zoomToVisibleResolution(): void;
27
27
  isVisibleInCurrentResolution(): boolean;
28
28
  zoomToFullExtent(): void;
29
+ convertToDrawing(): void;
29
30
  deleteLayer(): void;
30
31
  protected connectedCallback(): void;
31
32
  }
@@ -19,7 +19,7 @@ header{border-top:1px solid #ddd;align-items:center;height:2rem;display:flex}.to
19
19
  .hidden{display:none!important}.gg-rotate90{transform:rotate(90deg)}.gg-rotate180{transform:rotate(180deg)}.gg-rotate270{transform:rotate(270deg)}img{filter:var(--svg-filter)}img.legend-image{filter:var(--svg-map-filter);background:var(--svg-legend-bkg)}div{scrollbar-width:thin}a,a:visited{color:var(--link-color)}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes spin-wait{0%{transform:rotate(0)}7%{transform:rotate(360deg)}to{transform:rotate(360deg)}}.gg-spin{animation-name:spin;animation-duration:2s;animation-timing-function:linear;animation-iteration-count:infinite}.gg-spin-wait{animation-name:spin-wait;animation-duration:10s;animation-timing-function:linear;animation-iteration-count:infinite}::-webkit-scrollbar{width:5px}::-webkit-scrollbar-thumb{background:#999}button,input,select,textarea{font:inherit}.gg-button,.gg-select,.gg-input,.gg-textarea{background-color:var(--bkg-color);color:var(--text-color);border:var(--app-standard-border);box-sizing:border-box;cursor:pointer;border-radius:3px;outline:0;margin:0;padding:0 0 0 .5rem;display:inline-block}.gg-label{background-color:var(--bkg-color);color:var(--text-color);border:none;align-items:center;margin:0;padding:0;display:flex}.gg-button,.gg-select,.gg-input,.gg-label{min-height:calc(var(--app-standard-height) / 1.5)}.gg-textarea{max-height:initial;resize:vertical;height:6rem;padding:.5rem;line-height:1.3rem}.gg-input{cursor:text}.gg-checkbox{accent-color:var(--text-color);width:1.2rem}.gg-range{accent-color:var(--text-color)}.gg-button{padding:0 .5rem}.gg-button.active{border:solid 1px var(--text-color-grad1);background-color:var(--bkg-color-grad2)}.gg-button:disabled{background-color:var(--bkg-color-grad1);color:var(--text-color-grad2);cursor:not-allowed;border:none}.gg-button:disabled img{filter:opacity(.6)}.gg-input:disabled,.gg-select:disabled,.gg-textarea:disabled{background-color:var(--bkg-color-grad1);color:var(--text-color-grad2);cursor:not-allowed}.gg-button>img{vertical-align:middle}.gg-icon-button{color:var(--text-color);cursor:pointer;background-color:#0000;border:none;flex-direction:column;justify-content:center;align-items:center;padding:0;display:flex}.gg-icon{justify-content:center;align-items:center;display:flex}.gg-big,.gg-big-withtext{min-width:var(--app-standard-height);min-height:var(--app-standard-height);max-height:var(--app-standard-height)}.gg-big img,.gg-big-withtext img{width:calc(var(--app-standard-height) - 1.5rem);margin:0}.gg-big-withtext span{font-variant:small-caps;padding:0 1rem;font-size:.9rem}.gg-medium,.gg-medium-withtext{min-width:calc(var(--app-standard-height) / 1.2);min-height:calc(var(--app-standard-height) / 1.2);max-height:calc(var(--app-standard-height) / 1.2);flex-direction:row}.gg-medium img{width:calc(var(--app-standard-height) / 2.4);margin:0}.gg-medium-withtext img{width:calc(var(--app-standard-height) / 2.4);margin-left:.5rem}.gg-medium-withtext span{padding:0 1rem 0 .5rem;font-size:.9rem}.gg-small,.gg-small-withtext{min-width:calc(var(--app-standard-height) / 2);min-height:calc(var(--app-standard-height) / 2);max-height:calc(var(--app-standard-height) / 2);flex-direction:row}.gg-small img{width:calc(var(--app-standard-height) / 3);margin:0}.gg-small-withtext img{width:calc(var(--app-standard-height) / 3);margin-left:.5rem}.gg-small-withtext span{padding:0 .5rem 0 .3rem;font-size:.9rem}.gg-button:hover:not(:disabled),.gg-select:hover:not(:disabled),.gg-input:hover:not(:disabled),.gg-textarea:hover:not(:disabled),.gg-icon-button:hover:not(:disabled){background-color:var(--bkg-color-grad1)}.gg-opacity{opacity:.5}.gg-opacity:hover{opacity:1;background-color:#0000}.gg-tabs{cursor:pointer;grid-auto-flow:column;padding-bottom:1rem;font-size:1rem;display:grid}.gg-tab{border:none;border-bottom:var(--app-standard-border);cursor:pointer;color:var(--text-color);background:0 0;padding:.5rem}.gg-tab.active{border-bottom:solid 1px var(--text-color)}.girafe-button-big,.girafe-button-large,.girafe-button-small,.girafe-button-tiny{color:var(--text-color);background-color:#0000;border:none;flex-direction:column;display:flex}.girafe-button-big:hover,.girafe-button-large:hover,.girafe-button-small:hover,.girafe-button-tiny:hover{background-color:var(--bkg-color-grad1);cursor:pointer}.girafe-button-big.dark,.girafe-button-large.dark,.girafe-button-small.dark,.girafe-button-tiny.dark{background-color:var(--bkg-color);filter:invert()}.girafe-button-big{width:var(--app-standard-height);height:var(--app-standard-height);align-items:center;padding:1rem}.girafe-button-big img{overflow:hidden}.girafe-button-large{flex-direction:row}.girafe-button-large img{height:2rem;margin:.3rem}.girafe-button-large span{height:2rem;margin:.3rem;line-height:2rem}.girafe-button-small{min-width:calc(var(--app-standard-height) / 2);height:calc(var(--app-standard-height) / 2);align-items:center;padding:.5rem}.girafe-button-small img{overflow:hidden}.girafe-button-small span{text-align:left;text-overflow:ellipsis;width:100%;overflow:hidden}.girafe-button-tiny{align-items:center;width:1rem;height:1rem;padding:0}.girafe-button-tiny img{overflow:hidden}.girafe-onboarding-theme{background-color:var(--bkg-color)!important;color:var(--text-color)!important}.girafe-onboarding-theme button{background-color:var(--bkg-color)!important;color:var(--text-color)!important;text-shadow:none!important}.girafe-onboarding-theme button.driver-popover-close-btn{z-index:10000}.tippy-box{background-color:var(--bkg-color)!important;border:var(--app-standard-border)!important}.tippy-content{color:var(--text-color)!important}.tippy-arrow{color:var(--bkg-color)!important}.tippy-box[data-theme~=error]{background-color:var(--error-color);white-space:pre-line}div.tippy-box[data-theme=image-popup]{border:var(--app-standard-border)!important;& div.tippy-content{background-color:var(--bkg-color)!important;& img{max-width:20rem;max-height:20rem}}& div.tippy-arrow{color:var(--bkg-color)!important}}
20
20
  </style>
21
21
  <style>${this.customStyle}</style>
22
- <link rel="stylesheet" href="lib/tippy.js/tippy.css"><link rel="stylesheet" href="lib/tippy.js/backdrop.css"><link rel="stylesheet" href="lib/tippy.js/border.css"><link rel="stylesheet" href="lib/tippy.js/svg-arrow.css"><div id="container"><header><div class="gg-spacer gg-small"><img class="${this.layer.restricted ? (this.layer.active ? 'gg-small gg-lock gg-selected' : 'gg-small gg-lock gg-opacity') : 'hidden'}" alt="Protected layer" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8bWFzayBpZD0ibWFzazBfMzdfNDUxNyIgc3R5bGU9Im1hc2stdHlwZTphbHBoYSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij4KICA8cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIGZpbGw9IiNEOUQ5RDkiLz4KICA8L21hc2s+CiAgPGcgbWFzaz0idXJsKCNtYXNrMF8zN180NTE3KSI+CiAgPHBhdGggZD0iTTYgMjJDNS40NSAyMiA0Ljk3OTE3IDIxLjgwNDIgNC41ODc1IDIxLjQxMjVDNC4xOTU4MyAyMS4wMjA4IDQgMjAuNTUgNCAyMFYxMEM0IDkuNDUgNC4xOTU4MyA4Ljk3OTE3IDQuNTg3NSA4LjU4NzVDNC45NzkxNyA4LjE5NTgzIDUuNDUgOCA2IDhIN1Y2QzcgNC42MTY2NyA3LjQ4NzUgMy40Mzc1IDguNDYyNSAyLjQ2MjVDOS40Mzc1IDEuNDg3NSAxMC42MTY3IDEgMTIgMUMxMy4zODMzIDEgMTQuNTYyNSAxLjQ4NzUgMTUuNTM3NSAyLjQ2MjVDMTYuNTEyNSAzLjQzNzUgMTcgNC42MTY2NyAxNyA2VjhIMThDMTguNTUgOCAxOS4wMjA4IDguMTk1ODMgMTkuNDEyNSA4LjU4NzVDMTkuODA0MiA4Ljk3OTE3IDIwIDkuNDUgMjAgMTBWMjBDMjAgMjAuNTUgMTkuODA0MiAyMS4wMjA4IDE5LjQxMjUgMjEuNDEyNUMxOS4wMjA4IDIxLjgwNDIgMTguNTUgMjIgMTggMjJINlpNNiAyMEgxOFYxMEg2VjIwWk0xMiAxN0MxMi41NSAxNyAxMy4wMjA4IDE2LjgwNDIgMTMuNDEyNSAxNi40MTI1QzEzLjgwNDIgMTYuMDIwOCAxNCAxNS41NSAxNCAxNUMxNCAxNC40NSAxMy44MDQyIDEzLjk3OTIgMTMuNDEyNSAxMy41ODc1QzEzLjAyMDggMTMuMTk1OCAxMi41NSAxMyAxMiAxM0MxMS40NSAxMyAxMC45NzkyIDEzLjE5NTggMTAuNTg3NSAxMy41ODc1QzEwLjE5NTggMTMuOTc5MiAxMCAxNC40NSAxMCAxNUMxMCAxNS41NSAxMC4xOTU4IDE2LjAyMDggMTAuNTg3NSAxNi40MTI1QzEwLjk3OTIgMTYuODA0MiAxMS40NSAxNyAxMiAxN1pNOSA4SDE1VjZDMTUgNS4xNjY2NyAxNC43MDgzIDQuNDU4MzMgMTQuMTI1IDMuODc1QzEzLjU0MTcgMy4yOTE2NyAxMi44MzMzIDMgMTIgM0MxMS4xNjY3IDMgMTAuNDU4MyAzLjI5MTY3IDkuODc1IDMuODc1QzkuMjkxNjcgNC40NTgzMyA5IDUuMTY2NjcgOSA2VjhaIiBmaWxsPSIjMUMxQjFGIi8+CiAgPC9nPgo8L3N2Zz4KICA="></div><button class="${this.layer.active ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" tip="Activate / Deactivate" onclick="${() => this.toggle()}"><img class="${this.layer.active ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj48IS0tIUZvbnQgQXdlc29tZSBGcmVlIDYuNi4wIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlL2ZyZWUgQ29weXJpZ2h0IDIwMjQgRm9udGljb25zLCBJbmMuLS0+PHBhdGggZD0iTTY0IDMyQzI4LjcgMzIgMCA2MC43IDAgOTZMMCA0MTZjMCAzNS4zIDI4LjcgNjQgNjQgNjRsMzIwIDBjMzUuMyAwIDY0LTI4LjcgNjQtNjRsMC0zMjBjMC0zNS4zLTI4LjctNjQtNjQtNjRMNjQgMzJ6TTMzNyAyMDlMMjA5IDMzN2MtOS40IDkuNC0yNC42IDkuNC0zMy45IDBsLTY0LTY0Yy05LjQtOS40LTkuNC0yNC42IDAtMzMuOXMyNC42LTkuNCAzMy45IDBsNDcgNDdMMzAzIDE3NWM5LjQtOS40IDI0LjYtOS40IDMzLjkgMHM5LjQgMjQuNiAwIDMzLjl6Ii8+PC9zdmc+"> <img class="${!this.layer.active ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj48IS0tIUZvbnQgQXdlc29tZSBGcmVlIDYuNi4wIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlL2ZyZWUgQ29weXJpZ2h0IDIwMjQgRm9udGljb25zLCBJbmMuLS0+PHBhdGggZD0iTTM4NCA4MGM4LjggMCAxNiA3LjIgMTYgMTZsMCAzMjBjMCA4LjgtNy4yIDE2LTE2IDE2TDY0IDQzMmMtOC44IDAtMTYtNy4yLTE2LTE2TDQ4IDk2YzAtOC44IDcuMi0xNiAxNi0xNmwzMjAgMHpNNjQgMzJDMjguNyAzMiAwIDYwLjcgMCA5NkwwIDQxNmMwIDM1LjMgMjguNyA2NCA2NCA2NGwzMjAgMGMzNS4zIDAgNjQtMjguNyA2NC02NGwwLTMyMGMwLTM1LjMtMjguNy02NC02NC02NEw2NCAzMnoiLz48L3N2Zz4="></button><div class="${this.layer.hasError ? 'gg-icon gg-small gg-opacity' : 'hidden'}" tip="${this.layer.errorMessage ? this.layer.errorMessage : 'Error on layer'}"><img alt="Error on layer" src="icons/error.svg"></div><div class="${this.isVisibleInCurrentResolution() ? 'label-container' : 'label-container label-out-resolution'}"><button class="${this.layer.active ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" onclick="${() => this.toggle()}"><span i18n="${this.layer.name}" class="gg-tree-label">${this.layer.name}</span></button> <button class="${this.layer.active && this.layer instanceof LayerWms && !this.isVisibleInCurrentResolution() ? 'gg-icon-button gg-small resolution-tool' : 'hidden'}" tip="Zoom to visible resolution" onclick="${() => this.zoomToVisibleResolution()}"><img alt="Zoom to visible resolution" src="icons/zoom.svg"></button> <img class="${this.layer.active && this.iconUrl ? 'legend-icon' : 'hidden'}" alt="${'Icon for ' + this.layer.name}" src="${this.iconUrl}" loading="lazy" crossorigin="${this.getCrossOrigin(this.iconUrl)}"></div><button class="${this.layer.active && this.layer instanceof LayerLocalFile ? 'gg-icon-button gg-small gg-opacity tool' : 'hidden'}" tip="Zoom to full extent" onclick="${() => this.zoomToFullExtent()}"><img alt="Zoom to full extent" src="icons/zoom.svg"></button> <button class="${this.getButtonClass('swipedLeft')}" tip="Swipe layer to the left" onclick="${() => this.layer.swiped = (this.layer.swiped === 'left') ? 'no' : 'left'}"><img alt="Swipe layer to the left" src="icons/swipe-left.svg"></button> <button class="${this.getButtonClass('swipedRight')}" tip="Swipe layer to the right" onclick="${() => this.layer.swiped = (this.layer.swiped === 'right') ? 'no' : 'right'}"><img alt="Swipe layer to the right" src="icons/swipe-right.svg"></button> <button id="opacity" class="${this.getButtonClass('opacity')}" tip="Control opacity"><img alt="Control opacity" src="icons/opacity.svg"></button> <button id="filter" class="${this.getButtonClass('filter')}" tip="Filter layer"><img alt="Filter layer" src="icons/filter.svg"></button> <button id="advanced-filter" class="${this.getButtonClass('advanced-filter')}" tip="Open advanced filter tool" onclick="${() => this.state.interface.advancedFilterPanelVisible = !this.state.interface.advancedFilterPanelVisible}"><img alt="Advanced filter" src="icons/advanced-filter.svg"></button> <button id="timeRestriction" class="${this.getButtonClass('timeRestriction')}" tip="Define time restriction on layer"><img alt="Define time restriction on layer" src="icons/clock.svg"></button> <button class="${this.getButtonClass('snappable')}" onclick="${() => this.toggleSnap()}" tip="Snap to layer features"><img alt="Enable snapping" src="icons/snapping.svg"></button> <button class="${this.hasLegend ? (this.layer.isLegendExpanded ? 'gg-icon-button gg-small tool' : 'gg-icon-button gg-small gg-opacity tool') : 'hidden'}" tip="Toggle legend" onclick="${() => this.toggleLegend()}"><img alt="Toggle legend" src="icons/legend.svg" loading="lazy"></button> <button class="${this.layer.hasMetadata ? 'metadata gg-icon-button gg-small gg-opacity tool' : 'hidden'}" onclick="${() => this.showMetadata()}"><img alt="Metadata" src="icons/info.svg"></button> <button id="drag-button" class="${this.getButtonClass('draggable')}" tip="Move"><img alt="Move layer" src="icons/drag.svg"></button> <button class="${this.getButtonClass('removable')}" tip="Remove layer" onclick="${() => this.deleteLayer()}"><img alt="Remove layer" src="icons/trash.svg"></button></header>${Object.keys(this.legendUrls).map((l) => uHtml `<div class="${this.isLegendExpanded ? 'legend' : 'hidden'}"><div class="gg-spacer gg-small"></div><img alt="${'Legend for ' + l}" src="${this.legendUrls[l]}" loading="lazy" crossorigin="${this.getCrossOrigin(this.legendUrls[l])}"></div>`)}<div class="${this.layer instanceof LayerLocalFile && this.isLegendExpanded ? 'legend-file' : 'legend-file hidden'}"><div class="gg-spacer gg-small"></div><div class="gg-spacer gg-small"></div><div><span i18n="File from">File from</span> <span>${this.layer.lastModifiedDate}</span> <span>[</span> <span>${this.layer._features?.length}</span> <span i18n="features">features</span> <span>]</span></div></div></div>
22
+ <link rel="stylesheet" href="lib/tippy.js/tippy.css"><link rel="stylesheet" href="lib/tippy.js/backdrop.css"><link rel="stylesheet" href="lib/tippy.js/border.css"><link rel="stylesheet" href="lib/tippy.js/svg-arrow.css"><div id="container"><header><div class="gg-spacer gg-small"><img class="${this.layer.restricted ? (this.layer.active ? 'gg-small gg-lock gg-selected' : 'gg-small gg-lock gg-opacity') : 'hidden'}" alt="Protected layer" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8bWFzayBpZD0ibWFzazBfMzdfNDUxNyIgc3R5bGU9Im1hc2stdHlwZTphbHBoYSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij4KICA8cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIGZpbGw9IiNEOUQ5RDkiLz4KICA8L21hc2s+CiAgPGcgbWFzaz0idXJsKCNtYXNrMF8zN180NTE3KSI+CiAgPHBhdGggZD0iTTYgMjJDNS40NSAyMiA0Ljk3OTE3IDIxLjgwNDIgNC41ODc1IDIxLjQxMjVDNC4xOTU4MyAyMS4wMjA4IDQgMjAuNTUgNCAyMFYxMEM0IDkuNDUgNC4xOTU4MyA4Ljk3OTE3IDQuNTg3NSA4LjU4NzVDNC45NzkxNyA4LjE5NTgzIDUuNDUgOCA2IDhIN1Y2QzcgNC42MTY2NyA3LjQ4NzUgMy40Mzc1IDguNDYyNSAyLjQ2MjVDOS40Mzc1IDEuNDg3NSAxMC42MTY3IDEgMTIgMUMxMy4zODMzIDEgMTQuNTYyNSAxLjQ4NzUgMTUuNTM3NSAyLjQ2MjVDMTYuNTEyNSAzLjQzNzUgMTcgNC42MTY2NyAxNyA2VjhIMThDMTguNTUgOCAxOS4wMjA4IDguMTk1ODMgMTkuNDEyNSA4LjU4NzVDMTkuODA0MiA4Ljk3OTE3IDIwIDkuNDUgMjAgMTBWMjBDMjAgMjAuNTUgMTkuODA0MiAyMS4wMjA4IDE5LjQxMjUgMjEuNDEyNUMxOS4wMjA4IDIxLjgwNDIgMTguNTUgMjIgMTggMjJINlpNNiAyMEgxOFYxMEg2VjIwWk0xMiAxN0MxMi41NSAxNyAxMy4wMjA4IDE2LjgwNDIgMTMuNDEyNSAxNi40MTI1QzEzLjgwNDIgMTYuMDIwOCAxNCAxNS41NSAxNCAxNUMxNCAxNC40NSAxMy44MDQyIDEzLjk3OTIgMTMuNDEyNSAxMy41ODc1QzEzLjAyMDggMTMuMTk1OCAxMi41NSAxMyAxMiAxM0MxMS40NSAxMyAxMC45NzkyIDEzLjE5NTggMTAuNTg3NSAxMy41ODc1QzEwLjE5NTggMTMuOTc5MiAxMCAxNC40NSAxMCAxNUMxMCAxNS41NSAxMC4xOTU4IDE2LjAyMDggMTAuNTg3NSAxNi40MTI1QzEwLjk3OTIgMTYuODA0MiAxMS40NSAxNyAxMiAxN1pNOSA4SDE1VjZDMTUgNS4xNjY2NyAxNC43MDgzIDQuNDU4MzMgMTQuMTI1IDMuODc1QzEzLjU0MTcgMy4yOTE2NyAxMi44MzMzIDMgMTIgM0MxMS4xNjY3IDMgMTAuNDU4MyAzLjI5MTY3IDkuODc1IDMuODc1QzkuMjkxNjcgNC40NTgzMyA5IDUuMTY2NjcgOSA2VjhaIiBmaWxsPSIjMUMxQjFGIi8+CiAgPC9nPgo8L3N2Zz4KICA="></div><button class="${this.layer.active ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" tip="Activate / Deactivate" onclick="${() => this.toggle()}"><img class="${this.layer.active ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj48IS0tIUZvbnQgQXdlc29tZSBGcmVlIDYuNi4wIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlL2ZyZWUgQ29weXJpZ2h0IDIwMjQgRm9udGljb25zLCBJbmMuLS0+PHBhdGggZD0iTTY0IDMyQzI4LjcgMzIgMCA2MC43IDAgOTZMMCA0MTZjMCAzNS4zIDI4LjcgNjQgNjQgNjRsMzIwIDBjMzUuMyAwIDY0LTI4LjcgNjQtNjRsMC0zMjBjMC0zNS4zLTI4LjctNjQtNjQtNjRMNjQgMzJ6TTMzNyAyMDlMMjA5IDMzN2MtOS40IDkuNC0yNC42IDkuNC0zMy45IDBsLTY0LTY0Yy05LjQtOS40LTkuNC0yNC42IDAtMzMuOXMyNC42LTkuNCAzMy45IDBsNDcgNDdMMzAzIDE3NWM5LjQtOS40IDI0LjYtOS40IDMzLjkgMHM5LjQgMjQuNiAwIDMzLjl6Ii8+PC9zdmc+"> <img class="${!this.layer.active ? 'gg-checkbox' : 'hidden'}" alt="Expand/Collapse button" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj48IS0tIUZvbnQgQXdlc29tZSBGcmVlIDYuNi4wIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlL2ZyZWUgQ29weXJpZ2h0IDIwMjQgRm9udGljb25zLCBJbmMuLS0+PHBhdGggZD0iTTM4NCA4MGM4LjggMCAxNiA3LjIgMTYgMTZsMCAzMjBjMCA4LjgtNy4yIDE2LTE2IDE2TDY0IDQzMmMtOC44IDAtMTYtNy4yLTE2LTE2TDQ4IDk2YzAtOC44IDcuMi0xNiAxNi0xNmwzMjAgMHpNNjQgMzJDMjguNyAzMiAwIDYwLjcgMCA5NkwwIDQxNmMwIDM1LjMgMjguNyA2NCA2NCA2NGwzMjAgMGMzNS4zIDAgNjQtMjguNyA2NC02NGwwLTMyMGMwLTM1LjMtMjguNy02NC02NC02NEw2NCAzMnoiLz48L3N2Zz4="></button><div class="${this.layer.hasError ? 'gg-icon gg-small gg-opacity' : 'hidden'}" tip="${this.layer.errorMessage ? this.layer.errorMessage : 'Error on layer'}"><img alt="Error on layer" src="icons/error.svg"></div><div class="${this.isVisibleInCurrentResolution() ? 'label-container' : 'label-container label-out-resolution'}"><button class="${this.layer.active ? 'gg-icon-button gg-small gg-selected' : 'gg-icon-button gg-small gg-opacity'}" onclick="${() => this.toggle()}"><span i18n="${this.layer.name}" class="gg-tree-label">${this.layer.name}</span></button> <button class="${this.layer.active && this.layer instanceof LayerWms && !this.isVisibleInCurrentResolution() ? 'gg-icon-button gg-small resolution-tool' : 'hidden'}" tip="Zoom to visible resolution" onclick="${() => this.zoomToVisibleResolution()}"><img alt="Zoom to visible resolution" src="icons/zoom.svg"></button> <img class="${this.layer.active && this.iconUrl ? 'legend-icon' : 'hidden'}" alt="${'Icon for ' + this.layer.name}" src="${this.iconUrl}" loading="lazy" crossorigin="${this.getCrossOrigin(this.iconUrl)}"></div><button class="${this.layer.active && this.layer instanceof LayerLocalFile ? 'gg-icon-button gg-small gg-opacity tool' : 'hidden'}" tip="Zoom to full extent" onclick="${() => this.zoomToFullExtent()}"><img alt="Zoom to full extent" src="icons/zoom.svg"></button> <button class="${this.layer.active && this.layer instanceof LayerLocalFile ? 'gg-icon-button gg-small gg-opacity tool' : 'hidden'}" tip="Convert to drawing" onclick="${() => this.convertToDrawing()}"><img alt="draw-icon" src="icons/draw.svg"></button> <button class="${this.getButtonClass('swipedLeft')}" tip="Swipe layer to the left" onclick="${() => this.layer.swiped = (this.layer.swiped === 'left') ? 'no' : 'left'}"><img alt="Swipe layer to the left" src="icons/swipe-left.svg"></button> <button class="${this.getButtonClass('swipedRight')}" tip="Swipe layer to the right" onclick="${() => this.layer.swiped = (this.layer.swiped === 'right') ? 'no' : 'right'}"><img alt="Swipe layer to the right" src="icons/swipe-right.svg"></button> <button id="opacity" class="${this.getButtonClass('opacity')}" tip="Control opacity"><img alt="Control opacity" src="icons/opacity.svg"></button> <button id="filter" class="${this.getButtonClass('filter')}" tip="Filter layer"><img alt="Filter layer" src="icons/filter.svg"></button> <button id="advanced-filter" class="${this.getButtonClass('advanced-filter')}" tip="Open advanced filter tool" onclick="${() => this.state.interface.advancedFilterPanelVisible = !this.state.interface.advancedFilterPanelVisible}"><img alt="Advanced filter" src="icons/advanced-filter.svg"></button> <button id="timeRestriction" class="${this.getButtonClass('timeRestriction')}" tip="Define time restriction on layer"><img alt="Define time restriction on layer" src="icons/clock.svg"></button> <button class="${this.getButtonClass('snappable')}" onclick="${() => this.toggleSnap()}" tip="Snap to layer features"><img alt="Enable snapping" src="icons/snapping.svg"></button> <button class="${this.hasLegend ? (this.layer.isLegendExpanded ? 'gg-icon-button gg-small tool' : 'gg-icon-button gg-small gg-opacity tool') : 'hidden'}" tip="Toggle legend" onclick="${() => this.toggleLegend()}"><img alt="Toggle legend" src="icons/legend.svg" loading="lazy"></button> <button class="${this.layer.hasMetadata ? 'metadata gg-icon-button gg-small gg-opacity tool' : 'hidden'}" onclick="${() => this.showMetadata()}"><img alt="Metadata" src="icons/info.svg"></button> <button id="drag-button" class="${this.getButtonClass('draggable')}" tip="Move"><img alt="Move layer" src="icons/drag.svg"></button> <button class="${this.getButtonClass('removable')}" tip="Remove layer" onclick="${() => this.deleteLayer()}"><img alt="Remove layer" src="icons/trash.svg"></button></header>${Object.keys(this.legendUrls).map((l) => uHtml `<div class="${this.isLegendExpanded ? 'legend' : 'hidden'}"><div class="gg-spacer gg-small"></div><img alt="${'Legend for ' + l}" src="${this.legendUrls[l]}" loading="lazy" crossorigin="${this.getCrossOrigin(this.legendUrls[l])}"></div>`)}<div class="${this.layer instanceof LayerLocalFile && this.isLegendExpanded ? 'legend-file' : 'legend-file hidden'}"><div class="gg-spacer gg-small"></div><div class="gg-spacer gg-small"></div><div><span i18n="File from">File from</span> <span>${this.layer.lastModifiedDate}</span> <span>[</span> <span>${this.layer._features?.length}</span> <span i18n="features">features</span> <span>]</span></div></div></div>
23
23
  ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
24
24
  };
25
25
  iconUrl = null;
@@ -224,6 +224,13 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
224
224
  }
225
225
  this.context.mapManager.zoomToExtent(this.layer.extent);
226
226
  }
227
+ convertToDrawing() {
228
+ if (!(this.layer instanceof LayerLocalFile)) {
229
+ throw new Error(`${this.layer.name} is not a LocalFile layer, this method should not be called here.`);
230
+ }
231
+ this.context.localFileManager.convertToDrawing(this.layer);
232
+ this.context.stateManager.state.interface.drawingPanelVisible = true;
233
+ }
227
234
  deleteLayer() {
228
235
  if (this.layer instanceof LayerDrawing) {
229
236
  this.layer.removalRequested = true;
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "GeoGirafe PSC",
6
6
  "url": "https://doc.geogirafe.org"
7
7
  },
8
- "version": "1.2.0-dev.2763713312",
8
+ "version": "1.2.0-dev.2766641185",
9
9
  "type": "module",
10
10
  "engines": {
11
11
  "node": ">=20.19.0"
@@ -1 +1 @@
1
- {"version":"1.2.0-dev.2763713312", "build":"2763713312", "date":"16/08/2026"}
1
+ {"version":"1.2.0-dev.2766641185", "build":"2766641185", "date":"17/08/2026"}
@@ -12,15 +12,30 @@ import GMFManager from './gmfmanager.js';
12
12
  * But you do not get any valid cookie for the GMF Backend.
13
13
  *
14
14
  * For all those reasons, we cannot use the geomapfish oAuth process
15
- * Instead we will use the login.html page of the backend to delegate the login to the backend
16
- * This will be a standard GMF login, there is no oAuth Process here.
15
+ * Instead we delegate the login to the backend, which is a Backend-For-Frontend (BFF) pattern:
16
+ * the frontend only ever holds a session cookie, and never needs any OIDC-specific configuration
17
+ * (no clientId, no issuer url) -- the identity provider is only known to the backend.
18
+ *
19
+ * Two `gmfauth.loginMode` flavors are supported, both delegating entirely to the backend:
20
+ * - 'form' (default): the backend's login.html page, a standard GMF username/password login form.
21
+ * There is no oAuth process at all in this mode.
22
+ * - 'oidc': the backend's oidc/login route (c2cgeoportal_geoportal's OpenID Connect integration).
23
+ * The browser is redirected to oidc/login, which itself redirects to the identity provider;
24
+ * after the user authenticates there, the backend's oidc/callback exchanges the code for tokens
25
+ * (server-side, using the client_secret) and redirects back here with the auth_tkt session cookie
26
+ * set. This is exactly how ngeo authenticates against a GMF backend with
27
+ * `authentication.openid_connect` enabled: the browser never talks to the identity provider
28
+ * directly, and gg-viewer needs no more configuration for this mode than for 'form'.
29
+ *
30
+ * Once the redirect comes back (either flavor), everything below is identical: we only ever
31
+ * trust the session cookie and poll the backend's loginuser endpoint to know who is logged in.
17
32
  *
18
33
  * NOTE: If the geogirafe client is not running on the same domain as the GMF backend,
19
34
  * the GMF Backend needs to be configured with :
20
35
  * - CORS with credentials for specific domain (this can be done for example with an lua script at the in the haproxy configuration)
21
36
  * - The frontend domain has to be allowed as referer in the vars.yaml file.
22
37
  * - The variable AUTHTKT_SAMESITE has to be set to None, to allow authentication cookies to be sent to the backend from another domain
23
- * There is no need for any oAuth2 configuration in the admin tool.
38
+ * These constraints apply identically to both loginMode flavors.
24
39
  */
25
40
  export default class GMFConnectManager extends AbstractConnectManager {
26
41
  private readonly gmfManager;
@@ -10,15 +10,30 @@ import AbstractConnectManager from './abstractconnectmanager.js';
10
10
  * But you do not get any valid cookie for the GMF Backend.
11
11
  *
12
12
  * For all those reasons, we cannot use the geomapfish oAuth process
13
- * Instead we will use the login.html page of the backend to delegate the login to the backend
14
- * This will be a standard GMF login, there is no oAuth Process here.
13
+ * Instead we delegate the login to the backend, which is a Backend-For-Frontend (BFF) pattern:
14
+ * the frontend only ever holds a session cookie, and never needs any OIDC-specific configuration
15
+ * (no clientId, no issuer url) -- the identity provider is only known to the backend.
16
+ *
17
+ * Two `gmfauth.loginMode` flavors are supported, both delegating entirely to the backend:
18
+ * - 'form' (default): the backend's login.html page, a standard GMF username/password login form.
19
+ * There is no oAuth process at all in this mode.
20
+ * - 'oidc': the backend's oidc/login route (c2cgeoportal_geoportal's OpenID Connect integration).
21
+ * The browser is redirected to oidc/login, which itself redirects to the identity provider;
22
+ * after the user authenticates there, the backend's oidc/callback exchanges the code for tokens
23
+ * (server-side, using the client_secret) and redirects back here with the auth_tkt session cookie
24
+ * set. This is exactly how ngeo authenticates against a GMF backend with
25
+ * `authentication.openid_connect` enabled: the browser never talks to the identity provider
26
+ * directly, and gg-viewer needs no more configuration for this mode than for 'form'.
27
+ *
28
+ * Once the redirect comes back (either flavor), everything below is identical: we only ever
29
+ * trust the session cookie and poll the backend's loginuser endpoint to know who is logged in.
15
30
  *
16
31
  * NOTE: If the geogirafe client is not running on the same domain as the GMF backend,
17
32
  * the GMF Backend needs to be configured with :
18
33
  * - CORS with credentials for specific domain (this can be done for example with an lua script at the in the haproxy configuration)
19
34
  * - The frontend domain has to be allowed as referer in the vars.yaml file.
20
35
  * - The variable AUTHTKT_SAMESITE has to be set to None, to allow authentication cookies to be sent to the backend from another domain
21
- * There is no need for any oAuth2 configuration in the admin tool.
36
+ * These constraints apply identically to both loginMode flavors.
22
37
  */
23
38
  export default class GMFConnectManager extends AbstractConnectManager {
24
39
  gmfManager;
@@ -56,7 +71,8 @@ export default class GMFConnectManager extends AbstractConnectManager {
56
71
  redirectToIssuerLogin() {
57
72
  this.context.sessionManager.saveStateToSession();
58
73
  this.redirectUrl = this.getLoginRedirectUrl(false);
59
- const authorizationUrl = new URL(`${this.authConfig.url}login.html`);
74
+ const loginPath = this.authConfig.loginMode === 'oidc' ? 'oidc/login' : 'login.html';
75
+ const authorizationUrl = new URL(`${this.authConfig.url}${loginPath}`);
60
76
  authorizationUrl.searchParams.set('came_from', this.redirectUrl);
61
77
  window.open(authorizationUrl, '_self');
62
78
  }
@@ -201,6 +201,7 @@ declare class GirafeConfig {
201
201
  checkSessionOnLoad: boolean;
202
202
  audience: string[];
203
203
  authMode: 'cookie';
204
+ loginMode: 'form' | 'oidc';
204
205
  alwaysSendCookies: boolean;
205
206
  refererPolicy: ReferrerPolicy;
206
207
  audienceExcludedPaths: string[];
@@ -376,12 +376,18 @@ class GirafeConfig {
376
376
  if (!config.gmfauth.audience) {
377
377
  throw new Error(`Configuration for gmfauth.audience is required. See https://doc.geogirafe.org/docs/configuration.`);
378
378
  }
379
+ if (config.gmfauth.loginMode !== undefined &&
380
+ config.gmfauth.loginMode !== 'form' &&
381
+ config.gmfauth.loginMode !== 'oidc') {
382
+ throw new Error(`Configuration for gmfauth.loginMode must be 'form' or 'oidc'. See https://doc.geogirafe.org/docs/configuration.`);
383
+ }
379
384
  return {
380
385
  url: gmfauthUrl,
381
386
  audience: config.gmfauth.audience,
382
387
  loginRequired: config.gmfauth.loginRequired ?? false,
383
388
  checkSessionOnLoad: config.gmfauth.checkSessionOnLoad ?? true,
384
389
  authMode: 'cookie',
390
+ loginMode: config.gmfauth.loginMode ?? 'form',
385
391
  alwaysSendCookies: config.gmfauth.alwaysSendCookies ?? false,
386
392
  refererPolicy: config.gmfauth.refererPolicy ?? 'strict-origin-when-cross-origin',
387
393
  audienceExcludedPaths: config.gmfauth.audienceExcludedPaths ?? []
@@ -103,6 +103,10 @@ export default class DrawingFeature {
103
103
  getAzimuthText(circle: CircleGeom): string;
104
104
  isPointPolylineOrText(): boolean;
105
105
  getVertexStyle(activeNode?: boolean): Style;
106
+ /**
107
+ * Creates a new DrawingFeature from an ol feature
108
+ */
109
+ static createFromOlFeature(shape: DrawingShape, olFeature: Feature<Geometry>, context: IGirafeContext, drawingState: DrawingState): DrawingFeature;
106
110
  static deserialize(serializedFeature: SerializedFeature, context: IGirafeContext, drawingState: DrawingState): DrawingFeature;
107
111
  static circleToPolygon(center: number[], radius: number, nbEdges?: number): number[][];
108
112
  static geojsonFromOlFeature(olFeature: Feature<Geometry>, shapeType: DrawingShape): object;
@@ -233,6 +233,15 @@ export default class DrawingFeature {
233
233
  })
234
234
  });
235
235
  }
236
+ /**
237
+ * Creates a new DrawingFeature from an ol feature
238
+ */
239
+ static createFromOlFeature(shape, olFeature, context, drawingState) {
240
+ const newFeature = new DrawingFeature(shape, drawingState, context.configManager.Config.drawing);
241
+ newFeature.geojson = DrawingFeature.geojsonFromOlFeature(olFeature, shape);
242
+ newFeature.addToState();
243
+ return newFeature;
244
+ }
236
245
  static deserialize(serializedFeature, context, drawingState) {
237
246
  const newFeature = new DrawingFeature(serializedFeature.t, drawingState, context.configManager.Config.drawing);
238
247
  newFeature.geojson = serializedFeature.g;
@@ -17,6 +17,8 @@ declare class LocalFileManager extends GirafeSingleton {
17
17
  private createInteraction;
18
18
  loadLocalFile(localFile: File): Promise<void>;
19
19
  private loadLocalFileFeatures;
20
+ convertToDrawing(layer: LayerLocalFile): void;
21
+ private splitIntoSimpleGeometries;
20
22
  private handleUnsupportedFiles;
21
23
  private validateAndCompleteFeatures;
22
24
  addLayer(layerFile: LayerLocalFile): void;
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  import GirafeSingleton from '../../base/GirafeSingleton.js';
3
+ import { Feature } from 'ol';
3
4
  import DragAndDrop from 'ol/interaction/DragAndDrop.js';
4
5
  import { GPX, GeoJSON, IGC, KML, TopoJSON } from 'ol/format.js';
5
6
  import { Vector as VectorLayer } from 'ol/layer.js';
@@ -7,6 +8,8 @@ import { Vector as VectorSource } from 'ol/source.js';
7
8
  import LayerLocalFile from '../../models/layers/layerlocalfile.js';
8
9
  import { extend, intersects } from 'ol/extent.js';
9
10
  import { applyFeaturesToSelection } from '../utils/utils.js';
11
+ import DrawingFeature, { DrawingShape } from '../drawing/drawingFeature.js';
12
+ const DrawingStateLocation = 'drawing';
10
13
  class LocalFileManager extends GirafeSingleton {
11
14
  map;
12
15
  name;
@@ -76,11 +79,50 @@ class LocalFileManager extends GirafeSingleton {
76
79
  if (features && features.length > acceptableFeatures.features.length) {
77
80
  // Some features are outer extent
78
81
  layer.hasError = true;
79
- layer.errorMessage = `Only ${acceptableFeatures.features.length} features among ${features.length} could be loaded.
80
- Verify that those features can be displayed within the maximal extent configured in your application.`;
82
+ layer.errorMessage = this.context.i18nManager
83
+ .getTranslation('only-n-features-loaded')
84
+ .replace('_loadedCount_', String(acceptableFeatures.features.length))
85
+ .replace('_totalCount_', String(features.length));
81
86
  }
82
87
  this.context.userLayerManager.addUserLayerToTree(layer);
83
88
  }
89
+ convertToDrawing(layer) {
90
+ const drawingState = this.context.stateManager.state.extendedState[DrawingStateLocation];
91
+ for (const feature of layer._features) {
92
+ const geometry = feature.getGeometry();
93
+ if (!geometry)
94
+ continue;
95
+ for (const { geometry: simpleGeometry, shape } of this.splitIntoSimpleGeometries(geometry)) {
96
+ const drawingFeature = DrawingFeature.createFromOlFeature(shape, new Feature(simpleGeometry), this.context, drawingState);
97
+ drawingFeature.displayName = false;
98
+ drawingFeature.measureInformation = 'none';
99
+ }
100
+ }
101
+ this.context.userLayerManager.removeUserLayerFromTree(layer);
102
+ }
103
+ splitIntoSimpleGeometries(geometry) {
104
+ switch (geometry.getType()) {
105
+ case 'Point':
106
+ return [{ geometry, shape: DrawingShape.Point }];
107
+ case 'LineString':
108
+ return [{ geometry, shape: DrawingShape.Polyline }];
109
+ case 'Polygon':
110
+ return [{ geometry, shape: DrawingShape.Polygon }];
111
+ case 'MultiPoint':
112
+ return geometry.getPoints().map((g) => ({ geometry: g, shape: DrawingShape.Point }));
113
+ case 'MultiLineString':
114
+ return geometry
115
+ .getLineStrings()
116
+ .map((g) => ({ geometry: g, shape: DrawingShape.Polyline }));
117
+ case 'MultiPolygon':
118
+ return geometry.getPolygons().map((g) => ({ geometry: g, shape: DrawingShape.Polygon }));
119
+ case 'GeometryCollection':
120
+ return geometry.getGeometries().flatMap((g) => this.splitIntoSimpleGeometries(g));
121
+ default:
122
+ // Unsupported geometry type
123
+ return [];
124
+ }
125
+ }
84
126
  handleUnsupportedFiles(dropEvent) {
85
127
  if (!this.context.userInteractionManager.canListenerExecute('map.drop', this.name))
86
128
  return;