@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.
- package/api/apigeogirafeapp.js +1 -1
- package/assets/i18n/de.json +10 -1
- package/assets/i18n/en.json +10 -1
- package/assets/i18n/fr.json +10 -1
- package/assets/i18n/it.json +10 -1
- package/components/about/component.js +1 -1
- package/components/addbookmark/component.js +1 -1
- package/components/advancedfilter/component.js +1 -1
- package/components/alignnorthbutton-mobile/component.js +1 -1
- package/components/auth/component.js +1 -1
- package/components/basemap/component.js +1 -1
- package/components/contact/component.js +1 -1
- package/components/context-menu/custom-context-menu/component.js +1 -1
- package/components/context-menu/default-context-menu/component.js +1 -1
- package/components/coordinate/component.js +1 -1
- package/components/cross-section/cross-section-settings/component.js +1 -1
- package/components/cross-section/cross-section-viewer/component.js +1 -1
- package/components/displaymenubutton-mobile/component.js +1 -1
- package/components/displayselectorbutton-mobile/component.js +1 -1
- package/components/drawing/component-mobile.js +1 -1
- package/components/drawing/component.js +1 -1
- package/components/drawing/fixed-dimension/component.js +1 -1
- package/components/drawing-container-mobile/component.js +1 -1
- package/components/drawing-toolbar/component.js +1 -1
- package/components/edit/component.js +1 -1
- package/components/edit/editform/component.js +1 -1
- package/components/extlayers/component.js +1 -1
- package/components/feedbackbutton/component.js +1 -1
- package/components/geolocation-mobile/component.js +1 -1
- package/components/getdirections/component.js +1 -1
- package/components/help/component.js +1 -1
- package/components/infobox/component.js +1 -1
- package/components/infowindow/component.js +1 -1
- package/components/layout/component.js +1 -1
- package/components/lr-panel/component.js +1 -1
- package/components/main.d.ts +1 -0
- package/components/main.js +1 -0
- package/components/map-2d/component.js +1 -1
- package/components/map-3d/component.js +1 -1
- package/components/menu-mobile/component.js +1 -1
- package/components/menubutton/component.js +1 -1
- package/components/modals/component.js +1 -1
- package/components/navigation/component.js +1 -1
- package/components/news/news-button/component.js +1 -1
- package/components/news/news-panel/component.js +1 -1
- package/components/offline/component.js +1 -1
- package/components/print/component.js +1 -1
- package/components/prototypebanner/component.js +1 -1
- package/components/querybuilder/component.js +1 -1
- package/components/scale/component.js +1 -1
- package/components/search/component.d.ts +9 -0
- package/components/search/component.js +94 -25
- package/components/search/style.css +14 -4
- package/components/search-filter/component.d.ts +76 -0
- package/components/search-filter/component.js +388 -0
- package/components/search-filter/style.css +226 -0
- package/components/selectiongrid/component.js +1 -1
- package/components/selectionpanel-mobile/component.js +1 -1
- package/components/selectiontool/component.js +1 -1
- package/components/selectionwindow/component.js +1 -1
- package/components/share/component.js +1 -1
- package/components/share-mobile/component.js +1 -1
- package/components/swipe-up-panel-mobile/component.js +1 -1
- package/components/themes/component.js +1 -1
- package/components/timerestriction/component.js +1 -1
- package/components/timerestriction/timepicker/component.js +1 -1
- package/components/timerestriction/timeslider/component.js +1 -1
- package/components/treeview/treeviewgroup/component.js +1 -1
- package/components/treeview/treeviewitem/component.js +1 -1
- package/components/treeview/treeviewroot/component.js +1 -1
- package/components/treeview/treeviewtheme/component.js +1 -1
- package/components/userpreferences/component.js +1 -1
- package/components/videorecord/component.js +1 -1
- package/package.json +1 -1
- package/styles/common.css +1 -0
- package/styles/index.css +7 -4
- package/templates/index.html +7 -1
- package/templates/public/about.json +1 -1
- package/tools/app/geogirafeapp.js +2 -0
- package/tools/configuration/girafeconfig.d.ts +3 -0
- package/tools/configuration/girafeconfig.js +3 -1
- package/tools/main.d.ts +5 -4
- package/tools/search/abstractsearchclient.d.ts +32 -4
- package/tools/search/abstractsearchclient.js +68 -13
- package/tools/search/geoadminchsearchclient.d.ts +13 -6
- package/tools/search/geoadminchsearchclient.js +25 -15
- package/tools/search/gmfsearchclient.d.ts +18 -6
- package/tools/search/gmfsearchclient.js +59 -1
- package/tools/search/searchmanager.d.ts +19 -3
- package/tools/search/searchmanager.js +73 -10
- package/tools/state/graphicalInterface.d.ts +1 -0
- package/tools/state/graphicalInterface.js +1 -0
- package/tools/state/state.d.ts +8 -0
- package/tools/state/state.js +5 -0
- package/tools/tests/mockconfig.d.ts +9 -2
- 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,
|
|
28
|
+
protected constructor(context: IGirafeContext, config: BaseSearchClientConfig);
|
|
20
29
|
initializeSingleton(): void;
|
|
21
30
|
protected get mapProjection(): import("ol/proj.js").Projection;
|
|
22
|
-
|
|
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,
|
|
21
|
+
constructor(context, config) {
|
|
19
22
|
super(context);
|
|
20
|
-
this.type = type;
|
|
21
|
-
this.providerName =
|
|
22
|
-
this.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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
*
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
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
|
|
141
|
-
if (this.clients.has(
|
|
142
|
-
return this.clients.get(
|
|
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;
|
package/tools/state/state.d.ts
CHANGED
|
@@ -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 {};
|
package/tools/state/state.js
CHANGED
|
@@ -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;
|