@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.
@@ -5,6 +5,7 @@
5
5
 
6
6
  #searchbox {
7
7
  background-color: var(--bkg-color);
8
+ height: 3rem;
8
9
  border-right: none;
9
10
  border-radius: 4px;
10
11
  border: none;
@@ -71,23 +72,35 @@
71
72
  overflow-x: hidden;
72
73
  scrollbar-width: thin;
73
74
  bottom: 0;
74
- padding-top: 7vh;
75
+ /* padding-top = top-position of search field + height of search field + gap */
76
+ padding-top: calc(max(2.5em, calc(env(safe-area-inset-top) + 0.5rem)) + 3rem + 1rem);
75
77
  }
76
78
 
77
- .result,
78
- .title {
79
+ .entry {
79
80
  display: inline-block;
80
81
  padding: 0.3rem 1rem;
81
- width: Calc(36rem - 2rem);
82
+ width: calc(36rem - 2rem);
82
83
  line-height: 1.3rem;
84
+ color: var(--text-color);
83
85
  }
84
86
 
85
- .title {
86
- color: var(--text-color);
87
+ .provider,
88
+ .group {
87
89
  font-weight: 600;
88
- line-height: 2.5rem;
90
+ line-height: 3rem;
89
91
  font-size: 1.3rem;
90
- padding-top: 1.5rem;
92
+ }
93
+
94
+ .provider {
95
+ font-size: 1.5rem;
96
+ margin-top: 1rem;
97
+ background-color: color-mix(in srgb, var(--bkg-color-grad1) 75%, transparent);
98
+ border: 0 solid var(--bkg-color-grad2);
99
+ border-width: 1px 0;
100
+ }
101
+
102
+ #results > .provider:first-child {
103
+ margin-top: 0;
91
104
  }
92
105
 
93
106
  .result {
@@ -97,20 +110,19 @@
97
110
  background: transparent;
98
111
  text-align: left;
99
112
  width: 100%;
100
- color: var(--text-color);
101
113
  }
102
114
 
103
- .title img {
115
+ .entry img {
104
116
  margin-right: 1rem;
105
- width: 14px;
117
+ width: 0.8em;
106
118
  }
107
119
 
108
- .title span {
120
+ .entry span {
109
121
  text-transform: uppercase;
110
122
  }
111
123
 
112
124
  .result.selected {
113
- background-color: #aaa !important;
125
+ background-color: var(--bkg-color-grad2) !important;
114
126
  color: var(--text-color);
115
127
  }
116
128
 
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "GeoGirafe PSC",
6
6
  "url": "https://doc.geogirafe.org"
7
7
  },
8
- "version": "1.1.0-dev.2610902439",
8
+ "version": "1.1.0-dev.2625220907",
9
9
  "type": "module",
10
10
  "engines": {
11
11
  "node": ">=20.19.0"
@@ -1 +1 @@
1
- {"version":"1.1.0-dev.2610902439", "build":"2610902439", "date":"18/06/2026"}
1
+ {"version":"1.1.0-dev.2625220907", "build":"2625220907", "date":"24/06/2026"}
@@ -88,7 +88,16 @@ export default class OpenIdConnectManager extends AbstractConnectManager {
88
88
  authorizationUrl.searchParams.set('code_challenge', code_challenge);
89
89
  authorizationUrl.searchParams.set('code_challenge_method', this.issuerConfig.codeChallengeMethod);
90
90
  if (silent) {
91
- authorizationUrl.searchParams.set('prompt', 'none');
91
+ // Add custom parameters for silent login
92
+ for (const [key, value] of Object.entries(this.issuerConfig.customSilentLoginParams)) {
93
+ authorizationUrl.searchParams.set(key, value);
94
+ }
95
+ }
96
+ else {
97
+ // Add custom parameters for login
98
+ for (const [key, value] of Object.entries(this.issuerConfig.customLoginParams)) {
99
+ authorizationUrl.searchParams.set(key, value);
100
+ }
92
101
  }
93
102
  return authorizationUrl;
94
103
  }
@@ -43,8 +43,17 @@ declare class GirafeConfig {
43
43
  };
44
44
  };
45
45
  search: {
46
- url: string;
47
- resultsSrid: string;
46
+ providers: ({
47
+ type: 'geomapfish';
48
+ url: string;
49
+ resultSrid: string;
50
+ providerName?: string;
51
+ } | {
52
+ type: 'geoadminch';
53
+ bbox?: string;
54
+ locationOrigins?: string;
55
+ providerName?: string;
56
+ })[];
48
57
  objectPreview?: boolean;
49
58
  layerPreview?: boolean;
50
59
  minResolution?: number;
@@ -207,6 +216,12 @@ declare class GirafeConfig {
207
216
  audience: string[];
208
217
  audienceExcludedPaths: string[];
209
218
  alwaysSendCookies: boolean;
219
+ customSilentLoginParams: {
220
+ [key: string]: string;
221
+ };
222
+ customLoginParams: {
223
+ [key: string]: string;
224
+ };
210
225
  };
211
226
  geomapfish: {
212
227
  userInfoUrl: string;
@@ -87,10 +87,7 @@ class GirafeConfig {
87
87
  // The application can be started even if the search is not correctly configured
88
88
  // We just display a warning in the console
89
89
  console.warn(e);
90
- this.search = {
91
- url: '',
92
- resultsSrid: config.map.srid
93
- };
90
+ this.search = { providers: [{ type: 'geomapfish', url: '', resultSrid: this.map.srid }] };
94
91
  }
95
92
  try {
96
93
  this.share = this.initConfigShare(config);
@@ -211,15 +208,36 @@ class GirafeConfig {
211
208
  return config.print;
212
209
  }
213
210
  initConfigSearch(config) {
214
- if (!config.search?.url) {
215
- throw new Error(`Configuration for search.url is required. See https://doc.geogirafe.org/docs/configuration`);
216
- }
217
- if (!config.search.url.includes('###SEARCHTERM###')) {
218
- throw new Error(`search.url is missing the expected pattern. See https://doc.geogirafe.org/docs/configuration`);
211
+ if (!config.search) {
212
+ throw new Error('Configuration for search is required. See https://doc.geogirafe.org/docs/configuration');
213
+ }
214
+ const providers = [];
215
+ for (const provider of config.search.providers ?? []) {
216
+ if (provider.type === 'geomapfish') {
217
+ if (!provider.url.includes('###SEARCHTERM###')) {
218
+ throw new Error(`Search provider url of type "geomapfish" is missing the expected pattern. See https://doc.geogirafe.org/docs/configuration`);
219
+ }
220
+ providers.push({
221
+ type: provider.type,
222
+ providerName: provider.providerName ?? provider.type,
223
+ url: provider.url,
224
+ resultSrid: provider.resultSrid ?? config.map.srid
225
+ });
226
+ }
227
+ else if (provider.type === 'geoadminch') {
228
+ providers.push({
229
+ type: provider.type,
230
+ providerName: provider.providerName ?? provider.type,
231
+ bbox: provider.bbox,
232
+ locationOrigins: provider.locationOrigins
233
+ });
234
+ }
235
+ else {
236
+ console.warn(`Provider type is missing or not valid: ${provider['type']}`);
237
+ }
219
238
  }
220
239
  return {
221
- url: config.search.url,
222
- resultsSrid: config.search.resultsSrid ?? config.map.srid,
240
+ providers: providers,
223
241
  objectPreview: config.search.objectPreview ?? false,
224
242
  layerPreview: config.search.layerPreview ?? false,
225
243
  minResolution: config.search.minResolution ?? 0.5,
@@ -390,7 +408,9 @@ class GirafeConfig {
390
408
  checkSessionOnLoad: config.oauth.issuer.checkSessionOnLoad ?? false,
391
409
  audience: config.oauth.issuer.audience,
392
410
  audienceExcludedPaths: config.oauth.issuer.audienceExcludedPaths ?? [],
393
- alwaysSendCookies: config.oauth.issuer.alwaysSendCookies ?? false
411
+ alwaysSendCookies: config.oauth.issuer.alwaysSendCookies ?? false,
412
+ customSilentLoginParams: config.oauth.issuer.customSilentLoginParams ?? {},
413
+ customLoginParams: config.oauth.issuer.customLoginParams ?? {}
394
414
  };
395
415
  const geomapfishConfig = {
396
416
  userInfoUrl: config.oauth.geomapfish.userInfoUrl,
@@ -31,6 +31,7 @@ import WfsManager from '../wfs/wfsmanager.js';
31
31
  import WmsManager from '../wms/wmsmanager.js';
32
32
  import IGirafeContext from './icontext.js';
33
33
  import ThemeFavoritesManager from '../themes/themefavoritesmanager.js';
34
+ import SearchManager from '../search/searchmanager.js';
34
35
  import FeedbackManager from '../feedback/feedbackmanager.js';
35
36
  export default class GirafeContext implements IGirafeContext {
36
37
  readonly userDataManager: UserDataManager;
@@ -65,6 +66,7 @@ export default class GirafeContext 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>;
@@ -31,6 +31,7 @@ import UserDataManager from '../userdata/userdatamanager.js';
31
31
  import WfsManager from '../wfs/wfsmanager.js';
32
32
  import WmsManager from '../wms/wmsmanager.js';
33
33
  import ThemeFavoritesManager from '../themes/themefavoritesmanager.js';
34
+ import SearchManager from '../search/searchmanager.js';
34
35
  import FeedbackManager from '../feedback/feedbackmanager.js';
35
36
  export default class GirafeContext {
36
37
  userDataManager;
@@ -65,6 +66,7 @@ export default class GirafeContext {
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 GirafeContext {
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 GirafeContext {
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
  }
@@ -30,6 +30,7 @@ import UserDataManager from '../userdata/userdatamanager.js';
30
30
  import WfsManager from '../wfs/wfsmanager.js';
31
31
  import WmsManager from '../wms/wmsmanager.js';
32
32
  import ThemeFavoritesManager from '../themes/themefavoritesmanager.js';
33
+ import SearchManager from '../search/searchmanager.js';
33
34
  import FeedbackManager from '../feedback/feedbackmanager.js';
34
35
  export default interface IGirafeContext {
35
36
  readonly userDataManager: UserDataManager;
@@ -64,6 +65,7 @@ export default interface IGirafeContext {
64
65
  readonly localFileManager: LocalFileManager;
65
66
  readonly onBoardingManager: OnBoardingManager;
66
67
  readonly themeFavoritesManager: ThemeFavoritesManager;
68
+ readonly searchManager: SearchManager;
67
69
  readonly feedbackManager: FeedbackManager;
68
70
  initialize(): Promise<void>;
69
71
  }
package/tools/main.d.ts CHANGED
@@ -58,6 +58,13 @@ export { default as OrderingManager, LayerTreeStartOrder } from './ordering/orde
58
58
  export type { GeoTransform } from './raster/rasterutils.js';
59
59
  export { extractGeoTransform, mapToPixel, pixelToMap, getImage, getPixelValue } from './raster/rasterutils.js';
60
60
  export { default as ResizeWindow } from './resizewindow.js';
61
+ export { default as AbstractSearchClient, SEARCH_PROVIDER_DEFAULT, SEARCH_GROUP_DEFAULT, SEARCH_GROUP_RECENTER_MAP } from './search/abstractsearchclient.js';
62
+ export type { GeoAdminChSearchConfig } from './search/geoadminchsearchclient.js';
63
+ export { default as GeoAdminChSearchClient } from './search/geoadminchsearchclient.js';
64
+ export type { GmfSearchConfig } from './search/gmfsearchclient.js';
65
+ export { default as GmfSearchClient } from './search/gmfsearchclient.js';
66
+ export type { SearchProviderConfig } from './search/searchmanager.js';
67
+ export { default as SearchManager } from './search/searchmanager.js';
61
68
  export type { default as ISessionManager } from './share/isessionmanager.js';
62
69
  export type { UrlShortenerResponse, IUrlShortener } from './share/iurlshortener.js';
63
70
  export { default as ActiveBasemapsSerializer } from './share/serializers/activebasemapsserializer.js';
package/tools/main.js CHANGED
@@ -48,6 +48,10 @@ export { default as OnBoardingManager } from './onboarding/onboardingmanager.js'
48
48
  export { default as OrderingManager, LayerTreeStartOrder } from './ordering/orderingmanager.js';
49
49
  export { extractGeoTransform, mapToPixel, pixelToMap, getImage, getPixelValue } from './raster/rasterutils.js';
50
50
  export { default as ResizeWindow } from './resizewindow.js';
51
+ export { default as AbstractSearchClient, SEARCH_PROVIDER_DEFAULT, SEARCH_GROUP_DEFAULT, SEARCH_GROUP_RECENTER_MAP } from './search/abstractsearchclient.js';
52
+ export { default as GeoAdminChSearchClient } from './search/geoadminchsearchclient.js';
53
+ export { default as GmfSearchClient } from './search/gmfsearchclient.js';
54
+ export { default as SearchManager } from './search/searchmanager.js';
51
55
  export { default as ActiveBasemapsSerializer } from './share/serializers/activebasemapsserializer.js';
52
56
  export { default as CustomLayersSerializer } from './share/serializers/customlayersserializer.js';
53
57
  export { default as GlobeSerializer } from './share/serializers/globeserializer.js';
@@ -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;