@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.
- package/api/apicontext.d.ts +2 -0
- package/api/apicontext.js +4 -0
- package/assets/i18n/de.json +8 -0
- package/assets/i18n/en.json +8 -0
- package/assets/i18n/fr.json +8 -0
- package/assets/i18n/it.json +8 -0
- package/components/getdirections/component.js +1 -0
- package/components/search/component.d.ts +6 -15
- package/components/search/component.js +71 -112
- package/components/search/style.css +21 -13
- package/components/search-mobile/component.js +2 -2
- package/components/search-mobile/style.css +25 -13
- package/components/treeview/treeviewroot/style.css +1 -1
- package/package.json +2 -1
- package/templates/public/about.json +1 -1
- package/templates/tsconfig.json +1 -1
- package/templates/vite.config.js +1 -1
- package/tools/app/geogirafeapp.js +15 -6
- package/tools/configuration/girafeconfig.d.ts +13 -2
- package/tools/configuration/girafeconfig.js +32 -12
- package/tools/context/context.d.ts +2 -0
- package/tools/context/context.js +4 -0
- package/tools/context/icontext.d.ts +2 -0
- package/tools/i18n/i18nmanager.js +1 -1
- package/tools/main.d.ts +9 -0
- package/tools/main.js +5 -0
- package/tools/offline/offlinemanager.d.ts +5 -4
- package/tools/offline/offlinemanager.js +50 -80
- package/tools/search/abstractsearchclient.d.ts +42 -0
- package/tools/search/abstractsearchclient.js +85 -0
- package/tools/search/geoadminchsearchclient.d.ts +63 -0
- package/tools/search/geoadminchsearchclient.js +147 -0
- package/tools/search/gmfsearchclient.d.ts +23 -0
- package/tools/search/gmfsearchclient.js +37 -0
- package/tools/search/searchmanager.d.ts +33 -0
- package/tools/search/searchmanager.js +130 -0
- package/tools/sw/service-worker.d.ts +1 -0
- package/tools/sw/service-worker.js +94 -0
- package/tools/sw/service-worker.tools.d.ts +83 -0
- package/tools/sw/service-worker.tools.js +321 -0
- package/tools/tests/mockconfig.d.ts +5 -2
- package/tools/tests/mockconfig.js +2 -2
- package/tools/tests/mockcontext.d.ts +2 -0
- package/tools/tests/mockcontext.js +4 -0
- 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
|
-
|
|
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
|
-
|
|
46
|
+
globalThis.addEventListener('offline', () => {
|
|
42
47
|
this.state.isOffline = true;
|
|
43
48
|
});
|
|
44
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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()
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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 (!
|
|
186
|
+
if (!database) {
|
|
214
187
|
reject(new Error('Database is not initialized.'));
|
|
215
188
|
return;
|
|
216
189
|
}
|
|
217
|
-
const transaction =
|
|
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 =
|
|
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
|
-
|
|
268
|
-
this.database = await this.openIndexedDB();
|
|
269
|
-
}
|
|
241
|
+
const database = await this.database();
|
|
270
242
|
// Save bbox to the store
|
|
271
|
-
const transaction =
|
|
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
|
-
|
|
283
|
-
this.database = await this.openIndexedDB();
|
|
284
|
-
}
|
|
254
|
+
const database = await this.database();
|
|
285
255
|
// Read bbox for offline tiles
|
|
286
|
-
const transaction =
|
|
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;
|