@geogirafe/lib-geoportal 1.2.0-dev.2829697690 → 1.2.0-dev.2836809138

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 (96) hide show
  1. package/api/apigeogirafeapp.js +1 -1
  2. package/assets/i18n/de.json +10 -1
  3. package/assets/i18n/en.json +10 -1
  4. package/assets/i18n/fr.json +10 -1
  5. package/assets/i18n/it.json +10 -1
  6. package/components/about/component.js +1 -1
  7. package/components/addbookmark/component.js +1 -1
  8. package/components/advancedfilter/component.js +1 -1
  9. package/components/alignnorthbutton-mobile/component.js +1 -1
  10. package/components/auth/component.js +1 -1
  11. package/components/basemap/component.js +1 -1
  12. package/components/contact/component.js +1 -1
  13. package/components/context-menu/custom-context-menu/component.js +1 -1
  14. package/components/context-menu/default-context-menu/component.js +1 -1
  15. package/components/coordinate/component.js +1 -1
  16. package/components/cross-section/cross-section-settings/component.js +1 -1
  17. package/components/cross-section/cross-section-viewer/component.js +1 -1
  18. package/components/displaymenubutton-mobile/component.js +1 -1
  19. package/components/displayselectorbutton-mobile/component.js +1 -1
  20. package/components/drawing/component-mobile.js +1 -1
  21. package/components/drawing/component.js +1 -1
  22. package/components/drawing/fixed-dimension/component.js +1 -1
  23. package/components/drawing-container-mobile/component.js +1 -1
  24. package/components/drawing-toolbar/component.js +1 -1
  25. package/components/edit/component.js +1 -1
  26. package/components/edit/editform/component.js +1 -1
  27. package/components/extlayers/component.js +1 -1
  28. package/components/feedbackbutton/component.js +1 -1
  29. package/components/geolocation-mobile/component.js +1 -1
  30. package/components/getdirections/component.js +1 -1
  31. package/components/help/component.js +1 -1
  32. package/components/infobox/component.js +1 -1
  33. package/components/infowindow/component.js +1 -1
  34. package/components/layout/component.js +1 -1
  35. package/components/lr-panel/component.js +1 -1
  36. package/components/main.d.ts +1 -0
  37. package/components/main.js +1 -0
  38. package/components/map-2d/component.js +1 -1
  39. package/components/map-3d/component.js +1 -1
  40. package/components/menu-mobile/component.js +1 -1
  41. package/components/menubutton/component.js +1 -1
  42. package/components/modals/component.js +1 -1
  43. package/components/navigation/component.js +1 -1
  44. package/components/news/news-button/component.js +1 -1
  45. package/components/news/news-panel/component.js +1 -1
  46. package/components/offline/component.js +1 -1
  47. package/components/print/component.js +1 -1
  48. package/components/prototypebanner/component.js +1 -1
  49. package/components/querybuilder/component.js +1 -1
  50. package/components/scale/component.js +1 -1
  51. package/components/search/component.d.ts +9 -0
  52. package/components/search/component.js +94 -25
  53. package/components/search/style.css +14 -4
  54. package/components/search-filter/component.d.ts +76 -0
  55. package/components/search-filter/component.js +388 -0
  56. package/components/search-filter/style.css +226 -0
  57. package/components/selectiongrid/component.js +1 -1
  58. package/components/selectionpanel-mobile/component.js +1 -1
  59. package/components/selectiontool/component.js +1 -1
  60. package/components/selectionwindow/component.js +1 -1
  61. package/components/share/component.js +1 -1
  62. package/components/share-mobile/component.js +1 -1
  63. package/components/swipe-up-panel-mobile/component.js +1 -1
  64. package/components/themes/component.js +1 -1
  65. package/components/timerestriction/component.js +1 -1
  66. package/components/timerestriction/timepicker/component.js +1 -1
  67. package/components/timerestriction/timeslider/component.js +1 -1
  68. package/components/treeview/treeviewgroup/component.js +1 -1
  69. package/components/treeview/treeviewitem/component.js +1 -1
  70. package/components/treeview/treeviewroot/component.js +1 -1
  71. package/components/treeview/treeviewtheme/component.js +1 -1
  72. package/components/userpreferences/component.js +1 -1
  73. package/components/videorecord/component.js +1 -1
  74. package/package.json +1 -1
  75. package/styles/common.css +1 -0
  76. package/styles/index.css +7 -4
  77. package/templates/index.html +7 -1
  78. package/templates/public/about.json +1 -1
  79. package/tools/app/geogirafeapp.js +2 -0
  80. package/tools/configuration/girafeconfig.d.ts +3 -0
  81. package/tools/configuration/girafeconfig.js +3 -1
  82. package/tools/main.d.ts +5 -4
  83. package/tools/search/abstractsearchclient.d.ts +32 -4
  84. package/tools/search/abstractsearchclient.js +68 -13
  85. package/tools/search/geoadminchsearchclient.d.ts +13 -6
  86. package/tools/search/geoadminchsearchclient.js +25 -15
  87. package/tools/search/gmfsearchclient.d.ts +18 -6
  88. package/tools/search/gmfsearchclient.js +59 -1
  89. package/tools/search/searchmanager.d.ts +19 -3
  90. package/tools/search/searchmanager.js +73 -10
  91. package/tools/state/graphicalInterface.d.ts +1 -0
  92. package/tools/state/graphicalInterface.js +1 -0
  93. package/tools/state/state.d.ts +8 -0
  94. package/tools/state/state.js +5 -0
  95. package/tools/tests/mockconfig.d.ts +9 -2
  96. package/tools/tests/mockconfig.js +5 -1
