From f4c8597ee29a854aece656676639dc3058e8f6f1 Mon Sep 17 00:00:00 2001 From: Andre Henn Date: Tue, 20 Feb 2024 18:28:05 +0100 Subject: [PATCH] feat: introduce useNominatim hook --- .../NominatimSearch.example.md | 8 +- src/Field/NominatimSearch/NominatimSearch.tsx | 263 +++++------------- 2 files changed, 75 insertions(+), 196 deletions(-) diff --git a/src/Field/NominatimSearch/NominatimSearch.example.md b/src/Field/NominatimSearch/NominatimSearch.example.md index a43911963b..8fea82e420 100644 --- a/src/Field/NominatimSearch/NominatimSearch.example.md +++ b/src/Field/NominatimSearch/NominatimSearch.example.md @@ -3,6 +3,7 @@ This demonstrates the usage of the NominatimSearch. ```jsx import NominatimSearch from '@terrestris/react-geo/dist/Field/NominatimSearch/NominatimSearch'; import MapComponent from '@terrestris/react-util/dist/Components/MapComponent/MapComponent'; +import MapContext from '@terrestris/react-util/dist/Context/MapContext/MapContext'; import OlLayerTile from 'ol/layer/Tile'; import OlMap from 'ol/Map'; import {fromLonLat} from 'ol/proj'; @@ -35,10 +36,11 @@ const NominatimSearchExample = () => { } return ( -
+ +
@@ -48,7 +50,7 @@ const NominatimSearchExample = () => { height: '400px' }} /> -
+ ); }; diff --git a/src/Field/NominatimSearch/NominatimSearch.tsx b/src/Field/NominatimSearch/NominatimSearch.tsx index ba08ff6abb..55d9b44d6b 100644 --- a/src/Field/NominatimSearch/NominatimSearch.tsx +++ b/src/Field/NominatimSearch/NominatimSearch.tsx @@ -1,92 +1,24 @@ -import { AutoComplete } from 'antd'; -import { AutoCompleteProps } from 'antd/lib/auto-complete'; -import * as React from 'react'; -const Option = AutoComplete.Option; import './NominatimSearch.less'; -import Logger from '@terrestris/base-util/dist/Logger'; -import UrlUtil from '@terrestris/base-util/dist/UrlUtil/UrlUtil'; import { useMap } from '@terrestris/react-util/dist/Hooks/useMap/useMap'; -import { DefaultOptionType, OptionProps } from 'antd/lib/select'; -import { GeoJSON } from 'geojson'; +import useNominatim, { + NominatimPlace, + UseNominatimArgs +} from '@terrestris/react-util/dist/Hooks/useNominatim/useNominatim'; +import { AutoComplete } from 'antd'; +import { AutoCompleteProps } from 'antd/lib/auto-complete'; +import { OptionProps } from 'antd/lib/select'; +import _isNil from 'lodash/isNil'; import { Extent as OlExtent } from 'ol/extent'; -import OlMap from 'ol/Map'; import { transformExtent } from 'ol/proj'; -import { FC, useCallback, useEffect, useState } from 'react'; +import { DefaultOptionType } from 'rc-select/lib/Select'; +import React, { FC, useCallback, useState } from 'react'; import { CSS_PREFIX } from '../../constants'; -// See https://nominatim.org/release-docs/develop/api/Output/ for some more information -export type NominatimPlace = { - // eslint-disable-next-line camelcase - place_id: number; - // eslint-disable-next-line camelcase - osm_type: string; - // eslint-disable-next-line camelcase - osm_id: number; - boundingbox: string[]; - // eslint-disable-next-line camelcase - display_name: string; - category: string; - type: string; - importance: number; - icon?: string; - address?: any; - extratags?: any; - namedetails?: any; - geojson: GeoJSON; - licence: string; -} & DefaultOptionType; +const Option = AutoComplete.Option; interface OwnProps { - /** - * The Nominatim Base URL. See https://wiki.openstreetmap.org/wiki/Nominatim - */ - nominatimBaseUrl?: string; - /** - * Output format. - */ - format?: string; - /** - * The preferred area to find search results in [left],[top],[right],[bottom]. - */ - viewBox?: string; - /** - * Restrict the results to only items contained with the bounding box. - * Restricting the results to the bounding box also enables searching by - * amenity only. For example a search query of just "[pub]" would normally be - * rejected but with bounded=1 will result in a list of items matching within - * the bounding box. - */ - bounded?: number; - /** - * Output geometry of results in geojson format. - */ - polygonGeoJSON?: number; - /** - * Include a breakdown of the address into elements. - */ - addressDetails?: number; - /** - * Limit the number of returned results. - */ - limit?: number; - /** - * Limit search results to a specific country (or a list of countries). - * [countrycode] should be the ISO 3166-1alpha2 code, e.g. gb for the United - * Kingdom, de for Germany, etc. - */ - countryCodes?: string; - /** - * Preferred language order for showing search results, overrides the value - * specified in the "Accept-Language" HTTP header. Either use a standard RFC2616 - * accept-language string or a simple comma-separated list of language codes. - */ - searchResultLanguage?: string; - /** - * The minimal amount of characters entered in the input to start a search. - */ - minChars?: number; /** * A render function which gets called with the selected item as it is * returned by nominatim. It must return an `AutoComplete.Option`. @@ -96,7 +28,7 @@ interface OwnProps { * An onSelect function which gets called with the selected item as it is * returned by nominatim. */ - onSelect?: (item: NominatimPlace, olMap: OlMap) => void; + onSelect?: (item: NominatimPlace) => void; /** * Indicate if we should render the input and results. When setting to false, * you need to handle user input and result yourself @@ -107,14 +39,6 @@ interface OwnProps { * `visible` to `false`. */ searchTerm?: string; - /** - * A callback function which gets called with the successfully fetched data. - */ - onFetchSuccess?: (data: NominatimPlace[]) => void; - /** - * A callback function which gets called if data fetching has failed. - */ - onFetchError?: (error: any) => void; /** * An optional CSS class which should be added. */ @@ -124,50 +48,60 @@ interface OwnProps { * value is empty. */ onClear?: () => void; - /** - * Time in miliseconds that the search waits before doing a request. - */ - debounceTime?: number; } -export type NominatimSearchProps = OwnProps & Omit; +export type NominatimSearchProps = OwnProps & + Omit & Omit; /** * The NominatimSearch. */ export const NominatimSearch: FC = ({ - addressDetails = 1, - bounded = 1, + addressDetails, + bounded, className = `${CSS_PREFIX}nominatimsearch`, - countryCodes = 'de', - debounceTime = 300, - format = 'json', - limit = 10, - minChars = 3, - nominatimBaseUrl = 'https://nominatim.openstreetmap.org/search?', + countryCodes, + debounceTime, + format, + limit, + minChars, + nominatimBaseUrl, + onChange = () => undefined, onClear, onFetchError, - onFetchSuccess, onSelect, - polygonGeoJSON = 1, + polygonGeoJSON, renderOption, - onChange = () => undefined, searchResultLanguage, - viewBox = '-180,90,180,-90', + viewBox, visible = true, ...passThroughProps }) => { - const map = useMap(); - const [searchTerm, setSearchTerm] = useState(''); - const [dataSource, setDataSource] = useState([]); + const map = useMap(); - const finalOnSelect = useCallback((selected: NominatimPlace, olMap: OlMap) => { + const nominatimResults = useNominatim({ + addressDetails, + bounded, + countryCodes, + debounceTime, + format, + limit, + minChars, + nominatimBaseUrl, + onFetchError, + polygonGeoJSON, + searchResultLanguage, + searchTerm, + viewBox + }); + + const finalOnSelect = useCallback((selected: NominatimPlace) => { if (onSelect) { - onSelect(selected, olMap); + onSelect(selected); } else if (selected && selected.boundingbox) { - const olView = olMap.getView(); + const olView = map?.getView(); const bbox: number[] = selected.boundingbox.map(parseFloat); let extent = [ bbox[2], @@ -177,116 +111,59 @@ export const NominatimSearch: FC = ({ ] as OlExtent; extent = transformExtent(extent, 'EPSG:4326', - olView.getProjection().getCode()); + olView?.getProjection().getCode()); - olView.fit(extent, { + olView?.fit(extent, { duration: 500 }); } - }, [onSelect]); + }, [map, onSelect]); const finalRenderOption = useCallback((item: NominatimPlace): React.ReactElement => { + if (_isNil(item)) { + return <>; + } if (renderOption) { return renderOption(item); } else { return ( ); } }, [renderOption]); - const fetchResults = useCallback(async (baseParams: any) => { - const getRequestParams = UrlUtil.objectToRequestString(baseParams); - - const onError = (error: any) => { - Logger.error(`Error while requesting Nominatim: ${error}`); - onFetchError?.(error); - }; - - try { - let fetchOpts: RequestInit = {}; - if (searchResultLanguage) { - fetchOpts = { - headers: { - 'accept-language': searchResultLanguage - } - }; - } - - const response = await fetch(`${nominatimBaseUrl}${getRequestParams}`, fetchOpts); - - if (!response.ok) { - onError(new Error(`Return code: ${response.status}`)); - } - - const responseJson = await response.json(); - - setDataSource(responseJson); - onFetchSuccess?.(responseJson); - } catch (error) { - onError(error); - } - }, [nominatimBaseUrl, onFetchError, onFetchSuccess, searchResultLanguage]); - - /** - * Trigger search when searchTerm has changed - */ - useEffect(() => { - setDataSource([]); - - if (!searchTerm && onClear) { - onClear(); - } - - if (!searchTerm || searchTerm.length < minChars) { - return; - } - - const timeout = setTimeout(() => { - fetchResults({ - format: format, - viewbox: viewBox, - bounded: bounded, - // eslint-disable-next-line camelcase - polygon_geojson: polygonGeoJSON, - addressdetails: addressDetails, - limit: limit, - countrycodes: countryCodes, - q: searchTerm - }); - }, debounceTime); - - return () => { - clearTimeout(timeout); - }; - }, [searchTerm, minChars, debounceTime, addressDetails, bounded, countryCodes, - fetchResults, format, limit, onClear, polygonGeoJSON, viewBox]); - /** * The function describes what to do when an item is selected. * * @param option The selected OptionData */ - const onMenuItemSelected = useCallback((_: any, option: NominatimPlace) => { + const onMenuItemSelected = useCallback((_: any, option: DefaultOptionType) => { if (!map) { return; } - const selected = dataSource.find( - i => i.place_id.toString() === option.key + const selected = nominatimResults?.find( + i => `${i.place_id}` === option.key ); - if (selected) { - finalOnSelect(selected, map); + if (!_isNil(selected)) { + finalOnSelect(selected); } - }, [finalOnSelect, dataSource, map]); + }, [finalOnSelect, nominatimResults, map]); - const onValueChange = (value: string, option: NominatimPlace | NominatimPlace[]) => { + const onValueChange = (value: string, place: NominatimPlace) => { setSearchTerm(value); - onChange(value, option); + if (!_isNil(place)) { + onChange(value, { + ...place, + label: place.display_name + }); + } else { + onChange(value, place); + } }; if (!visible) { @@ -303,7 +180,7 @@ export const NominatimSearch: FC = ({ {...passThroughProps} > { - dataSource.map(finalRenderOption) + nominatimResults?.map(finalRenderOption) } );