@geogirafe/lib-geoportal 1.1.0-dev.2605752849 → 1.1.0-dev.2623495582

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/api/apicontext.d.ts +2 -0
  2. package/api/apicontext.js +4 -0
  3. package/assets/i18n/de.json +8 -0
  4. package/assets/i18n/en.json +8 -0
  5. package/assets/i18n/fr.json +8 -0
  6. package/assets/i18n/it.json +8 -0
  7. package/components/getdirections/component.js +1 -0
  8. package/components/search/component.d.ts +6 -15
  9. package/components/search/component.js +71 -112
  10. package/components/search/style.css +21 -13
  11. package/components/search-mobile/component.js +2 -2
  12. package/components/search-mobile/style.css +25 -13
  13. package/components/treeview/treeviewroot/style.css +1 -1
  14. package/package.json +2 -1
  15. package/templates/public/about.json +1 -1
  16. package/templates/tsconfig.json +1 -1
  17. package/templates/vite.config.js +1 -1
  18. package/tools/app/geogirafeapp.js +15 -6
  19. package/tools/configuration/girafeconfig.d.ts +13 -2
  20. package/tools/configuration/girafeconfig.js +32 -12
  21. package/tools/context/context.d.ts +2 -0
  22. package/tools/context/context.js +4 -0
  23. package/tools/context/icontext.d.ts +2 -0
  24. package/tools/i18n/i18nmanager.js +1 -1
  25. package/tools/main.d.ts +9 -0
  26. package/tools/main.js +5 -0
  27. package/tools/offline/offlinemanager.d.ts +5 -4
  28. package/tools/offline/offlinemanager.js +50 -80
  29. package/tools/search/abstractsearchclient.d.ts +42 -0
  30. package/tools/search/abstractsearchclient.js +85 -0
  31. package/tools/search/geoadminchsearchclient.d.ts +63 -0
  32. package/tools/search/geoadminchsearchclient.js +147 -0
  33. package/tools/search/gmfsearchclient.d.ts +23 -0
  34. package/tools/search/gmfsearchclient.js +37 -0
  35. package/tools/search/searchmanager.d.ts +33 -0
  36. package/tools/search/searchmanager.js +130 -0
  37. package/tools/sw/service-worker.d.ts +1 -0
  38. package/tools/sw/service-worker.js +94 -0
  39. package/tools/sw/service-worker.tools.d.ts +83 -0
  40. package/tools/sw/service-worker.tools.js +321 -0
  41. package/tools/tests/mockconfig.d.ts +5 -2
  42. package/tools/tests/mockconfig.js +2 -2
  43. package/tools/tests/mockcontext.d.ts +2 -0
  44. package/tools/tests/mockcontext.js +4 -0
  45. package/service-worker.js +0 -331
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  import GirafeSingleton from '../../base/GirafeSingleton.js';
3
+ import { IndexedDbHelper } from '../sw/service-worker.tools';
3
4
  import { Feature } from 'ol';
4
5
  import VectorLayer from 'ol/layer/Vector.js';
5
6
  import VectorSource from 'ol/source/Vector.js';
@@ -8,13 +9,17 @@ import Style from 'ol/style/Style.js';
8
9
  import { Stroke } from 'ol/style.js';