@@ -3,6 +3,12 @@ import { GeoJSON } from 'ol/format.js';
3
3
  import GirafeSingleton from '../../base/GirafeSingleton.js';
4
4
  import IGirafeContext from '../context/icontext.js';
5
5
  import { Extent } from 'ol/extent.js';
6
+ export type BaseSearchClientConfig = {
7
+ type: string;
8
+ providerName: string;
9
+ url: string;
10
+ resultSrid: string;
11
+ };
6
12
  export declare const SEARCH_PROVIDER_DEFAULT = "default";
7
13
  export declare const SEARCH_GROUP_DEFAULT = "default";
8
14
  export declare const SEARCH_GROUP_RECENTER_MAP = "recenter_map";
@@ -13,13 +19,31 @@ declare abstract class AbstractSearchClient extends GirafeSingleton {
13
19
  protected readonly resultSrid: string;
14
20
  protected readonly searchTermPlaceholder = "###SEARCHTERM###";
15
21
  protected readonly searchLangPlaceholder = "###SEARCHLANG###";
22
+ protected searchTermQueryParameter: string | undefined;
23
+ protected searchLangQueryParameter: string | undefined;
24
+ protected searchFilterQueryParameter: string | undefined;
16
25
  protected readonly geoJsonFormatter: GeoJSON<Feature<import("ol/geom.js").Geometry, {
17
26
  [x: string]: any;
18
27
  }>>;
19
- protected constructor(context: IGirafeContext, type: string, name: string, url: string, resultSrid: string);
28
+ protected constructor(context: IGirafeContext, config: BaseSearchClientConfig);
20
29
  initializeSingleton(): void;
21
30
  protected get mapProjection(): import("ol/proj.js").Projection;
22
- requestSearch(term: string, abortController: AbortController): Promise<Feature[]>;
31
+ /**
32
+ * Sanitize the url by removing placeholders that are wrongfully interpreted as URL fragments by the URL parser.
33
+ * The parameters are introduced again and set when fetching the search results.
34
+ */
35
+ private sanitizeUrl;
36
+ private extractQueryParamWithPlaceholder;
37
+ private sanitizePlaceholder;
38
+ /**
39
+ * Returns a list of categories that can be searched by.
40
+ */
41
+ getSearchCategories(): Promise<string[]>;
42
+ protected buildRequestUrl(term: string, filter: string[]): URL;
43
+ /**
44
+ * Executes a search request with the provided search term.
45
+ */
46
+ requestSearch(term: string, filter: string[], abortController: AbortController): Promise<Feature[]>;
23
47
  /**
24
48
  * Parses the response from the server and returns an array of features.
25
49
  * Features must have the following properties to be used correctly with the search component:
@@ -30,13 +54,17 @@ declare abstract class AbstractSearchClient extends GirafeSingleton {
30
54
  */
31
55
  protected parseResponse(response: Response): Promise<Feature[]>;
32
56
  /**
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.
57
+ * Groups the results by the default group, which equivalates to no grouping. Should be overridden by the client class
58
+ * to group results by a custom criteria like search category.
35
59
  */
36
60
  groupedResults(results: Feature[]): Record<string, Feature[]>;
37
61
  /**
38
62
  * Returns a bbox around the geometry that is 50% larger than the geometry.
39
63
  */
40
64
  getZoomToBbox(feature: Feature): Extent | undefined;
65
+ /**
66
+ * Retrieves the translation key for a given category. Default: use category string directly.
67
+ */
68
+ getI18nKeyForCategory(category: string): string;
41
69
  }
42
70
  export default AbstractSearchClient;
@@ -3,7 +3,7 @@ import GirafeSingleton from '../../base/GirafeSingleton.js';
3
3
  import { createBufferedExtentFromFeature } from '../geometrytools.js';
