@geogirafe/lib-geoportal 1.1.0-dev.2610902439 → 1.1.0-dev.2625220907

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.
@@ -31,6 +31,7 @@ import IGirafeContext from '../tools/context/icontext.js';
31
31
  import ApiSessionManager from './apisessionmanager.js';
32
32
  import OnBoardingManager from '../tools/onboarding/onboardingmanager.js';
33
33
  import ThemeFavoritesManager from '../tools/themes/themefavoritesmanager.js';
34
+ import SearchManager from '../tools/search/searchmanager.js';
34
35
  import FeedbackManager from '../tools/feedback/feedbackmanager.js';
35
36
  export default class GirafeApiContext implements IGirafeContext {
36
37
  readonly userDataManager: UserDataManager;
@@ -65,6 +66,7 @@ export default class GirafeApiContext implements IGirafeContext {
65
66
  readonly localFileManager: LocalFileManager;
66
67
  readonly onBoardingManager: OnBoardingManager;
67
68
  readonly themeFavoritesManager: ThemeFavoritesManager;
69
+ readonly searchManager: SearchManager;
68
70
  readonly feedbackManager: FeedbackManager;
69
71
  constructor();
70
72
  initialize(): Promise<void>;
package/api/apicontext.js CHANGED
@@ -31,6 +31,7 @@ import WmsManager from '../tools/wms/wmsmanager.js';
31
31
  import ApiSessionManager from './apisessionmanager.js';
32
32
  import OnBoardingManager from '../tools/onboarding/onboardingmanager.js';
33
33
  import ThemeFavoritesManager from '../tools/themes/themefavoritesmanager.js';
34
+ import SearchManager from '../tools/search/searchmanager.js';
34
35
  import FeedbackManager from '../tools/feedback/feedbackmanager.js';
35
36
  export default class GirafeApiContext {
36
37
  userDataManager;
@@ -65,6 +66,7 @@ export default class GirafeApiContext {
65
66
  localFileManager;
66
67
  onBoardingManager;
67
68
  themeFavoritesManager;
69
+ searchManager;
68
70
  feedbackManager;
69
71
  constructor() {
70
72
  this.componentManager = new ComponentManager(this);
@@ -99,6 +101,7 @@ export default class GirafeApiContext {
99
101
  this.ogcApiFeaturesManager = new OgcApiFeaturesManager(this);
100
102
  this.onBoardingManager = new OnBoardingManager(this);
101
103
  this.themeFavoritesManager = new ThemeFavoritesManager(this);
104
+ this.searchManager = new SearchManager(this);
102
105
  this.feedbackManager = new FeedbackManager(this);
103
106
  }
104
107
  async initialize() {
@@ -137,6 +140,7 @@ export default class GirafeApiContext {
137
140
  this.ogcApiFeaturesManager.initializeSingleton();
138
141
  this.onBoardingManager.initializeSingleton();
139
142
  this.themeFavoritesManager.initializeSingleton();
143
+ this.searchManager.initializeSingleton();
140
144
  this.feedbackManager.initializeSingleton();
141
145
  }
142
146
  }
@@ -220,6 +220,14 @@
220
220
  "Generate CSV": "CSV generieren",
221
221
  "Generating code": "Code wird generiert",
222
222
  "Generating link": "Link wird generiert",
223
+ "geoadminch_address": "Adressen",
224
+ "geoadminch_district": "Bezirke",
225
+ "geoadminch_gazetteer": "Orts- und Flurnamen",
226
+ "geoadminch_gg25": "Administrative Grenzen",
227
+ "geoadminch_haltestellen": "Haltestellen",
228
+ "geoadminch_kantone": "Kantone",
229
+ "geoadminch_parcel": "Parzellen",
230
+ "geoadminch_zipcode": "PLZ",
223
231
  "GeoJSON": "GeoJSON",
224
232
  "Geolocation browser error": "Ihr Browser unterstützt keine Geolokalisierung",
225
233
  "Geolocation error": "Geolocation-Fehler",
@@ -222,6 +222,14 @@
222
222
  "Generate CSV": "Generate CSV",
223
223
  "Generating code": "Generating code",
224
224
  "Generating link": "Generating link",
225
+ "geoadminch_address": "Addresses",
226
+ "geoadminch_district": "Districts",
227
+ "geoadminch_gazetteer": "Location Names",
228
+ "geoadminch_gg25": "Administrative Boundaries",
229
+ "geoadminch_haltestellen": "Public transport stops",
230
+ "geoadminch_kantone": "Cantons",
231
+ "geoadminch_parcel": "Parcels",
232
+ "geoadminch_zipcode": "ZIP codes",
225
233
  "GeoJSON": "GeoJSON",
226
234
  "Geolocation browser error": "Your browser does not support geolocation",
227
235
  "Geolocation error": "Geolocation error",
@@ -220,6 +220,14 @@
220
220
  "Generate CSV": "Générer CSV",
221
221
  "Generating code": "Génération du code",
222
222
  "Generating link": "Génération du lien",
223
+ "geoadminch_address": "Adresse",
224
+ "geoadminch_district": "District",
225
+ "geoadminch_gazetteer": "Noms de lieux et noms de champs",
226
+ "geoadminch_gg25": "Frontières administratives",
227
+ "geoadminch_haltestellen": "Arrêts de transport public",
228
+ "geoadminch_kantone": "Canton",
229
+ "geoadminch_parcel": "Parcelles",
230
+ "geoadminch_zipcode": "Codes postaux",
223
231
  "GeoJSON": "GeoJSON",
224
232
  "Geolocation browser error": "Votre navigateur ne supporte pas la géolocalisation",
225
233
  "Geolocation error": "Erreur de géolocalisation",
@@ -220,6 +220,14 @@
220
220
  "Generate CSV": "Genera CSV",
221
221
  "Generating code": "Generazione del codice",
222
222
  "Generating link": "Generazione del link",
223
+ "geoadminch_address": "Indirizzi",
224
+ "geoadminch_district": "Distretti",
225
+ "geoadminch_gazetteer": "Nomi di luoghi e nomi di campi",
226
+ "geoadminch_gg25": "Frontiere amministrative",
227
+ "geoadminch_haltestellen": "Stazioni di trasporto pubblico",
228
+ "geoadminch_kantone": "Cantoni",
229
+ "geoadminch_parcel": "Parcelle",
230
+ "geoadminch_zipcode": "Codici postali",
223
231
  "GeoJSON": "GeoJSON",
224
232
  "Geolocation browser error": "Il tuo browser non supporta la geolocalizzazione",
225
233
  "Geolocation error": "Errore di geolocalizzazione",
@@ -10,15 +10,10 @@ declare class SearchComponent extends GirafeHTMLElement {
10
10
  private readonly previewFeaturesCollection;
11
11
  private previewLayers?;
12
12
  private previewGeoLayer;
13
- private maxExtent?;
14
- private readonly geoJsonFormatter;
15
13
  private ignoreBlur;
16
- groupedResults: Record<string, Feature[]>;
14
+ groupedResults: Record<string, Record<string, Feature[]>>;
17
15
  protected allResults: Feature[];
18
16
  protected forceHide: boolean;
19
- private readonly searchTermPlaceholder;
20
- private readonly searchLangPlaceholder;
21
- private readonly COORD_REGEX;
22
17
  private focusedResultIndex;
23
18
  private focusedResult;
24
19
  private selectedResult;
@@ -29,7 +24,6 @@ declare class SearchComponent extends GirafeHTMLElement {
29
24
  defaultSearchFillColor: string;
30
25
  searchStrokeColor: string;
31
26
  searchFillColor: string | number[];
32
- private abortController;
33
27
  showNoResultWarning: boolean;
34
28
  private ongoingSearchTimeoutId;
35
29
  constructor();
@@ -42,21 +36,18 @@ declare class SearchComponent extends GirafeHTMLElement {
42
36
  render(): void;
43
37
  registerEvents(): void;
44
38
  protected connectedCallback(): void;
39
+ private flattenResultList;
45
40
  protected clearSearch(purge?: boolean): void;
46
41
  doSearch(e: Event): Promise<void>;
47
- protected fetchSearch(term: string): Promise<Feature[]>;
48
42
  /**
49
43
  * Debounce the fetch call to API to prevent sending request at every stroke.
50
44
  * @param e
51
45
  */
52
46
  doSearchDebounced(e: Event): Promise<void>;
53
- /**
54
- * Will render the result of the search with coordinates
55
- * @param term typed string
56
- */
57
- private displayCoordinates;
58
- private displayResults;
59
- getIcon(searchGroup: string): any;
47
+ getGroupIcon(searchGroup: string): any;
48
+ getProviderClass(provider: string): "hidden" | "provider entry";
49
+ getGroupClass(group: string): "hidden" | "group entry";
50
+ getLabel(result: Feature): unknown;
60
51
  onMouseOver(result: Feature): void;
61
52
  onMouseLeave(): void;
62
53
  private focusResultFromIndex;
@@ -1,14 +1,17 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
1
7
  import { html as uHtml } from 'uhtml';
2
8
  // SPDX-License-Identifier: Apache-2.0
3
9
  import Collection from 'ol/Collection.js';
4
- import Feature from 'ol/Feature.js';
5
- import GeoJSON from 'ol/format/GeoJSON.js';
6
10
  import tippy from 'tippy.js';
7
11
  import VectorSource from 'ol/source/Vector.js';
8
12
  import VectorLayer from 'ol/layer/Vector.js';
9
- import { Point } from 'ol/geom.js';
10
13
  import { Fill, Icon, Stroke, Style } from 'ol/style.js';
11
- import { buffer, containsExtent, getCenter, getHeight, getWidth } from 'ol/extent.js';
14
+ import { containsExtent, getCenter } from 'ol/extent.js';
12
15
  import GirafeColorPicker from '../../tools/utils/girafecolorpicker.js';
13
16
  import PinIcon from './images/pin.svg';
14
17
  import LayerIcon from './images/layer.svg';
@@ -16,7 +19,8 @@ import LayerGroupIcon from './images/layergroup.svg';
16
19
  import SearchIcon from './images/search.svg';
17
20
  import PaintbrushIcon from './images/paintbrush.svg';
18
21
  import GirafeHTMLElement from '../../base/GirafeHTMLElement.js';
19
- import { parseCoordinates } from '../../tools/geometrytools.js';
22
+ import { UsedInTemplateOnly } from '../../decorators.js';
23
+ import { SEARCH_GROUP_DEFAULT, SEARCH_PROVIDER_DEFAULT } from '../../tools/search/abstractsearchclient.js';
20
24
  class SearchComponent extends GirafeHTMLElement {
21
25
  templateUrl = null;
22
26
  styleUrls = null;
@@ -24,10 +28,10 @@ class SearchComponent extends GirafeHTMLElement {
24
28
  return uHtml `<style>
25
29
  *{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}
26
30
  </style><style>
27
- .hidden{display:none}#searchbox{background-color:var(--bkg-color);border:1px solid #cfd6dd;border-radius:4px;flex-direction:row;width:100%;height:2.25rem;display:flex;& slot[name=search-tooltip]{display:none}}#search{-webkit-appearance:none;width:auto;height:2.25rem;color:var(--text-color);background:0 0;border:0;outline:none;-webkit-flex:auto;-ms-flex:auto;flex:auto;margin:0 0 0 .5rem;padding-left:.8rem;font-size:1rem}.search-icon,.close-icon,.select-color-icon{width:14px;color:var(--text-color);padding:0 1rem}.close-button,.select-color-button{cursor:pointer;background-color:#0000;border:none;padding:0}#results{z-index:1000;background-color:var(--bkg-color);scrollbar-width:thin;width:36rem;max-height:31rem;margin-top:.1rem;margin-bottom:.5rem;position:absolute;top:3.5rem;overflow-x:hidden}.result,.title{width:34rem;padding:.3rem 1rem;line-height:1.3rem;display:inline-block}.title{color:var(--text-color);padding-top:1.5rem;font-size:1.3rem;font-weight:600;line-height:2.5rem}.result{cursor:pointer;text-align:left;width:100%;color:var(--text-color);background:0 0;border:none;border-top:1px solid #ccc}.title img{width:14px;margin-right:1rem}.title span{text-transform:uppercase}.result.selected{color:var(--text-color);background-color:#aaa!important}#no-result{background:var(--warning-color);opacity:.75;visibility:hidden;pointer-events:none;border-radius:6px;width:fit-content;margin:auto;padding:.5rem 0;font-size:1rem;position:absolute;top:3.5rem;left:0;right:0}#no-result>span{padding:1.3rem}@keyframes fadeOut{0%{opacity:1}80%{opacity:1}to{opacity:0}}.shown-then-fadeout{animation:2s ease-in-out forwards fadeOut;visibility:visible!important}
31
+ .hidden{display:none}#searchbox{background-color:var(--bkg-color);border:1px solid #cfd6dd;border-radius:4px;flex-direction:row;width:100%;height:2.25rem;display:flex;& slot[name=search-tooltip]{display:none}}#search{-webkit-appearance:none;width:auto;height:2.25rem;color:var(--text-color);background:0 0;border:0;outline:none;-webkit-flex:auto;-ms-flex:auto;flex:auto;margin:0 0 0 .5rem;padding-left:.8rem;font-size:1rem}.search-icon,.close-icon,.select-color-icon{width:14px;color:var(--text-color);padding:0 1rem}.close-button,.select-color-button{cursor:pointer;background-color:#0000;border:none;padding:0}#results{z-index:1000;background-color:var(--bkg-color);scrollbar-width:thin;width:36rem;max-height:31rem;margin-top:.1rem;margin-bottom:.5rem;position:absolute;top:3.5rem;overflow-x:hidden}.entry{width:34rem;color:var(--text-color);padding:.3rem 1rem;line-height:1.3rem;display:inline-block}.provider,.group{font-size:1.2rem;font-weight:600;line-height:3rem}.provider{background-color:color-mix(in srgb, var(--bkg-color-grad1) 75%, transparent);margin-top:1rem;font-size:1.5rem}#results>.provider:first-child{margin-top:0}.result{cursor:pointer;text-align:left;background:0 0;border:none;border-top:1px solid #ccc;width:100%}.entry img{width:.8em;margin-right:1rem}.entry span{text-transform:uppercase}.result.selected{color:var(--text-color);background-color:var(--bkg-color-grad2)!important}#no-result{background:var(--warning-color);opacity:.75;visibility:hidden;pointer-events:none;border-radius:6px;width:fit-content;margin:auto;padding:.5rem 0;font-size:1rem;position:absolute;top:3.5rem;left:0;right:0}#no-result>span{padding:1.3rem}@keyframes fadeOut{0%{opacity:1}80%{opacity:1}to{opacity:0}}.shown-then-fadeout{animation:2s ease-in-out forwards fadeOut;visibility:visible!important}
28
32
  </style>
29
33
  <style>${this.customStyle}</style>
30
- <link rel="stylesheet" href="lib/vanilla-picker/vanilla-picker.csp.css"><link rel="stylesheet" href="styles/girafecolorpicker.css"><div id="searchbox"><slot name="search-tooltip"></slot><input id="search" class="gg-input" length="20" maxlength="1000" autocomplete="off" autocorrect="off" i18n="Search..." placeholder="Search..." oninput="${(e) => this.doSearchDebounced(e)}" onfocusin="${() => this.onFocusIn()}" onfocusout="${() => this.onFocusOut()}" onkeydown="${(e) => this.onKeyDown(e)}"> <img class="${this.allResults.length > 0 ? 'hidden' : 'search-icon'}" alt="search-icon" src="${this.searchIcon}"> <button class="${this.allResults.length > 0 ? 'close-button' : 'hidden'}" onclick="${() => this.clearSearch(true)}"><img class="close-icon" alt="close-icon" src="icons/close.svg"></button> <button tip="change_search_result_color" id="colorPickerBtn" class="${this.paintSearchResults ? 'select-color-button' : 'hidden'}"><img class="select-color-icon" alt="select-color-icon" src="${this.paintbrushIcon}"></button><div id="results" class="${Object.keys(this.groupedResults).length === 0 || this.forceHide ? 'hidden' : ''}" onmousemove="${() => this.onMouseMove()}">${Object.keys(this.groupedResults).map(group => uHtml `<div class="title"><img alt="result icon" src="${this.getIcon(group)}"> <span i18n="${group}"></span></div>${this.groupedResults[group].map(result => uHtml ` <button class="${result.get('selected') ? 'result selected' : 'result'}" onmousedown="${() => this.onMouseDown()}" onclick="${() => this.onSelect(result)}" onmouseover="${() => this.onMouseOver(result)}" onmouseleave="${() => this.onMouseLeave()}">${result.get('label')}</button> `)} `)}</div><div id="no-result" class="${this.showNoResultWarning ? 'shown-then-fadeout' : ''}"><span i18n="${'No match found'}"></span></div></div>
34
+ <link rel="stylesheet" href="lib/vanilla-picker/vanilla-picker.csp.css"><link rel="stylesheet" href="styles/girafecolorpicker.css"><div id="searchbox"><slot name="search-tooltip"></slot><input id="search" class="gg-input" length="20" maxlength="1000" autocomplete="off" autocorrect="off" i18n="Search..." placeholder="Search..." aria-label="Search" oninput="${(e) => this.doSearchDebounced(e)}" onfocusin="${() => this.onFocusIn()}" onfocusout="${() => this.onFocusOut()}" onkeydown="${(e) => this.onKeyDown(e)}"> <img class="${this.allResults.length > 0 ? 'hidden' : 'search-icon'}" alt="search-icon" src="${this.searchIcon}"> <button class="${this.allResults.length > 0 ? 'close-button' : 'hidden'}" onclick="${() => this.clearSearch(true)}"><img class="close-icon" alt="close-icon" src="icons/close.svg"></button> <button tip="change_search_result_color" id="colorPickerBtn" class="${this.paintSearchResults ? 'select-color-button' : 'hidden'}"><img class="select-color-icon" alt="select-color-icon" src="${this.paintbrushIcon}"></button><div id="results" class="${!this.allResults.length || this.forceHide ? 'hidden' : ''}" onmousemove="${() => this.onMouseMove()}">${(Object.keys(this.groupedResults) ?? []).map(provider => uHtml `<div class="${this.getProviderClass(provider)}"><img alt="provider icon" src="${this.searchIcon}"> <span i18n="${provider}">${provider}</span></div>${(Object.keys(this.groupedResults[provider]) ?? []).map(group => uHtml `<div class="${this.getGroupClass(group)}"><img alt="result icon" src="${this.getGroupIcon(group)}"> <span i18n="${group}">${group}</span></div>${(this.groupedResults[provider][group] ?? []).map(result => uHtml ` <button class="${result.get('selected') ? 'result entry selected' : 'result entry'}" onmousedown="${() => this.onMouseDown()}" onclick="${() => this.onSelect(result)}" onmouseover="${() => this.onMouseOver(result)}" onmouseleave="${() => this.onMouseLeave()}">${this.getLabel(result)}</button> `)} `)} `)}</div><div id="no-result" class="${this.showNoResultWarning ? 'shown-then-fadeout' : ''}"><span i18n="${'No match found'}"></span></div></div>
31
35
  ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
32
36
  };
33
37
  searchIcon = SearchIcon;
@@ -38,15 +42,10 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
38
42
  previewFeaturesCollection = new Collection();
39
43
  previewLayers;
40
44
  previewGeoLayer = null;
41
- maxExtent;
42
- geoJsonFormatter = new GeoJSON();
43
45
  ignoreBlur = false;
44
46
  groupedResults = {};
45
47
  allResults = [];
46
48
  forceHide = true;
47
- searchTermPlaceholder = '###SEARCHTERM###';
48
- searchLangPlaceholder = '###SEARCHLANG###';
49
- COORD_REGEX = /^(\d+[.,]?\d*)\s*[,;/\s]\s*(\d+[.,]?\d*)$/;
50
49
  focusedResultIndex = -1;
51
50
  focusedResult = null;
52
51
  selectedResult = null;
@@ -57,7 +56,6 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
57
56
  defaultSearchFillColor;
58
57
  searchStrokeColor;
59
58
  searchFillColor;
60
- abortController = new AbortController();
61
59
  showNoResultWarning = false;
62
60
  // Keeping track of the last input timeout
63
61
  ongoingSearchTimeoutId = 0;
@@ -66,17 +64,18 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
66
64
  }
67
65
  async initialSearch() {
68
66
  const searchTerm = this.context.permalinkManager.getSearchTerm();
69
- const results = await this.fetchSearch(searchTerm);
70
- if (results.length > 0) {
67
+ const results = await this.context.searchManager.doSearch(searchTerm);
68
+ if (results !== null) {
71
69
  // Apply the first search result in the list
72
- const firstResult = results[0];
73
- this.preview(firstResult);
74
- this.onSelect(firstResult);
70
+ const firstResults = this.flattenResultList(results);
71
+ if (firstResults.length) {
72
+ this.preview(firstResults[0]);
73
+ this.onSelect(firstResults[0]);
74
+ }
75
75
  }
76
76
  }
77
77
  createPreviewLayer() {
78
78
  this.paintSearchResults = this.context.configManager.Config.search.paintSearchResults;
79
- this.maxExtent = this.context.configManager.Config.map.maxExtent?.split(',').map(Number);
80
79
  this.previewGeoLayer = new VectorLayer({
81
80
  properties: {
82
81
  addToPrintedLayers: true
@@ -133,11 +132,15 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
133
132
  if (this.context.permalinkManager.hasSearch()) {
134
133
  this.subscribe('application.isReady', () => {
135
134
  if (this.state.application.isReady) {
136
- this.initialSearch();
135
+ void this.initialSearch();
137
136
  }
138
137
  });
139
138
  }
140
139
  }
140
+ flattenResultList(results) {
141
+ results ??= this.groupedResults;
142
+ return Object.values(results).flatMap((providerResults) => Object.values(providerResults).flat());
143
+ }
141
144
  clearSearch(purge = false) {
142
145
  if (purge) {
143
146
  if (this.searchInput) {
@@ -154,26 +157,26 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
154
157
  }
155
158
  async doSearch(e) {
156
159
  this.showNoResultWarning = false;
157
- // Cancel any previous search
158
- this.abortController.abort();
159
- // Create a new controller for the new request
160
- this.abortController = new AbortController();
160
+ this.context.searchManager.abortSearch();
161
+ this.clearSearch();
161
162
  const target = e.target;
162
163
  const term = target.value.trim();
163
- this.clearSearch();
164
- if (this.COORD_REGEX.test(term)) {
165
- this.displayCoordinates(term);
166
- return;
167
- }
168
164
  if (term.length > 0) {
169
165
  try {
170
- const features = await this.fetchSearch(term);
171
- // If the search term is at least two charecter but yieds no result, a warning
172
- // box is displayed for 2 seconds and then fades out (CSS)
173
- if (features.length === 0 && term.length >= 2) {
174
- this.showNoResultWarning = true;
166
+ const features = await this.context.searchManager.doSearch(term);
167
+ // Check if a search was performed at all (min term length!)
168
+ if (features !== null) {
169
+ this.groupedResults = features;
170
+ this.allResults = this.flattenResultList();
171
+ if (!this.allResults.length) {
172
+ // If the search did not yield any results, a warning
173
+ // box is displayed for 2 seconds and then fades out (CSS)
174
+ this.showNoResultWarning = true;
175
+ }
175
176
  }
176
- this.displayResults(features);
177
+ // And then rerender the results
178
+ super.render();
179
+ super.girafeTranslate();
177
180
  }
178
181
  catch (error) {
179
182
  if (error instanceof DOMException && error.name === 'AbortError') {
@@ -184,17 +187,6 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
184
187
  }
185
188
  }
186
189
  }
187
- async fetchSearch(term) {
188
- const url = this.context.configManager.Config.search.url
189
- .replace(this.searchTermPlaceholder, term)
190
- .replace(this.searchLangPlaceholder, this.state.language);
191
- const response = await fetch(url, { signal: this.abortController.signal });
192
- const data = await response.json();
193
- return this.geoJsonFormatter.readFeatures(data, {
194
- dataProjection: this.context.configManager.getDefaultConfigValue('search.resultsSrid'),
195
- featureProjection: this.map.getView().getProjection()
196
- });
197
- }
198
190
  /**
199
191
  * Debounce the fetch call to API to prevent sending request at every stroke.
200
192
  * @param e
@@ -213,64 +205,7 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
213
205
  this.doSearch(syntheticEvent);
214
206
  }, 300);
215
207
  }
216
- /**
217
- * Will render the result of the search with coordinates
218
- * @param term typed string
219
- */
220
- displayCoordinates(term) {
221
- const matches = this.COORD_REGEX.exec(term);
222
- const coord1 = Number.parseFloat(matches[1].replace(',', '.'));
223
- const coord2 = Number.parseFloat(matches[2].replace(',', '.'));
224
- const current_srid = this.map.getView().getProjection().getCode();
225
- const [east_coord, north_coord] = parseCoordinates([coord1, coord2], this.maxExtent, current_srid);
226
- // Don't show result if no corresponding coordinates were parsed
227
- if (!east_coord || !north_coord) {
228
- return;
229
- }
230
- const feature = new Feature({
231
- geometry: new Point([east_coord, north_coord]),
232
- label: `${coord1} ${coord2}`,
233
- layer_name: 'recenter_map'
234
- });
235
- this.allResults = [feature];
236
- this.groupedResults.recenter_map = [feature];
237
- super.render();
238
- super.girafeTranslate();
239
- }
240
- displayResults(features) {
241
- // First, group the results
242
- for (const result of features) {
243
- // results.features.forEach((result) => {
244
- let type = 'Unknown layer type';
245
- if (result.get('layer_name')) {
246
- type = result.get('layer_name');
247
- }
248
- else if (result.get('actions')[0].action.startsWith('add_theme')) {
249
- type = 'add_theme';
250
- }
251
- else if (result.get('actions')[0].action.startsWith('add_group')) {
252
- type = 'add_group';
253
- }
254
- else if (result.get('actions')[0].action.startsWith('add_layer')) {
255
- type = 'add_layer';
256
- }
257
- let resultList;
258
- if (type in this.groupedResults) {
259
- resultList = this.groupedResults[type];
260
- }
261
- else {
262
- resultList = [];
263
- this.groupedResults[type] = resultList;
264
- }
265
- resultList.push(result);
266
- }
267
- // Manage a flat list with all results
268
- this.allResults = Object.values(this.groupedResults).flat();
269
- // And then rerender the results
270
- super.render();
271
- super.girafeTranslate();
272
- }
273
- getIcon(searchGroup) {
208
+ getGroupIcon(searchGroup) {
274
209
  switch (searchGroup) {
275
210
  case 'Group':
276
211
  return LayerGroupIcon;
@@ -280,6 +215,21 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
280
215
  return PinIcon;
281
216
  }
282
217
  }
218
+ getProviderClass(provider) {
219
+ if (Object.keys(this.groupedResults).length === 1 || provider === SEARCH_PROVIDER_DEFAULT) {
220
+ return 'hidden';
221
+ }
222
+ return 'provider entry';
223
+ }
224
+ getGroupClass(group) {
225
+ if (group === SEARCH_GROUP_DEFAULT) {
226
+ return 'hidden';
227
+ }
228
+ return 'group entry';
229
+ }
230
+ getLabel(result) {
231
+ return result.get('label_html') ? this.htmlUnsafe(result.get('label_html')) : result.get('label');
232
+ }
283
233
  onMouseOver(result) {
284
234
  this.focusResult(result);
285
235
  }
@@ -349,10 +299,10 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
349
299
  this.forceHide = true;
350
300
  this.previewLayers = undefined;
351
301
  super.render();
352
- const geom = feature.getGeometry();
353
- if (geom) {
354
- // Result with geometry
355
- this.zoomTo(geom.getExtent());
302
+ // If the result has a specific zoom-to extent defined or contains a geometry, zoom to it
303
+ const zoomBbox = this.context.searchManager.getZoomToBbox(feature);
304
+ if (zoomBbox) {
305
+ this.zoomTo(zoomBbox);
356
306
  }
357
307
  else {
358
308
  this.addResultToTreeView(feature);
@@ -398,9 +348,6 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
398
348
  }
399
349
  }
400
350
  zoomTo(extent) {
401
- // We create a buffer around the extent from 50% of the width/height
402
- const bufferValue = Math.max((getWidth(extent) * 50) / 100, (getHeight(extent) * 50) / 100);
403
- const bufferedExtent = buffer(extent, bufferValue);
404
351
  const minResolution = this.context.configManager.Config.search.minResolution;
405
352
  const currentResolution = this.map.getView().getResolution();
406
353
  const currentExtent = this.map.getView().calculateExtent();
@@ -408,7 +355,7 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
408
355
  if (currentResolution > minResolution) {
409
356
  // If we are in a bigger resolution as the minimal one,
410
357
  // Zoom to object with minResolution
411
- this.context.mapManager.zoomToExtent(bufferedExtent, minResolution);
358
+ this.context.mapManager.zoomToExtent(extent, minResolution);
412
359
  }
413
360
  else if (!containsExtent(currentExtent, extent)) {
414
361
  // Else, if the extent is NOT already within the current extent of the map
@@ -570,4 +517,16 @@ ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
570
517
  return `data:image/svg+xml;utf8,${encodeURIComponent(pin)}`;
571
518
  }
572
519
  }
520
+ __decorate([
521
+ UsedInTemplateOnly()
522
+ ], SearchComponent.prototype, "getGroupIcon", null);
523
+ __decorate([
524
+ UsedInTemplateOnly()
525
+ ], SearchComponent.prototype, "getProviderClass", null);
526
+ __decorate([
527
+ UsedInTemplateOnly()
528
+ ], SearchComponent.prototype, "getGroupClass", null);
529
+ __decorate([
530
+ UsedInTemplateOnly()
531
+ ], SearchComponent.prototype, "getLabel", null);
573
532
  export default SearchComponent;
@@ -62,20 +62,29 @@
62
62
  scrollbar-width: thin;
63
63
  }
64
64
 
65
- .result,
66
- .title {
65
+ .entry {
67
66
  display: inline-block;
68
67
  padding: 0.3rem 1rem;
69
- width: Calc(36rem - 2rem);
68
+ width: calc(36rem - 2rem);
70
69
  line-height: 1.3rem;
70
+ color: var(--text-color);
71
71
  }
72
72
 
73
- .title {
74
- color: var(--text-color);
73
+ .provider,
74
+ .group {
75
75
  font-weight: 600;
76
- line-height: 2.5rem;
77
- font-size: 1.3rem;
78
- padding-top: 1.5rem;
76
+ line-height: 3rem;
77
+ font-size: 1.2rem;
78
+ }
79
+
80
+ .provider {
81
+ font-size: 1.5rem;
82
+ margin-top: 1rem;
83
+ background-color: color-mix(in srgb, var(--bkg-color-grad1) 75%, transparent);
84
+ }
85
+
86
+ #results > .provider:first-child {
87
+ margin-top: 0;
79
88
  }
80
89
 
81
90
  .result {
@@ -85,20 +94,19 @@
85
94
  background: transparent;
86
95
  text-align: left;
87
96
  width: 100%;
88
- color: var(--text-color);
89
97
  }
90
98
 
91
- .title img {
99
+ .entry img {
92
100
  margin-right: 1rem;
93
- width: 14px;
101
+ width: 0.8em;
94
102
  }
95
103
 
96
- .title span {
104
+ .entry span {
97
105
  text-transform: uppercase;
98
106
  }
99
107
 
100
108
  .result.selected {
101
- background-color: #aaa !important;
109
+ background-color: var(--bkg-color-grad2) !important;
102
110
  color: var(--text-color);
103
111
  }
104
112
 
@@ -8,10 +8,10 @@ class MobileSearchComponent extends SearchComponent {
8
8
  return uHtml `<style>
9
9
  .hidden{display:none}.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)}.gg-icon-button,.gg-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-button{text-align:left}.gg-small{flex-direction:row}.gg-small img{width:calc(var(--app-standard-height) / 2.5);margin:0}.gg-small span{margin-left:.5rem;font-size:1rem}.girafe-button-big{filter:drop-shadow(0 0 8px #0000004d);background-color:var(--bkg-color);width:3.5rem;height:3.5rem;color:var(--text-color);border:none;border-radius:4rem;flex-direction:column;align-items:center;padding:.5rem;display:flex}.girafe-button-big img{width:95%;height:auto;padding:5%;overflow:hidden}.girafe-button-big:hover,.girafe-button-large:hover,.girafe-button-small:hover,.girafe-button-tiny:hover{background-color:var(--bkg-color)}
10
10
  </style><style>
11
- .hidden{display:none}#searchbox{background-color:var(--bkg-color);z-index:101;border:none;border-radius:4px;flex-direction:row;width:100%;display:flex;position:absolute;box-shadow:0 0 10px 1px #0003}#search{z-index:2;-webkit-appearance:none;width:100%;color:var(--text-color);background:0 0;border:0;outline:none;-webkit-flex:auto;-ms-flex:auto;flex:auto;margin:0 0 0 .5rem;padding-left:.8rem;font-size:1.8em;line-height:1.8em}#search::placeholder{color:inherit;opacity:.4}#search:focus::placeholder{opacity:0;color:#0000;transition:opacity .3s}.search-icon,.close-icon,.select-color-icon{width:17px;color:var(--text-color);opacity:.7;padding:0 1rem}.close-button,.select-color-button{cursor:pointer;background-color:#0000;border:none;padding:0}#results{z-index:100;background-color:var(--bkg-color);scrollbar-width:thin;padding-top:7vh;position:fixed;inset:0;overflow-x:hidden}.result,.title{width:34rem;padding:.3rem 1rem;line-height:1.3rem;display:inline-block}.title{color:var(--text-color);padding-top:1.5rem;font-size:1.3rem;font-weight:600;line-height:2.5rem}.result{cursor:pointer;text-align:left;width:100%;color:var(--text-color);background:0 0;border:none;border-top:1px solid #ccc}.title img{width:14px;margin-right:1rem}.title span{text-transform:uppercase}.result.selected{color:var(--text-color);background-color:#aaa!important}#no-result{background:var(--warning-color);opacity:.75;visibility:hidden;pointer-events:none;border-radius:6px;width:fit-content;margin:auto;padding:.5rem 0;font-size:1rem;position:absolute;top:3rem;left:0;right:0}#no-result>span{padding:1.3rem}@keyframes fadeOut{0%{opacity:1}80%{opacity:1}to{opacity:0}}.shown-then-fadeout{animation:2s ease-in-out forwards fadeOut;visibility:visible!important}
11
+ .hidden{display:none}#searchbox{background-color:var(--bkg-color);z-index:101;border:none;border-radius:4px;flex-direction:row;width:100%;height:3rem;display:flex;position:absolute;box-shadow:0 0 10px 1px #0003}#search{z-index:2;-webkit-appearance:none;width:100%;color:var(--text-color);background:0 0;border:0;outline:none;-webkit-flex:auto;-ms-flex:auto;flex:auto;margin:0 0 0 .5rem;padding-left:.8rem;font-size:1.8em;line-height:1.8em}#search::placeholder{color:inherit;opacity:.4}#search:focus::placeholder{opacity:0;color:#0000;transition:opacity .3s}.search-icon,.close-icon,.select-color-icon{width:17px;color:var(--text-color);opacity:.7;padding:0 1rem}.close-button,.select-color-button{cursor:pointer;background-color:#0000;border:none;padding:0}#results{z-index:100;background-color:var(--bkg-color);scrollbar-width:thin;padding-top:calc(max(2.5em, calc(env(safe-area-inset-top) + .5rem)) + 3rem + 1rem);position:fixed;inset:0;overflow-x:hidden}.entry{width:34rem;color:var(--text-color);padding:.3rem 1rem;line-height:1.3rem;display:inline-block}.provider,.group{font-size:1.3rem;font-weight:600;line-height:3rem}.provider{background-color:color-mix(in srgb, var(--bkg-color-grad1) 75%, transparent);border:0 solid var(--bkg-color-grad2);border-width:1px 0;margin-top:1rem;font-size:1.5rem}#results>.provider:first-child{margin-top:0}.result{cursor:pointer;text-align:left;background:0 0;border:none;border-top:1px solid #ccc;width:100%}.entry img{width:.8em;margin-right:1rem}.entry span{text-transform:uppercase}.result.selected{color:var(--text-color);background-color:var(--bkg-color-grad2)!important}#no-result{background:var(--warning-color);opacity:.75;visibility:hidden;pointer-events:none;border-radius:6px;width:fit-content;margin:auto;padding:.5rem 0;font-size:1rem;position:absolute;top:3rem;left:0;right:0}#no-result>span{padding:1.3rem}@keyframes fadeOut{0%{opacity:1}80%{opacity:1}to{opacity:0}}.shown-then-fadeout{animation:2s ease-in-out forwards fadeOut;visibility:visible!important}
12
12
  </style>
13
13
  <style>${this.customStyle}</style>
14
- <link rel="stylesheet" href="lib/vanilla-picker/vanilla-picker.csp.css"><div id="searchbox"><input id="search" class="gg-input" length="20" maxlength="1000" autocomplete="off" autocorrect="off" i18n="Search" placeholder="Search..." oninput="${(e) => this.doSearchDebounced(e)}" onkeydown="${(e) => this.onKeyDown(e)}"> <img class="${this.allResults.length > 0 ? 'hidden' : 'search-icon'}" alt="search-icon" src="${this.searchIcon}"> <button class="${this.allResults.length > 0 ? 'close-button' : 'hidden'}" onclick="${() => this.clearSearch(true)}"><img class="close-icon" alt="close-icon" src="icons/close.svg"></button></div><div id="results" class="${Object.keys(this.groupedResults).length === 0 || this.forceHide ? 'hidden' : ''}">${Object.keys(this.groupedResults).map(group => uHtml `<div class="title"><img alt="result icon" src="${this.getIcon(group)}"> <span i18n="${group}"></span></div>${this.groupedResults[group].map(result => uHtml ` <button class="${result.get('selected') ? 'result selected' : 'result'}" onmousedown="${() => this.onMouseDown()}" onclick="${() => this.onSelect(result)}">${result.get('label')}</button> `)} `)}</div><div id="no-result" class="${this.showNoResultWarning ? 'shown-then-fadeout' : ''}"><span i18n="${'No match found'}"></span></div>
14
+ <link rel="stylesheet" href="lib/vanilla-picker/vanilla-picker.csp.css"><div id="searchbox"><input id="search" class="gg-input" length="20" maxlength="1000" autocomplete="off" autocorrect="off" i18n="Search" placeholder="Search..." aria-label="Search" oninput="${(e) => this.doSearchDebounced(e)}" onkeydown="${(e) => this.onKeyDown(e)}"> <img class="${this.allResults.length > 0 ? 'hidden' : 'search-icon'}" alt="search-icon" src="${this.searchIcon}"> <button class="${this.allResults.length > 0 ? 'close-button' : 'hidden'}" onclick="${() => this.clearSearch(true)}"><img class="close-icon" alt="close-icon" src="icons/close.svg"></button></div><div id="results" class="${!this.allResults.length || this.forceHide ? 'hidden' : ''}">${(Object.keys(this.groupedResults) ?? []).map(provider => uHtml `<div class="${this.getProviderClass(provider)}"><img alt="provider icon" src="${this.searchIcon}"> <span i18n="${provider}">${provider}</span></div>${(Object.keys(this.groupedResults[provider]) ?? []).map(group => uHtml `<div class="${this.getGroupClass(group)}"><img alt="result icon" src="${this.getGroupIcon(group)}"> <span i18n="${group}">${group}</span></div>${(this.groupedResults[provider][group] ?? []).map(result => uHtml ` <button class="${result.get('selected') ? 'result entry selected' : 'result entry'}" onmousedown="${() => this.onMouseDown()}" onclick="${() => this.onSelect(result)}">${this.getLabel(result)}</button> `)} `)} `)}</div><div id="no-result" class="${this.showNoResultWarning ? 'shown-then-fadeout' : ''}"><span i18n="${'No match found'}"></span></div>
15
15
  ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
16
16
  };
17
17
  }