9
10
  class OfflineManager extends GirafeSingleton {
10
11
  serviceWorker = null;
11
- database;
12
+ indexedDbHelper = new IndexedDbHelper();
12
13
  get map() {
13
14
  return this.context.mapManager.getMap();
14
15
  }
15
16
  get state() {
16
17
  return this.context.stateManager.state;
17
18
  }
19
+ async database() {
20
+ const database = await this.indexedDbHelper.openDb(this.dbCacheName, this.storeVersion, this.upgradeIndexedDb);
21
+ return database;
22
+ }
18
23
  totalLength = 0;
19
24
  counter = 0;
20
25
  progressCallback;
@@ -38,10 +43,10 @@ class OfflineManager extends GirafeSingleton {
38
43
  this.context.stateManager.state.isOffline = isOffline;
39
44
  }
40
45
  registerEvents() {
41
- window.addEventListener('offline', () => {
46
+ globalThis.addEventListener('offline', () => {
42
47
  this.state.isOffline = true;
43
48
  });
44
- window.addEventListener('online', () => {
49
+ globalThis.addEventListener('online', () => {
45
50
  this.state.isOffline = false;
46
51
  });
47
52
  this.context.stateManager.subscribe('isOffline', () => {
@@ -55,45 +60,16 @@ class OfflineManager extends GirafeSingleton {
55
60
  const tileUrls = this.getAllTileUrls(bbox, wmtsLayers);
56
61
  await this.fetchAndSaveTiles(tileUrls);
57
62
  }
58
- async openIndexedDB() {
59
- return new Promise((resolve, reject) => {
60
- console.debug('Opening IndexedDB');
61
- const request = indexedDB.open(this.dbCacheName, this.storeVersion);
62
- let timedOut = false;
63
- const timeoutId = setTimeout(() => {
64
- timedOut = true;
65
- console.debug('Timeout while opening IndexedDB');
66
- reject(new Error('Timeout while opening IndexedDB'));
67
- }, 3000);
68
- request.onerror = (event) => {
69
- if (!timedOut) {
70
- clearTimeout(timeoutId);
71
- const message = event.target.error?.message;
72
- console.debug(`IndexedDB could not be opened : ${message}`);
73
- reject(new Error(message));
74
- }
75
- };
76
- request.onsuccess = (event) => {
77
- if (!timedOut) {
78
- clearTimeout(timeoutId);
79
- console.debug('IndexedDB is open');
80
- resolve(event.target.result);
81
- }
82
- };
83
- request.onupgradeneeded = (event) => {
84
- // Version migration is necessary.
85
- // open the indexedDB and create the new structure
86
- console.debug('Upgrading IndexedDB');
87
- const database = event.target.result;
88
- // First : a store for tiles
89
- const tilesStore = database.createObjectStore('tiles', { autoIncrement: true });
90
- tilesStore.createIndex('url', 'url', { unique: true });
91
- // Second : A store for offline available bbox
92
- database.createObjectStore('bbox', { autoIncrement: true });
93
- console.debug('IndexedDB upgraded.');
94
- resolve(database);
95
- };
96
- });
63
+ upgradeIndexedDb(database) {
64
+ // Version migration is necessary.
65
+ // open the indexedDB and create the new structure
66
+ console.debug('Upgrading IndexedDB');
67
+ // First : a store for tiles
68
+ const tilesStore = database.createObjectStore('tiles', { autoIncrement: true });
69
+ tilesStore.createIndex('url', 'url', { unique: true });
70
+ // Second : A store for offline available bbox
71
+ database.createObjectStore('bbox', { autoIncrement: true });
72
+ console.debug('IndexedDB upgraded.');
97
73
  }
98
74
  /** The offline manager works with a ServiceWorker in charge of intercepting
99
75
  * the fetch requests and read the data from the local cache if the application
@@ -108,7 +84,6 @@ class OfflineManager extends GirafeSingleton {
108
84
  this.serviceWorker = sw;
109
85
  this.storeVersion = storeVersion;
110
86
  this.dbCacheName = dbCacheName;
111
- this.database = await this.openIndexedDB();
112
87
  this.serviceWorker.postMessage({
113
88
  storeVersion: this.storeVersion,
114
89
  dbCacheName: this.dbCacheName,
@@ -165,9 +140,7 @@ class OfflineManager extends GirafeSingleton {
165
140
  const iterator = tileUrls.values();
166
141
  // Use 4 parallel workers to download tiles
167
142
  this.counter = 0;
168
- const workers = Array(4)
169
- .fill(iterator)
170
- .map((iterator) => this.doWork(iterator));
143
+ const workers = new Array(4).fill(iterator).map((iterator) => this.doWork(iterator));
171
144
  Promise.allSettled(workers).then(() => {
172
145
  console.debug('Everything downloaded.');
173
146
  if (this.progressCallback) {
@@ -180,41 +153,41 @@ class OfflineManager extends GirafeSingleton {
180
153
  for (const url of iterator) {
181
154
  const response = await fetch(url);
182
155
  if (response.ok) {
183
- response.blob().then((blob) => {
184
- const transaction = this.database.transaction([this.tilesStoreName], 'readwrite');
185
- const store = transaction.objectStore(this.tilesStoreName);
186
- const index = store.index('url');
187
- const dbRequest = index.getKey(url);
188
- dbRequest.onsuccess = () => {
189
- let request;
190
- if (dbRequest.result) {
191
- const key = dbRequest.result;
192
- request = store.put({ url: url, data: blob }, key);
193
- }
194
- else {
195
- request = store.put({ url: url, data: blob });
156
+ const blob = await response.blob();
157
+ const transaction = (await this.database()).transaction([this.tilesStoreName], 'readwrite');
158
+ const store = transaction.objectStore(this.tilesStoreName);
159
+ const index = store.index('url');
160
+ const dbRequest = index.getKey(url);
161
+ dbRequest.onsuccess = () => {
162
+ let request;
163
+ if (dbRequest.result) {
164
+ const key = dbRequest.result;
165
+ request = store.put({ url: url, data: blob }, key);
166
+ }
167
+ else {
168
+ request = store.put({ url: url, data: blob });
169
+ }
170
+ request.onsuccess = () => {
171
+ if (this.progressCallback) {
172
+ this.progressCallback(Math.round((this.counter * 100) / this.totalLength));
196
173
  }
197
- request.onsuccess = () => {
198
- if (this.progressCallback) {
199
- this.progressCallback(Math.round((this.counter * 100) / this.totalLength));
200
- }
201
- console.debug(`${this.counter++}/${this.totalLength} Tile ${url} added.`);
202
- };
203
- request.onerror = () => {
204
- console.error(`Error while saving tile ${url}`);
205
- };
174
+ console.debug(`${this.counter++}/${this.totalLength} Tile ${url} added.`);
206
175
  };
207
- });
176
+ request.onerror = () => {
177
+ console.error(`Error while saving tile ${url}`);
178
+ };
179
+ };
208
180
  }
209
181
  }
210
182
  }
211
183
  async getTotalSizeMB() {
184
+ const database = await this.database();
212
185
  return new Promise((resolve, reject) => {
213
- if (!this.database) {
186
+ if (!database) {
214
187
  reject(new Error('Database is not initialized.'));
215
188
  return;
216
189
  }
217
- const transaction = this.database.transaction([this.tilesStoreName], 'readonly');
190
+ const transaction = database.transaction([this.tilesStoreName], 'readonly');
218
191
  const store = transaction.objectStore(this.tilesStoreName);
219
192
  const request = store.openCursor();
220
193
  let totalBytes = 0;
@@ -239,12 +212,13 @@ class OfflineManager extends GirafeSingleton {
239
212
  });
240
213
  }
241
214
  async clearStore(storeName) {
215
+ const database = await this.database();
242
216
  return new Promise((resolve, reject) => {
243
217
  if (!this.database) {
244
218
  reject(new Error('Database is not initialized.'));
245
219
  return;
246
220
  }
247
- const transaction = this.database.transaction([storeName], 'readwrite');
221
+ const transaction = database.transaction([storeName], 'readwrite');
248
222
  const store = transaction.objectStore(storeName);
249
223
  const request = store.clear();
250
224
  request.onsuccess = () => {
@@ -264,11 +238,9 @@ class OfflineManager extends GirafeSingleton {
264
238
  this.clearStore(this.bboxStoreName);
265
239
  }
266
240
  async saveBoundingBox(bbox) {
267
- if (this.database === undefined) {
268
- this.database = await this.openIndexedDB();
269
- }
241
+ const database = await this.database();
270
242
  // Save bbox to the store
271
- const transaction = this.database.transaction([this.bboxStoreName], 'readwrite');
243
+ const transaction = database.transaction([this.bboxStoreName], 'readwrite');
272
244
  const store = transaction.objectStore(this.bboxStoreName);
273
245
  const request = store.put(bbox);
274
246
  request.onsuccess = () => {
@@ -279,11 +251,9 @@ class OfflineManager extends GirafeSingleton {
279
251
  };
280
252
  }
281
253
  async displayBoundBoxes() {
282
- if (this.database === undefined) {
283
- this.database = await this.openIndexedDB();
284
- }
254
+ const database = await this.database();
285
255
  // Read bbox for offline tiles
286
- const transaction = this.database.transaction([this.bboxStoreName], 'readonly');
256
+ const transaction = database.transaction([this.bboxStoreName], 'readonly');
287
257
  const store = transaction.objectStore(this.bboxStoreName);
288
258
  const request = store.getAll();
289
259
  request.onsuccess = () => {
@@ -0,0 +1,42 @@
1
+ import Feature from 'ol/Feature.js';
2
+ import { GeoJSON } from 'ol/format.js';
3
+ import GirafeSingleton from '../../base/GirafeSingleton.js';
4
+ import IGirafeContext from '../context/icontext.js';
5
+ import { Extent } from 'ol/extent.js';
6
+ export declare const SEARCH_PROVIDER_DEFAULT = "default";
7
+ export declare const SEARCH_GROUP_DEFAULT = "default";
8
+ export declare const SEARCH_GROUP_RECENTER_MAP = "recenter_map";
9
+ declare abstract class AbstractSearchClient extends GirafeSingleton {
10
+ readonly type: string;
11
+ readonly providerName: string;
12
+ protected url: URL;
13
+ protected readonly resultSrid: string;
14
+ protected readonly searchTermPlaceholder = "###SEARCHTERM###";
15
+ protected readonly searchLangPlaceholder = "###SEARCHLANG###";
16
+ protected readonly geoJsonFormatter: GeoJSON<Feature<import("ol/geom.js").Geometry, {
17
+ [x: string]: any;
18
+ }>>;
19
+ protected constructor(context: IGirafeContext, type: string, name: string, url: string, resultSrid: string);
20
+ initializeSingleton(): void;
21
+ protected get mapProjection(): import("ol/proj.js").Projection;
22
+ requestSearch(term: string, abortController: AbortController): Promise<Feature[]>;
23
+ /**
24
+ * Parses the response from the server and returns an array of features.
25
+ * Features must have the following properties to be used correctly with the search component:
26
+ * - `provider`
27
+ * - `label`
28
+ * - `label_html`: optional, for styled entries in the search results list
29
+ * - `actions`: optional, a list of SearchResultsActions
30
+ */
31
+ protected parseResponse(response: Response): Promise<Feature[]>;
32
+ /**
33
+ * Groups results by the default group, which equivalates to no grouping. Should be overridden by the client class
34
+ * to group results by a custom criteria.
35
+ */
36
+ groupedResults(results: Feature[]): Record<string, Feature[]>;
37
+ /**
38
+ * Returns a bbox around the geometry that is 50% larger than the geometry.
39
+ */
40
+ getZoomToBbox(feature: Feature): Extent | undefined;
41
+ }
42
+ export default AbstractSearchClient;
@@ -0,0 +1,85 @@
1
+ import { GeoJSON } from 'ol/format.js';
2
+ import GirafeSingleton from '../../base/GirafeSingleton.js';
3
+ import { buffer, getHeight, getWidth } from 'ol/extent.js';
4
+ // The default provider is a container for search results that do not result from a provider search (e.g. map re-centering)
5
+ export const SEARCH_PROVIDER_DEFAULT = 'default';
6
+ // The default search group contains search results that do not belong to a specific category
7
+ export const SEARCH_GROUP_DEFAULT = 'default';
8
+ // The map recenter search group contains the coordinates for re-centering the map
9
+ export const SEARCH_GROUP_RECENTER_MAP = 'recenter_map';
10
+ class AbstractSearchClient extends GirafeSingleton {
11
+ type;
12
+ providerName;
13
+ url;
14
+ resultSrid;
15
+ searchTermPlaceholder = '###SEARCHTERM###';
16
+ searchLangPlaceholder = '###SEARCHLANG###';
17
+ geoJsonFormatter = new GeoJSON();
18
+ constructor(context, type, name, url, resultSrid) {
19
+ super(context);
20
+ this.type = type;
21
+ this.providerName = name;
22
+ this.url = new URL(url);
23
+ this.resultSrid = resultSrid;
24
+ }
25
+ initializeSingleton() {
26
+ super.initializeSingleton();
27
+ }
28
+ get mapProjection() {
29
+ return this.context.mapManager.getMap().getView().getProjection();
30
+ }
31
+ async requestSearch(term, abortController) {
32
+ const url = this.url
33
+ .toString()
34
+ .replace(this.searchTermPlaceholder, term)
35
+ .replace(this.searchLangPlaceholder, this.context.stateManager.state.language);
36
+ try {
37
+ const response = await fetch(url, { signal: abortController.signal });
38
+ return await this.parseResponse(response);
39
+ }
40
+ catch (error) {
41
+ console.error(`Error fetching search results from ${this.url.toString()}:`, error);
42
+ return [];
43
+ }
44
+ }
45
+ /**
46
+ * Parses the response from the server and returns an array of features.
47
+ * Features must have the following properties to be used correctly with the search component:
48
+ * - `provider`
49
+ * - `label`
50
+ * - `label_html`: optional, for styled entries in the search results list
51
+ * - `actions`: optional, a list of SearchResultsActions
52
+ */
53
+ async parseResponse(response) {
54
+ const data = (await response.json());
55
+ const features = this.geoJsonFormatter.readFeatures(data, {
56
+ dataProjection: this.resultSrid,
57
+ featureProjection: this.mapProjection
58
+ });
59
+ return features.map((f) => {
60
+ f.set('provider', this.providerName);
61
+ return f;
62
+ });
63
+ }
64
+ /**
65
+ * Groups results by the default group, which equivalates to no grouping. Should be overridden by the client class
66
+ * to group results by a custom criteria.
67
+ */
68
+ groupedResults(results) {
69
+ return { [SEARCH_GROUP_DEFAULT]: results };
70
+ }
71
+ /**
72
+ * Returns a bbox around the geometry that is 50% larger than the geometry.
73
+ */
74
+ getZoomToBbox(feature) {
75
+ const geom = feature.getGeometry();
76
+ if (geom) {
77
+ const extent = geom.getExtent();
78
+ // We create a buffer around the extent from 50% of the width/height
79
+ const bufferValue = Math.max((getWidth(extent) * 50) / 100, (getHeight(extent) * 50) / 100);
80
+ return buffer(extent, bufferValue);
81
+ }
82
+ return undefined;
83
+ }
84
+ }
85
+ export default AbstractSearchClient;
@@ -0,0 +1,63 @@
1
+ import Feature from 'ol/Feature.js';
2
+ import AbstractSearchClient from './abstractsearchclient.js';
3
+ import IGirafeContext from '../context/icontext.js';
4
+ import { Extent } from 'ol/extent.js';
5
+ export type GeoAdminChSearchConfig = {
6
+ type: 'geoadminch';
7
+ providerName: string;
8
+ bbox?: string;
9
+ locationOrigins?: string;
10
+ };
11
+ /**
12
+ * Search provider for the Swiss search service of geo.admin.ch.
13
+ * See documentation at https://docs.geo.admin.ch/access-data/search.html
14
+ *
15
+ * The GeoAdmin search provider allows 3 different types of searches:
16
+ * - locations: POIs like addresses, swissnames, boundaries, parcels
17
+ * - layers: layers of the GeoAdmin portal
18
+ * - features: features inside one or multiple layers of the GeoAdmin portal
19
+ *
20
+ * This client currently only supports location search.
21
+ * The API returns max 50 items per request.
22
+ *
23
+ * Configuration:
24
+ * - providerName: The name of the search provider, or geoadminch.
25
+ * - resultSrid: The spatial reference system (SRID) of the search results, always EPSG:2056.
26
+ * - bbox: Comma-separated list of bounding box coordinates in EPSG:2056 (e.g. "2580000,1150000,2600000,1200000")
27
+ * If no bbox is set, the max extent of the map is used.
28
+ * - locationOrigins: Array of categories that are searched. Default: All categories.
29
+ * Available categories and data origin:
30
+ * - zipcode (ch.swisstopo-vd.ortschaftenverzeichnis_plz)
31
+ * - gg25 (ch.swisstopo.swissboundaries3d-gemeinde-flaeche.fill)
32
+ * - district (ch.swisstopo.swissboundaries3d-bezirk-flaeche.fill)
33
+ * - kantone (ch.swisstopo.swissboundaries3d-kanton-flaeche.fill)
34
+ * - gazetteer (ch.swisstopo.swissnames3d, ch.bav.haltestellen-oev)
35
+ * - address (ch.swisstopo.amtliches-gebaeudeadressverzeichnis, including EGID)
36
+ * - parcel
37
+ */
38
+ declare class GeoAdminChSearchClient extends AbstractSearchClient {
39
+ constructor(context: IGirafeContext, config: GeoAdminChSearchConfig);
40
+ private setSearchExtent;
41
+ requestSearch(term: string, abortController: AbortController): Promise<Feature[]>;
42
+ protected parseResponse(response: Response): Promise<Feature[]>;
43
+ /**
44
+ * GeoAdmin geoJson responses contain coordinates as [north, east], but the GeoJson standard is [east, north].
45
+ * To be able to use the ol GeoJson format reader, the coordinates are rearranged.
46
+ * Note that other spatial information in the GeoAdmin response is valid, e.g. the bbox has the correct order.
47
+ */
48
+ private fixGeoAdminCoordinates;
49
+ /**
50
+ * Swap first (north) and second (east) coordinate in an array of coordinates.
51
+ */
52
+ private swapCoordinates;
53
+ /**
54
+ * Property origin contains the result category:
55
+ */
56
+ groupedResults(results: Feature[]): Record<string, Feature[]>;
57
+ getZoomToBbox(feature: Feature): Extent | undefined;
58
+ /**
59
+ * Remove bold and italic HTML tags from the label
60
+ */
61
+ private sanitizeHtmlLabel;
62
+ }
63
+ export default GeoAdminChSearchClient;
@@ -0,0 +1,147 @@
1
+ import AbstractSearchClient from './abstractsearchclient.js';
2
+ import { transformExtent } from 'ol/proj.js';
3
+ /**
4
+ * Search provider for the Swiss search service of geo.admin.ch.
5
+ * See documentation at https://docs.geo.admin.ch/access-data/search.html
6
+ *
7
+ * The GeoAdmin search provider allows 3 different types of searches:
8
+ * - locations: POIs like addresses, swissnames, boundaries, parcels
9
+ * - layers: layers of the GeoAdmin portal
10
+ * - features: features inside one or multiple layers of the GeoAdmin portal
11
+ *
12
+ * This client currently only supports location search.
13
+ * The API returns max 50 items per request.
14
+ *
15
+ * Configuration:
16
+ * - providerName: The name of the search provider, or geoadminch.
17
+ * - resultSrid: The spatial reference system (SRID) of the search results, always EPSG:2056.
18
+ * - bbox: Comma-separated list of bounding box coordinates in EPSG:2056 (e.g. "2580000,1150000,2600000,1200000")
19
+ * If no bbox is set, the max extent of the map is used.
20
+ * - locationOrigins: Array of categories that are searched. Default: All categories.
21
+ * Available categories and data origin:
22
+ * - zipcode (ch.swisstopo-vd.ortschaftenverzeichnis_plz)
23
+ * - gg25 (ch.swisstopo.swissboundaries3d-gemeinde-flaeche.fill)
24
+ * - district (ch.swisstopo.swissboundaries3d-bezirk-flaeche.fill)
25
+ * - kantone (ch.swisstopo.swissboundaries3d-kanton-flaeche.fill)
26
+ * - gazetteer (ch.swisstopo.swissnames3d, ch.bav.haltestellen-oev)
27
+ * - address (ch.swisstopo.amtliches-gebaeudeadressverzeichnis, including EGID)
28
+ * - parcel
29
+ */
30
+ class GeoAdminChSearchClient extends AbstractSearchClient {
31
+ constructor(context, config) {
32
+ super(context, 'geoadminch', config.providerName, 'https://api3.geo.admin.ch/rest/services/api/SearchServer', 'EPSG:2056');
33
+ this.url.searchParams.set('type', 'locations');
34
+ this.url.searchParams.set('sr', this.resultSrid.replace('EPSG:', ''));
35
+ this.url.searchParams.set('geometryFormat', 'geojson');
36
+ this.url.searchParams.set('sortbox', 'false');
37
+ if (config.locationOrigins) {
38
+ this.url.searchParams.set('origins', config.locationOrigins);
39
+ }
40
+ this.setSearchExtent(config.bbox);
41
+ }
42
+ setSearchExtent(bbox) {
43
+ let searchExtent;
44
+ if (bbox) {
45
+ searchExtent = bbox;
46
+ }
47
+ else {
48
+ // If no bbox is configured, set the search extent to the max map extent
49
+ searchExtent = this.context.configManager.Config.map.maxExtent;
50
+ }
51
+ if (!searchExtent) {
52
+ return;
53
+ }
54
+ // If the default map extent isn't specified in EPSG:2056, transform it
55
+ const defaultSrid = this.context.configManager.getDefaultConfigValue('map.srid');
56
+ if (defaultSrid !== this.resultSrid) {
57
+ const extent = searchExtent.split(',').map(Number);
58
+ searchExtent = transformExtent(extent, defaultSrid, this.resultSrid).join(',');
59
+ }
60
+ this.url.searchParams.set('bbox', searchExtent);
61
+ }
62
+ async requestSearch(term, abortController) {
63
+ try {
64
+ const url = new URL(this.url);
65
+ url.searchParams.set('searchText', term);
66
+ url.searchParams.set('lang', this.context.stateManager.state.language);
67
+ const response = await fetch(url, { signal: abortController.signal });
68
+ return await this.parseResponse(response);
69
+ }
70
+ catch (error) {
71
+ console.error('Error fetching search results from GeoAdminSearch:', error);
72
+ return new Promise(() => []);
73
+ }
74
+ }
75
+ async parseResponse(response) {
76
+ const data = this.fixGeoAdminCoordinates((await response.json()));
77
+ const features = this.geoJsonFormatter.readFeatures(data, {
78
+ dataProjection: this.resultSrid,
79
+ featureProjection: this.mapProjection
80
+ });
81
+ // Property `label` contains an HTML string. Rename `label` to `label_html` to use in the dropdown,
82
+ // and sanitize `label` to be used in the input field
83
+ return features.map((f) => {
84
+ f.set('provider', this.providerName);
85
+ f.set('label_html', f.get('label'));
86
+ f.set('label', this.sanitizeHtmlLabel(f.get('label')));
87
+ return f;
88
+ });
89
+ }
90
+ /**
91
+ * GeoAdmin geoJson responses contain coordinates as [north, east], but the GeoJson standard is [east, north].
92
+ * To be able to use the ol GeoJson format reader, the coordinates are rearranged.
93
+ * Note that other spatial information in the GeoAdmin response is valid, e.g. the bbox has the correct order.
94
+ */
95
+ fixGeoAdminCoordinates(data) {
96
+ if ('features' in data && Array.isArray(data.features)) {
97
+ for (const feature of data.features) {
98
+ if (feature.geometry) {
99
+ feature.geometry.coordinates = this.swapCoordinates(feature.geometry.coordinates);
100
+ }
101
+ }
102
+ }
103
+ return data;
104
+ }
105
+ /**
106
+ * Swap first (north) and second (east) coordinate in an array of coordinates.
107
+ */
108
+ swapCoordinates(coordinates) {
109
+ if (Array.isArray(coordinates) &&
110
+ coordinates.length >= 2 &&
111
+ typeof coordinates[0] === 'number' &&
112
+ typeof coordinates[1] === 'number' &&
113
+ coordinates[0] < coordinates[1]) {
114
+ const pos = coordinates;
115
+ return [pos[1], pos[0], ...pos.slice(2)];
116
+ }
117
+ return coordinates.map(this.swapCoordinates);
118
+ }
119
+ /**
120
+ * Property origin contains the result category:
121
+ */
122
+ groupedResults(results) {
123
+ const groupedResults = {};
124
+ for (const result of results) {
125
+ const group = `geoadminch_${result.get('origin') ?? 'Other'}`;
126
+ groupedResults[group] ??= [];
127
+ groupedResults[group].push(result);
128
+ }
129
+ return groupedResults;
130
+ }
131
+ getZoomToBbox(feature) {
132
+ const st_bbox = feature.get('geom_st_box2d');
133
+ if (st_bbox) {
134
+ const bbox_coords = st_bbox.replace('BOX(', '').replace(')', '').replaceAll(' ', ',');
135
+ const extent = bbox_coords.split(',').map((coord) => Number.parseFloat(coord));
136
+ return transformExtent(extent, this.resultSrid, this.mapProjection);
137
+ }
138
+ return undefined;
139
+ }
140
+ /**
141
+ * Remove bold and italic HTML tags from the label
142
+ */
143
+ sanitizeHtmlLabel(label) {
144
+ return label.replace(/<\/?[ib]>/g, '');
145
+ }
146
+ }
147
+ export default GeoAdminChSearchClient;
@@ -0,0 +1,23 @@
1
+ import IGirafeContext from '../context/icontext.js';
2
+ import Feature from 'ol/Feature.js';
3
+ import AbstractSearchClient from './abstractsearchclient.js';
4
+ export type GmfSearchConfig = {
5
+ type: 'geomapfish';
6
+ providerName: string;
7
+ url: string;
8
+ resultSrid: string;
9
+ };
10
+ /**
11
+ * A search provider for the GeoMapFish backend.
12
+ * Extends the AbstractSearchClient and provides methods tailored to processing GMF-specific search results.
13
+ *
14
+ * Configuration:
15
+ * - providerName: The name of the search provider, or geomapfish.
16
+ * - url: The URL of the GMF search service.
17
+ * - resultSrid: The spatial reference system (SRID) of the search results. Default same as map SRID.
18
+ */
19
+ declare class GmfSearchClient extends AbstractSearchClient {
20
+ constructor(context: IGirafeContext, config: GmfSearchConfig);
21
+ groupedResults(results: Feature[]): Record<string, Feature[]>;
22
+ }
23
+ export default GmfSearchClient;
@@ -0,0 +1,37 @@
1
+ import AbstractSearchClient from './abstractsearchclient.js';
2
+ /**
3
+ * A search provider for the GeoMapFish backend.
4
+ * Extends the AbstractSearchClient and provides methods tailored to processing GMF-specific search results.
5
+ *
6
+ * Configuration:
7
+ * - providerName: The name of the search provider, or geomapfish.
8
+ * - url: The URL of the GMF search service.
9
+ * - resultSrid: The spatial reference system (SRID) of the search results. Default same as map SRID.
10
+ */
11
+ class GmfSearchClient extends AbstractSearchClient {
12
+ constructor(context, config) {
13
+ super(context, 'geomapfish', config.providerName, config.url, config.resultSrid);
14
+ }
15
+ groupedResults(results) {
16
+ const groupedResults = {};
17
+ for (const result of results) {
18
+ let group = 'Unknown layer type';
19
+ if (result.get('layer_name')) {
20
+ group = result.get('layer_name');
21
+ }
22
+ else if (result.get('actions')[0].action.startsWith('add_theme')) {
23
+ group = 'add_theme';
24
+ }
25
+ else if (result.get('actions')[0].action.startsWith('add_group')) {
26
+ group = 'add_group';
27
+ }
28
+ else if (result.get('actions')[0].action.startsWith('add_layer')) {
29
+ group = 'add_layer';
30
+ }
31
+ groupedResults[group] ??= [];
32
+ groupedResults[group].push(result);
33
+ }
34
+ return groupedResults;
35
+ }
36
+ }
37
+ export default GmfSearchClient;
@@ -0,0 +1,33 @@
1
+ import IGirafeContext from '../context/icontext.js';
2
+ import GirafeSingleton from '../../base/GirafeSingleton.js';
3
+ import Feature from 'ol/Feature.js';
4
+ import { Extent } from 'ol/extent.js';
5
+ export type SearchProviderConfig = {
6
+ type: 'geomapfish' | 'geoadminch';
7
+ providerName: string;
8
+ [key: string]: unknown;
9
+ };
10
+ declare class SearchManager extends GirafeSingleton {
11
+ private readonly clients;
12
+ private abortController;
13
+ readonly minSearchTermLength = 3;
14
+ constructor(context: IGirafeContext);
15
+ initializeSingleton(): void;
16
+ private createClient;
17
+ abortSearch(): void;
18
+ /**
19
+ * Performs a search operation with all available clients based on the provided term and returns results
20
+ * grouped by provider name and result category.
21
+ */
22
+ doSearch(term: string): Promise<Record<string, Record<string, Feature[]>> | null>;
23
+ private createCoordinateSearchResult;
24
+ /**
25
+ * Detects and parses a coordinate search term into an array of two numerical values.
26
+ * The method attempts to identify valid coordinate pairs from the given term,
27
+ * using different separators such as `;`, `/`, `,`, or whitespace.
28
+ */
29
+ detectCoordinatesInTerm(term: string): [number, number] | null;
30
+ private parseCoordinate;
31
+ getZoomToBbox(feature: Feature): Extent | undefined;
32
+ }
33
+ export default SearchManager;