@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
134 lines (133 loc) • 5.82 kB
JavaScript
import { GeoJSON } from 'ol/format.js';
import GirafeSingleton from '../../base/GirafeSingleton.js';
import { createBufferedExtentFromFeature } from '../geometrytools.js';
// The default provider is a container for search results that do not result from a provider search (e.g. map re-centering)
export const SEARCH_PROVIDER_DEFAULT = 'default';
// The default search group contains search results that do not belong to a specific category, they are listed without a list header
export const SEARCH_GROUP_DEFAULT = 'default';
// The map recenter search group contains the coordinates for re-centering the map
export const SEARCH_GROUP_RECENTER_MAP = 'recenter_map';
class AbstractSearchClient extends GirafeSingleton {
type;
providerName;
url;
resultSrid;
searchTermPlaceholder = '###SEARCHTERM###';
searchLangPlaceholder = '###SEARCHLANG###';
searchTermQueryParameter;
searchLangQueryParameter;
searchFilterQueryParameter;
geoJsonFormatter = new GeoJSON();
constructor(context, config) {
super(context);
this.type = config.type;
this.providerName = config.providerName;
this.url = this.sanitizeUrl(config.url);
this.resultSrid = config.resultSrid;
}
async initializeSingleton() {
await super.initializeSingleton();
}
get mapProjection() {
return this.context.mapManager.getMap().getView().getProjection();
}
/**
* Sanitize the url by removing placeholders that are wrongfully interpreted as URL fragments by the URL parser.
* The parameters are introduced again and set when fetching the search results.
*/
sanitizeUrl(url) {
// First, replace problematic characters in the placeholders and url that cause issues with the URL parser
const sanitizedSearchTermPlaceholder = this.sanitizePlaceholder(this.searchTermPlaceholder);
const sanitizedSearchLangPlaceholder = this.sanitizePlaceholder(this.searchLangPlaceholder);
const sanitizedUrl = new URL(this.sanitizePlaceholder(url));
// Then, save and remove the query parameters that contain placeholders; they'll be introduced again when fetching the search results
this.searchTermQueryParameter = this.extractQueryParamWithPlaceholder(sanitizedUrl, sanitizedSearchTermPlaceholder);
this.searchLangQueryParameter = this.extractQueryParamWithPlaceholder(sanitizedUrl, sanitizedSearchLangPlaceholder);
return sanitizedUrl;
}
extractQueryParamWithPlaceholder(url, sanitizedPlaceholder) {
const queryParameter = Array.from(url.searchParams.entries()).find((entry) => entry[1] === sanitizedPlaceholder)?.[0];
if (queryParameter) {
url.searchParams.delete(queryParameter);
}
return queryParameter;
}
sanitizePlaceholder(urlOrPlaceholder) {
// Replace ### type placeholders because they are interpreted as URL fragments
const placeholderMarker = '###';
const urlSafePlaceholderMarker = '_';
return urlOrPlaceholder.replaceAll(placeholderMarker, urlSafePlaceholderMarker);
}
/**
* Returns a list of categories that can be searched by.
*/
async getSearchCategories() {
return [];
}
buildRequestUrl(term, filter) {
const url = new URL(this.url);
if (this.searchTermQueryParameter) {
url.searchParams.set(this.searchTermQueryParameter, term);
}
if (this.searchLangQueryParameter && this.context.stateManager.state.language) {
url.searchParams.set(this.searchLangQueryParameter, this.context.stateManager.state.language);
}
if (this.searchFilterQueryParameter && filter.length) {
url.searchParams.set(this.searchFilterQueryParameter, filter.join(','));
}
return url;
}
/**
* Executes a search request with the provided search term.
*/
async requestSearch(term, filter, abortController) {
const url = this.buildRequestUrl(term, filter);
try {
const response = await fetch(url, { signal: abortController.signal });
return await this.parseResponse(response);
}
catch (error) {
console.error(`Error fetching search results from ${this.url.toString()}:`, error);
return [];
}
}
/**
* Parses the response from the server and returns an array of features.
* Features must have the following properties to be used correctly with the search component:
* - `provider`
* - `label`
* - `label_html`: optional, for styled entries in the search results list
* - `actions`: optional, a list of SearchResultsActions
*/
async parseResponse(response) {
const data = (await response.json());
const features = this.geoJsonFormatter.readFeatures(data, {
dataProjection: this.resultSrid,
featureProjection: this.mapProjection
});
return features.map((f) => {
f.set('provider', this.providerName);
return f;
});
}
/**
* Groups the results by the default group, which equivalates to no grouping. Should be overridden by the client class
* to group results by a custom criteria like search category.
*/
groupedResults(results) {
return { [SEARCH_GROUP_DEFAULT]: results };
}
/**
* Returns a bbox around the geometry that is 50% larger than the geometry.
*/
getZoomToBbox(feature) {
return createBufferedExtentFromFeature(feature);
}
/**
* Retrieves the translation key for a given category. Default: use category string directly.
*/
getI18nKeyForCategory(category) {
return category;
}
}
export default AbstractSearchClient;