@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
150 lines (149 loc) • 6.84 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { SEARCH_GROUP_RECENTER_MAP, SEARCH_PROVIDER_DEFAULT } from './abstractsearchclient.js';
import GeoAdminChSearchClient from './geoadminchsearchclient.js';
import GmfSearchClient from './gmfsearchclient.js';
import GirafeSingleton from '../../base/GirafeSingleton.js';
import Feature from 'ol/Feature.js';
import { createBufferedExtentFromFeature, parseCoordinates } from '../geometrytools.js';
import { transform, transformExtent } from 'ol/proj.js';
import { Point } from 'ol/geom.js';
class SearchManager extends GirafeSingleton {
clients = new Map();
abortController = new AbortController();
defaultSrid;
defaultMaxExtent;
minSearchTermLength = 3;
constructor(context) {
super(context);
}
initializeSingleton() {
super.initializeSingleton();
for (const providerConfig of this.context.configManager.Config.search?.providers ?? []) {
this.createClient(providerConfig);
}
this.defaultSrid = this.context.configManager.getDefaultConfigValue('map.srid');
this.defaultMaxExtent = this.context.configManager.Config.map.maxExtent?.split(',').map(Number);
}
createClient(providerConfig) {
switch (providerConfig.type) {
case 'geomapfish':
this.clients.set(providerConfig.type, new GmfSearchClient(this.context, providerConfig));
break;
case 'geoadminch':
this.clients.set(providerConfig.type, new GeoAdminChSearchClient(this.context, providerConfig));
break;
default:
console.error(`Unknown search type: ${providerConfig.type}`);
}
}
abortSearch() {
this.abortController.abort();
}
/**
* Performs a search operation with all available clients based on the provided term and returns results
* grouped by provider name and result category.
*/
async doSearch(term) {
// Cancel any previous search
this.abortSearch();
// Create a new controller for the new request
this.abortController = new AbortController();
let searchResults = null;
const coordinates = this.detectCoordinatesInTerm(term);
if (coordinates) {
searchResults = this.createCoordinateSearchResult(coordinates);
}
if (!searchResults && term.length >= this.minSearchTermLength) {
const responsesArray = await Promise.all(Array.from(this.clients.values()).map(async (client) => {
const response = await client.requestSearch(term, this.abortController);
return { [client.providerName]: client.groupedResults(response) };
}));
// Merge the responses of multiple clients into a single object that has the client name as key
// and the grouped results as values
searchResults = responsesArray.reduce((acc, curr) => ({ ...acc, ...curr }), {});
}
return searchResults;
}
createCoordinateSearchResult(coordinates) {
const currentSrid = this.context.mapManager.getMap().getView().getProjection().getCode();
let parsedCoordinates;
// Checks if the coordinates are within the default SRID max extent or geographic
parsedCoordinates = parseCoordinates(coordinates, this.defaultMaxExtent, this.defaultSrid);
if (this.isCoordinatePairValid(parsedCoordinates) && this.defaultSrid !== currentSrid) {
parsedCoordinates = transform(parsedCoordinates, this.defaultSrid, currentSrid);
parsedCoordinates = parsedCoordinates.map((number) => Math.round(number * 100) / 100);
}
// Try to parse the coordinates in the current map projection
if (!this.isCoordinatePairValid(parsedCoordinates) && this.defaultMaxExtent) {
const currentMaxExtent = transformExtent(this.defaultMaxExtent, this.defaultSrid, currentSrid);
parsedCoordinates = parseCoordinates(coordinates, currentMaxExtent, currentSrid);
}
if (!this.isCoordinatePairValid(parsedCoordinates)) {
return null;
}
const resultFeature = new Feature({
geometry: new Point(parsedCoordinates),
label: `${parsedCoordinates[0]} ${parsedCoordinates[1]}`
});
// Return this result grouped by the default provider and as part of the `recenter_map` group
return { [SEARCH_PROVIDER_DEFAULT]: { [SEARCH_GROUP_RECENTER_MAP]: [resultFeature] } };
}
/**
* Detects and parses a coordinate search term into an array of two numerical values.
* The method attempts to identify valid coordinate pairs from the given term,
* using different separators such as `;`, `/`, `,`, or whitespace.
*/
detectCoordinatesInTerm(term) {
term = term.trim();
let parts = [];
// Supported separators:
const separators = [
{ char: ';' },
{ char: '/' },
// Only search for one comma, because a comma could also be used as a decimal separator
{ char: ',', condition: () => [...term].filter((c) => c === ',').length === 1 },
// Whitespace separator
{ char: null, regex: /\s+/ }
];
// Search for the coordinate separator step-wise
for (const { char, regex, condition } of separators) {
if (char && (condition ? condition() : term.includes(char))) {
parts = term.split(char);
break;
}
else if (regex?.test(term)) {
parts = term.split(regex);
break;
}
}
if (parts.length !== 2) {
return null;
}
const first = this.parseCoordinateFromString(parts[0]);
const second = this.parseCoordinateFromString(parts[1]);
return first === null || second === null ? null : [first, second];
}
parseCoordinateFromString(value) {
const normalized = value
// Remove coordinate prefix and thousand separator
.replace(/[NESW']/g, '')
// Replace decimal separator
.replace(',', '.')
.trim();
const number = Number(normalized);
return Number.isFinite(number) ? number : null;
}
isCoordinatePairValid(coordinates) {
return !!coordinates && coordinates.length === 2 && coordinates.every((coordinate) => Number.isFinite(coordinate));
}
getZoomToBbox(feature) {
const provider = feature.get('provider');
if (this.clients.has(provider)) {
return this.clients.get(provider)?.getZoomToBbox(feature);
}
else {
return createBufferedExtentFromFeature(feature);
}
}
}
export default SearchManager;