4
4
  // The default provider is a container for search results that do not result from a provider search (e.g. map re-centering)
5
5
  export const SEARCH_PROVIDER_DEFAULT = 'default';
6
- // The default search group contains search results that do not belong to a specific category
6
+ // The default search group contains search results that do not belong to a specific category, they are listed without a list header
7
7
  export const SEARCH_GROUP_DEFAULT = 'default';
8
8
  // The map recenter search group contains the coordinates for re-centering the map
9
9
  export const SEARCH_GROUP_RECENTER_MAP = 'recenter_map';
@@ -14,13 +14,16 @@ class AbstractSearchClient extends GirafeSingleton {
14
14
  resultSrid;
15
15
  searchTermPlaceholder = '###SEARCHTERM###';
16
16
  searchLangPlaceholder = '###SEARCHLANG###';
17
+ searchTermQueryParameter;
18
+ searchLangQueryParameter;
19
+ searchFilterQueryParameter;
17
20
  geoJsonFormatter = new GeoJSON();
18
- constructor(context, type, name, url, resultSrid) {
21
+ constructor(context, config) {
19
22
  super(context);
20
- this.type = type;
21
- this.providerName = name;
22
- this.url = new URL(url);
23
- this.resultSrid = resultSrid;
23
+ this.type = config.type;
24
+ this.providerName = config.providerName;
25
+ this.url = this.sanitizeUrl(config.url);
26
+ this.resultSrid = config.resultSrid;
24
27
  }
25
28
  initializeSingleton() {
26
29
  super.initializeSingleton();
@@ -28,11 +31,57 @@ class AbstractSearchClient extends GirafeSingleton {
28
31
  get mapProjection() {
29
32
  return this.context.mapManager.getMap().getView().getProjection();
30
33
  }
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);
34
+ /**
35
+ * Sanitize the url by removing placeholders that are wrongfully interpreted as URL fragments by the URL parser.
36
+ * The parameters are introduced again and set when fetching the search results.
37
+ */
38
+ sanitizeUrl(url) {
39
+ // First, replace problematic characters in the placeholders and url that cause issues with the URL parser
40
+ const sanitizedSearchTermPlaceholder = this.sanitizePlaceholder(this.searchTermPlaceholder);
41
+ const sanitizedSearchLangPlaceholder = this.sanitizePlaceholder(this.searchLangPlaceholder);
42
+ const sanitizedUrl = new URL(this.sanitizePlaceholder(url));
43
+ // Then, save and remove the query parameters that contain placeholders; they'll be introduced again when fetching the search results
44
+ this.searchTermQueryParameter = this.extractQueryParamWithPlaceholder(sanitizedUrl, sanitizedSearchTermPlaceholder);
45
+ this.searchLangQueryParameter = this.extractQueryParamWithPlaceholder(sanitizedUrl, sanitizedSearchLangPlaceholder);
46
+ return sanitizedUrl;
47
+ }
48
+ extractQueryParamWithPlaceholder(url, sanitizedPlaceholder) {
49
+ const queryParameter = Array.from(url.searchParams.entries()).find((entry) => entry[1] === sanitizedPlaceholder)?.[0];
50
+ if (queryParameter) {
51
+ url.searchParams.delete(queryParameter);
52
+ }
53
+ return queryParameter;
54
+ }
55
+ sanitizePlaceholder(urlOrPlaceholder) {
56
+ // Replace ### type placeholders because they are interpreted as URL fragments
57
+ const placeholderMarker = '###';
58
+ const urlSafePlaceholderMarker = '_';
59
+ return urlOrPlaceholder.replaceAll(placeholderMarker, urlSafePlaceholderMarker);
60
+ }
61
+ /**
62
+ * Returns a list of categories that can be searched by.
63
+ */
64
+ async getSearchCategories() {
65
+ return [];
66
+ }
67
+ buildRequestUrl(term, filter) {
68
+ const url = new URL(this.url);
69
+ if (this.searchTermQueryParameter) {
70
+ url.searchParams.set(this.searchTermQueryParameter, term);
71
+ }
72
+ if (this.searchLangQueryParameter && this.context.stateManager.state.language) {
73
+ url.searchParams.set(this.searchLangQueryParameter, this.context.stateManager.state.language);
74
+ }
75
+ if (this.searchFilterQueryParameter && filter.length) {
76
+ url.searchParams.set(this.searchFilterQueryParameter, filter.join(','));
77
+ }
78
+ return url;
79
+ }
80
+ /**
81
+ * Executes a search request with the provided search term.
82
+ */
83
+ async requestSearch(term, filter, abortController) {
84
+ const url = this.buildRequestUrl(term, filter);
36
85
  try {
37
86
  const response = await fetch(url, { signal: abortController.signal });
38
87
  return await this.parseResponse(response);
@@ -62,8 +111,8 @@ class AbstractSearchClient extends GirafeSingleton {
62
111
  });
63
112
  }
64
113
  /**
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.
114
+ * Groups the results by the default group, which equivalates to no grouping. Should be overridden by the client class
115
+ * to group results by a custom criteria like search category.
67
116
  */
68
117
  groupedResults(results) {
69
118
  return { [SEARCH_GROUP_DEFAULT]: results };
@@ -74,5 +123,11 @@ class AbstractSearchClient extends GirafeSingleton {
74
123
  getZoomToBbox(feature) {
75
124
  return createBufferedExtentFromFeature(feature);
76
125
  }
126
+ /**
127
+ * Retrieves the translation key for a given category. Default: use category string directly.
128
+ */
129
+ getI18nKeyForCategory(category) {
130
+ return category;
131
+ }
77
132
  }
78
133
  export default AbstractSearchClient;
@@ -1,10 +1,9 @@
1
1
  import Feature from 'ol/Feature.js';
2
- import AbstractSearchClient from './abstractsearchclient.js';
2
+ import AbstractSearchClient, { BaseSearchClientConfig } from './abstractsearchclient.js';
3
3
  import IGirafeContext from '../context/icontext.js';
4
4
  import { Extent } from 'ol/extent.js';
5
- export type GeoAdminChSearchConfig = {
5
+ export type GeoAdminChSearchClientConfig = BaseSearchClientConfig & {
6
6
  type: 'geoadminch';
7
- providerName: string;
8
7
  bbox?: string;
9
8
  locationOrigins?: string;
10
9
  };
@@ -36,9 +35,16 @@ export type GeoAdminChSearchConfig = {
36
35
  * - parcel
37
36
  */
38
37
  declare class GeoAdminChSearchClient extends AbstractSearchClient {
39
- constructor(context: IGirafeContext, config: GeoAdminChSearchConfig);
38
+ protected searchTermQueryParameter: string;
39
+ protected searchLangQueryParameter: string;
40
+ protected searchFilterQueryParameter: string;
41
+ private readonly locationOrigins;
42
+ private readonly hasLimitedOriginsConfigured;
43
+ private readonly i18nKeyPrefix;
44
+ constructor(context: IGirafeContext, config: GeoAdminChSearchClientConfig);
40
45
  private setSearchExtent;
41
- requestSearch(term: string, abortController: AbortController): Promise<Feature[]>;
46
+ getSearchCategories(): Promise<string[]>;
47
+ protected buildRequestUrl(term: string, filter: string[]): URL;
42
48
  protected parseResponse(response: Response): Promise<Feature[]>;
43
49
  /**
44
50
  * GeoAdmin geoJson responses contain coordinates as [north, east], but the GeoJson standard is [east, north].
@@ -51,7 +57,7 @@ declare class GeoAdminChSearchClient extends AbstractSearchClient {
51
57
  */
52
58
  private swapCoordinates;
53
59
  /**
54
- * Property origin contains the result category:
60
+ * The property `origin` contains the result category.
55
61
  */
56
62
  groupedResults(results: Feature[]): Record<string, Feature[]>;
57
63
  getZoomToBbox(feature: Feature): Extent | undefined;
@@ -59,5 +65,6 @@ declare class GeoAdminChSearchClient extends AbstractSearchClient {
59
65
  * Remove bold and italic HTML tags from the label
60
66
  */
61
67
  private sanitizeHtmlLabel;
68
+ getI18nKeyForCategory(category: string): string;
62
69
  }
63
70
  export default GeoAdminChSearchClient;
@@ -1,5 +1,6 @@
1
1
  import AbstractSearchClient from './abstractsearchclient.js';
2
2
  import { transformExtent } from 'ol/proj.js';
3
+ const locationOrigins = ['zipcode', 'gg25', 'district', 'kantone', 'gazetteer', 'address', 'parcel'];
3
4
  /**
4
5
  * Search provider for the Swiss search service of geo.admin.ch.
5
6
  * See documentation at https://docs.geo.admin.ch/access-data/search.html
@@ -28,14 +29,23 @@ import { transformExtent } from 'ol/proj.js';
28
29
  * - parcel
29
30
  */
30
31
  class GeoAdminChSearchClient extends AbstractSearchClient {
32
+ searchTermQueryParameter = 'searchText';
33
+ searchLangQueryParameter = 'lang';
34
+ searchFilterQueryParameter = 'origins';
35
+ locationOrigins = locationOrigins;
36
+ hasLimitedOriginsConfigured = false;
37
+ i18nKeyPrefix = 'geoadminch_';
31
38
  constructor(context, config) {
32
- super(context, 'geoadminch', config.providerName, 'https://api3.geo.admin.ch/rest/services/api/SearchServer', 'EPSG:2056');
39
+ config.url = 'https://api3.geo.admin.ch/rest/services/api/SearchServer';
40
+ config.resultSrid = 'EPSG:2056';
41
+ super(context, config);
33
42
  this.url.searchParams.set('type', 'locations');
34
43
  this.url.searchParams.set('sr', this.resultSrid.replace('EPSG:', ''));
35
44
  this.url.searchParams.set('geometryFormat', 'geojson');
36
45
  this.url.searchParams.set('sortbox', 'false');
37
46
  if (config.locationOrigins) {
38
- this.url.searchParams.set('origins', config.locationOrigins);
47
+ this.locationOrigins = config.locationOrigins.split(',');
48
+ this.hasLimitedOriginsConfigured = true;
39
49
  }
40
50
  this.setSearchExtent(config.bbox);
41
51
  }
@@ -59,18 +69,15 @@ class GeoAdminChSearchClient extends AbstractSearchClient {
59
69
  }
60
70
  this.url.searchParams.set('bbox', searchExtent);
61
71
  }
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(() => []);
72
+ async getSearchCategories() {
73
+ return this.locationOrigins;
74
+ }
75
+ buildRequestUrl(term, filter) {
76
+ if (this.hasLimitedOriginsConfigured && !filter.length) {
77
+ // Apply the limitations to searchable origins from config.json
78
+ filter = this.locationOrigins;
73
79
  }
80
+ return super.buildRequestUrl(term, filter);
74
81
  }
75
82
  async parseResponse(response) {
76
83
  const data = this.fixGeoAdminCoordinates((await response.json()));
@@ -117,12 +124,12 @@ class GeoAdminChSearchClient extends AbstractSearchClient {
117
124
  return coordinates.map(this.swapCoordinates);
118
125
  }
119
126
  /**
120
- * Property origin contains the result category:
127
+ * The property `origin` contains the result category.
121
128
  */
122
129
  groupedResults(results) {
123
130
  const groupedResults = {};
124
131
  for (const result of results) {
125
- const group = `geoadminch_${result.get('origin') ?? 'Other'}`;
132
+ const group = this.getI18nKeyForCategory(result.get('origin') ?? 'other');
126
133
  groupedResults[group] ??= [];
127
134
  groupedResults[group].push(result);
128
135
  }
@@ -143,5 +150,8 @@ class GeoAdminChSearchClient extends AbstractSearchClient {
143
150
  sanitizeHtmlLabel(label) {
144
151
  return label.replace(/<\/?[ib]>/g, '');
145
152
  }
153
+ getI18nKeyForCategory(category) {
154
+ return `${this.i18nKeyPrefix}${category}`;
155
+ }
146
156
  }
147
157
  export default GeoAdminChSearchClient;
@@ -1,11 +1,8 @@
1
1
  import IGirafeContext from '../context/icontext.js';
2
2
  import Feature from 'ol/Feature.js';
3
- import AbstractSearchClient from './abstractsearchclient.js';
4
- export type GmfSearchConfig = {
3
+ import AbstractSearchClient, { BaseSearchClientConfig } from './abstractsearchclient.js';
4
+ export type GmfSearchClientConfig = BaseSearchClientConfig & {
5
5
  type: 'geomapfish';
6
- providerName: string;
7
- url: string;
8
- resultSrid: string;
9
6
  };
10
7
  /**
11
8
  * A search provider for the GeoMapFish backend.
@@ -17,7 +14,22 @@ export type GmfSearchConfig = {
17
14
  * - resultSrid: The spatial reference system (SRID) of the search results. Default same as map SRID.
18
15
  */
19
16
  declare class GmfSearchClient extends AbstractSearchClient {
20
- constructor(context: IGirafeContext, config: GmfSearchConfig);
17
+ protected searchFilterQueryParameter: string;
18
+ private readonly layerTreeCategory;
19
+ constructor(context: IGirafeContext, config: GmfSearchClientConfig);
20
+ /**
21
+ * Create the search capabilities URL by using the search URL and appending '/capabilities' to it.
22
+ */
23
+ private get searchCapabilitiesUrl();
24
+ /**
25
+ * Returns a list of categories that can be searched by
26
+ */
27
+ getSearchCategories(): Promise<string[]>;
28
+ /**
29
+ * If the search request should filter results by layer tree elements, the URL needs to include an additional parameter
30
+ * `null_category` to include layer results.
31
+ */
32
+ protected buildRequestUrl(term: string, filter: string[]): URL;
21
33
  groupedResults(results: Feature[]): Record<string, Feature[]>;
22
34
  }
23
35
  export default GmfSearchClient;
@@ -9,8 +9,66 @@ import AbstractSearchClient from './abstractsearchclient.js';
9
9
  * - resultSrid: The spatial reference system (SRID) of the search results. Default same as map SRID.
10
10
  */
11
11
  class GmfSearchClient extends AbstractSearchClient {
12
+ searchFilterQueryParameter = 'categories';
13
+ layerTreeCategory = 'category_layer_tree';
12
14
  constructor(context, config) {
13
- super(context, 'geomapfish', config.providerName, config.url, config.resultSrid);
15
+ super(context, config);
16
+ }
17
+ /**
18
+ * Create the search capabilities URL by using the search URL and appending '/capabilities' to it.
19
+ */
20
+ get searchCapabilitiesUrl() {
21
+ const capabilitiesUrl = new URL(this.url.toString().split('?')[0]);
22
+ capabilitiesUrl.pathname += '/capabilities';
23
+ // Reintroduce language and interface parameter
24
+ const interfaceParameter = this.url.searchParams.get('interface');
25
+ if (interfaceParameter) {
26
+ capabilitiesUrl.searchParams.set('interface', interfaceParameter);
27
+ }
28
+ if (this.searchLangQueryParameter && this.context.stateManager.state.language) {
29
+ capabilitiesUrl.searchParams.set(this.searchLangQueryParameter, this.context.stateManager.state.language);
30
+ }
31
+ return capabilitiesUrl;
32
+ }
33
+ /**
34
+ * Returns a list of categories that can be searched by
35
+ */
36
+ async getSearchCategories() {
37
+ const response = await fetch(this.searchCapabilitiesUrl);
38
+ if (response.ok) {
39
+ try {
40
+ const content = (await response.json());
41
+ // Add a fixed category to search layers in the layer tree
42
+ const categories = content.categories;
43
+ categories.unshift(this.layerTreeCategory);
44
+ return categories;
45
+ }
46
+ catch (error) {
47
+ console.warn(`No search categories available: Search capabilities response malformed.\n${error}`);
48
+ }
49
+ }
50
+ else {
51
+ console.warn('No search categories available: Search capabilities endpoint not reachable.');
52
+ }
53
+ return [];
54
+ }
55
+ /**
56
+ * If the search request should filter results by layer tree elements, the URL needs to include an additional parameter
57
+ * `null_category` to include layer results.
58
+ */
59
+ buildRequestUrl(term, filter) {
60
+ if (!filter.length) {
61
+ return super.buildRequestUrl(term, filter);
62
+ }
63
+ let includeLayerResults = 'false';
64
+ const modifiedFilter = [...filter];
65
+ if (modifiedFilter.includes(this.layerTreeCategory)) {
66
+ modifiedFilter.splice(modifiedFilter.indexOf(this.layerTreeCategory), 1);
67
+ includeLayerResults = 'true';
68
+ }
69
+ const url = super.buildRequestUrl(term, modifiedFilter);
70
+ url.searchParams.set('null_category', includeLayerResults);
71
+ return url;
14
72
  }
15
73
  groupedResults(results) {
16
74
  const groupedResults = {};
@@ -8,6 +8,9 @@ export type SearchProviderConfig = {
8
8
  providerName: string;
9
9
  [key: string]: unknown;
10
10
  };
11
+ export interface SearchCategories {
12
+ [provider: string]: string[];
13
+ }
11
14
  declare class SearchManager extends GirafeSingleton {
12
15
  protected readonly clients: Map<string, AbstractSearchClient>;
13
16
  private abortController;
@@ -17,12 +20,24 @@ declare class SearchManager extends GirafeSingleton {
17
20
  constructor(context: IGirafeContext);
18
21
  initializeSingleton(): void;
19
22
  protected createClient(providerConfig: SearchProviderConfig): void;
23
+ /**
24
+ * Returns available search categories per provider.
25
+ */
26
+ getSearchCategories(): Promise<SearchCategories | null>;
27
+ hasFilterShortcutsEnabled(): boolean;
28
+ /**
29
+ * Detects whether a search filter shortcut is present in the provided search term.
30
+ * Updates the search state with the extracted prefix and shortcut if applicable.
31
+ */
32
+ detectSearchFilterShortcut(term: string): boolean;
33
+ private extractFilterPrefixFromTerm;
34
+ private extractFilterShortcutFromTerm;
20
35
  abortSearch(): void;
21
36
  /**
22
- * Performs a search operation with all available clients based on the provided term and returns results
23
- * grouped by provider name and result category.
37
+ * Performs a search operation with all available clients based on the provided term and optional search filters.
38
+ * Returns results grouped by provider name and result category.
24
39
  */
25
- doSearch(term: string): Promise<Record<string, Record<string, Feature[]>> | null>;
40
+ doSearch(term: string, searchFilter?: SearchCategories | null): Promise<Record<string, Record<string, Feature[]>> | null>;
26
41
  private createCoordinateSearchResult;
27
42
  /**
28
43
  * Detects and parses a coordinate search term into an array of two numerical values.
@@ -33,5 +48,6 @@ declare class SearchManager extends GirafeSingleton {
33
48
  private parseCoordinateFromString;
34
49
  private isCoordinatePairValid;
35
50
  getZoomToBbox(feature: Feature): Extent | undefined;
51
+ getI18nKeyForCategory(providerName: string, category: string): string;
36
52
  }
37
53
  export default SearchManager;
@@ -36,26 +36,83 @@ class SearchManager extends GirafeSingleton {
36
36
  console.error(`Unknown search type: ${providerConfig.type}`);
37
37
  }
38
38
  }
39
+ /**
40
+ * Returns available search categories per provider.
41
+ */
42
+ async getSearchCategories() {
43
+ const categoriesByProvider = {};
44
+ await Promise.all(Array.from(this.clients.values()).map(async (client) => {
45
+ categoriesByProvider[client.providerName] = await client.getSearchCategories();
46
+ }));
47
+ // If only no provider or no categories are available, return null instead
48
+ if (Object.keys(categoriesByProvider).length === 0 ||
49
+ Object.values(categoriesByProvider).every((categories) => categories.length === 0))
50
+ return null;
51
+ return categoriesByProvider;
52
+ }
53
+ hasFilterShortcutsEnabled() {
54
+ const searchConfig = this.context.configManager.Config.search;
55
+ return (!!searchConfig?.filterEnabled &&
56
+ !!searchConfig?.filterShortcuts &&
57
+ !!searchConfig?.filterShortcutPrefix &&
58
+ !this.context.stateManager.state.interface.isMobile);
59
+ }
60
+ /**
61
+ * Detects whether a search filter shortcut is present in the provided search term.
62
+ * Updates the search state with the extracted prefix and shortcut if applicable.
63
+ */
64
+ detectSearchFilterShortcut(term) {
65
+ if (!this.hasFilterShortcutsEnabled()) {
66
+ return false;
67
+ }
68
+ term = term.trimStart();
69
+ const prefix = this.extractFilterPrefixFromTerm(term);
70
+ this.context.stateManager.state.search.filterShortcutPrefix = prefix;
71
+ if (prefix) {
72
+ this.context.stateManager.state.search.filterShortcut = this.extractFilterShortcutFromTerm(term, prefix);
73
+ return true;
74
+ }
75
+ return false;
76
+ }
77
+ extractFilterPrefixFromTerm(term) {
78
+ return (this.context.configManager.Config.search.filterShortcutPrefix.find((prefix) => term.startsWith(prefix)) ?? null);
79
+ }
80
+ extractFilterShortcutFromTerm(term, prefix) {
81
+ const availableShortcuts = this.context.configManager.Config.search?.filterShortcuts ?? {};
82
+ const shortcutCandidateWithoutPrefix = term.substring(prefix.length).split(' ')[0];
83
+ const categoriesToActivate = availableShortcuts[shortcutCandidateWithoutPrefix] ?? [];
84
+ if (categoriesToActivate.length) {
85
+ // A valid shortcut was detected
86
+ if (term.startsWith(`${prefix}${shortcutCandidateWithoutPrefix} `)) {
87
+ // Only when a space is added after the finished shortcut, the search filter is applied
88
+ return shortcutCandidateWithoutPrefix;
89
+ }
90
+ }
91
+ return null;
92
+ }
39
93
  abortSearch() {
40
94
  this.abortController.abort();
41
95
  }
42
96
  /**
43
- * Performs a search operation with all available clients based on the provided term and returns results
44
- * grouped by provider name and result category.
97
+ * Performs a search operation with all available clients based on the provided term and optional search filters.
98
+ * Returns results grouped by provider name and result category.
45
99
  */
46
- async doSearch(term) {
100
+ async doSearch(term, searchFilter = null) {
47
101
  // Cancel any previous search
48
102
  this.abortSearch();
49
103
  // Create a new controller for the new request
50
104
  this.abortController = new AbortController();
51
105
  let searchResults = null;
52
- const coordinates = this.detectCoordinatesInTerm(term);
53
- if (coordinates) {
54
- searchResults = this.createCoordinateSearchResult(coordinates);
106
+ term = term.trim();
107
+ if (!searchFilter) {
108
+ const coordinates = this.detectCoordinatesInTerm(term);
109
+ if (coordinates) {
110
+ searchResults = this.createCoordinateSearchResult(coordinates);
111
+ }
55
112
  }
56
113
  if (!searchResults && term.length >= this.minSearchTermLength) {
57
114
  const responsesArray = await Promise.all(Array.from(this.clients.values()).map(async (client) => {
58
- const response = await client.requestSearch(term, this.abortController);
115
+ const response = await client.requestSearch(term, searchFilter ? (searchFilter[client.providerName] ?? []) : [], this.abortController);
59
116
  return { [client.providerName]: client.groupedResults(response) };
60
117
  }));
61
118
  // Merge the responses of multiple clients into a single object that has the client name as key
@@ -95,6 +152,9 @@ class SearchManager extends GirafeSingleton {
95
152
  */
96
153
  detectCoordinatesInTerm(term) {
97
154
  term = term.trim();
155
+ if (term.length < 3) {
156
+ return null;
157
+ }
98
158
  let parts = [];
99
159
  // Supported separators:
100
160
  const separators = [
@@ -137,13 +197,16 @@ class SearchManager extends GirafeSingleton {
137
197
  return !!coordinates && coordinates.length === 2 && coordinates.every((coordinate) => Number.isFinite(coordinate));
138
198
  }
139
199
  getZoomToBbox(feature) {
140
- const provider = feature.get('provider');
141
- if (this.clients.has(provider)) {
142
- return this.clients.get(provider)?.getZoomToBbox(feature);
200
+ const providerName = feature.get('provider');
201
+ if (this.clients.has(providerName)) {
202
+ return this.clients.get(providerName)?.getZoomToBbox(feature);
143
203
  }
144
204
  else {
145
205
  return createBufferedExtentFromFeature(feature);
146
206
  }
147
207
  }
208
+ getI18nKeyForCategory(providerName, category) {
209
+ return this.clients.get(providerName)?.getI18nKeyForCategory(category) ?? category;
210
+ }
148
211
  }
149
212
  export default SearchManager;
@@ -19,6 +19,7 @@ export default class GraphicalInterface {
19
19
  userPreferencesPanelVisible: boolean;
20
20
  infoWindowVisible: boolean;
21
21
  searchComponentVisible: boolean;
22
+ searchFilterComponentVisible: boolean;
22
23
  basemapComponentVisible: boolean;
23
24
  darkMapMode: boolean;
24
25
  fullscreenMode: boolean;
@@ -19,6 +19,7 @@ export default class GraphicalInterface {
19
19
  userPreferencesPanelVisible = false;
20
20
  infoWindowVisible = false;
21
21
  searchComponentVisible = true;
22
+ searchFilterComponentVisible = false;
22
23
  basemapComponentVisible = true;
23
24
  darkMapMode = false;
24
25
  fullscreenMode = false;
@@ -82,6 +82,13 @@ export type InfoWindow = {
82
82
  top: string | number | null;
83
83
  left: string | number | null;
84
84
  };
85
+ export type SearchState = {
86
+ filter: {
87
+ [provider: string]: string[];
88
+ } | null;
89
+ filterShortcutPrefix: string | null;
90
+ filterShortcut: string | null;
91
+ };
85
92
  export type ExtendedState = Record<string, object>;
86
93
  export default class State {
87
94
  /**
@@ -125,6 +132,7 @@ export default class State {
125
132
  infoWindow: InfoWindow;
126
133
  isOffline: boolean;
127
134
  oauth: LoginState;
135
+ search: SearchState;
128
136
  extendedState: ExtendedState;
129
137
  }
130
138
  export {};
@@ -114,6 +114,11 @@ export default class State {
114
114
  audience: [],
115
115
  somethingChanged: true
116
116
  };
117
+ search = {
118
+ filter: null,
119
+ filterShortcutPrefix: null,
120
+ filterShortcut: null
121
+ };
117
122
  // The State object is defined as <not extensible> by the StateManager.
118
123
  // This property can be used by third-parts components or extensions
119
124
  // to add custom attributes to the state.
@@ -33,10 +33,17 @@ export declare const MockConfig: {
33
33
  maxExtent: string;
34
34
  };
35
35
  search: {
36
- providers: {
36
+ providers: ({
37
37
  type: string;
38
38
  url: string;
39
- }[];
39
+ } | {
40
+ type: string;
41
+ url?: undefined;
42
+ })[];
43
+ filterShortcutPrefix: string[];
44
+ filterShortcuts: {
45
+ adr: string[];
46
+ };
40
47
  };
41
48
  share: {
42
49
  createUrl: string;