UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

150 lines (149 loc) 6.84 kB
// 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;