Skip to content

Commit

Permalink
feat: introduce useNominatim hook
Browse files Browse the repository at this point in the history
  • Loading branch information
ahennr authored and simonseyock committed May 6, 2024
1 parent 03acea0 commit f4c8597
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 196 deletions.
8 changes: 5 additions & 3 deletions src/Field/NominatimSearch/NominatimSearch.example.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -35,10 +36,11 @@ const NominatimSearchExample = () => {
}

return (
<div>
<MapContext.Provider value={map}>

<div className="example-block">
<label>The NominatimSearch<br />
<NominatimSearch map={map} />
<NominatimSearch />
</label>
</div>

Expand All @@ -48,7 +50,7 @@ const NominatimSearchExample = () => {
height: '400px'
}}
/>
</div>
</MapContext.Provider>
);
};

Expand Down
263 changes: 70 additions & 193 deletions src/Field/NominatimSearch/NominatimSearch.tsx
Original file line number Diff line number Diff line change
@@ -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`.
Expand All @@ -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
Expand All @@ -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.
*/
Expand All @@ -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<AutoCompleteProps, 'onSelect'>;
export type NominatimSearchProps = OwnProps &
Omit<AutoCompleteProps, 'onSelect'> & Omit<UseNominatimArgs, 'searchTerm'>;

/**
* The NominatimSearch.
*/
export const NominatimSearch: FC<NominatimSearchProps> = ({
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<string>('');
const [dataSource, setDataSource] = useState<NominatimPlace[]>([]);
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],
Expand All @@ -177,116 +111,59 @@ export const NominatimSearch: FC<NominatimSearchProps> = ({
] 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<OptionProps> => {
if (_isNil(item)) {
return <></>;
}
if (renderOption) {
return renderOption(item);
} else {
return (
<Option
key={item.place_id}
value={item.display_name}
key={item?.place_id}
value={item?.display_name}
>
{item.display_name}
{item?.display_name}
</Option>
);
}
}, [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) {
Expand All @@ -303,7 +180,7 @@ export const NominatimSearch: FC<NominatimSearchProps> = ({
{...passThroughProps}
>
{
dataSource.map(finalRenderOption)
nominatimResults?.map(finalRenderOption)
}
</AutoComplete>
);
Expand Down

0 comments on commit f4c8597

Please sign in to comment.