Skip to content

Commit 9d37d20

Browse files
committed
feat: unify functionality of useWfs and useNominatim into on useSearch hook
BREAKING CHANGE: The `useWfs` and `useNominatim` hooks are removed. You can instead now use the `useSearch` hook with the needed search functions for example like this: ``` const [searchTerm, setSearchTerm] = useState<string>(''); const searchFunction = useCallback(createNominatimSearchFunction({}), []); const { featureCollection, loading } = useSearch(searchFunction, searchTerm); ```
1 parent 270bbad commit 9d37d20

10 files changed

+297
-323
lines changed

package-lock.json

+19-26
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"@terrestris/ol-util": "^19.0.0",
5656
"@testing-library/jest-dom": "^6.4.5",
5757
"@testing-library/react": "^15.0.7",
58+
"@types/geojson": "^7946.0.14",
5859
"@types/jest": "^29.5.12",
5960
"@types/react": "^18.3.3",
6061
"@types/react-dom": "^18.3.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// See https://nominatim.org/release-docs/develop/api/Output/ for some more information
2+
import { UrlUtil } from '@terrestris/base-util';
3+
import { Feature, Geometry } from 'geojson';
4+
import { Extent } from 'ol/extent';
5+
6+
import { SearchFunction } from './useSearch/useSearch';
7+
8+
export type NominatimPlace = {
9+
// eslint-disable-next-line camelcase
10+
place_id: number;
11+
// eslint-disable-next-line camelcase
12+
osm_type: string;
13+
// eslint-disable-next-line camelcase
14+
osm_id: number;
15+
boundingbox: string[];
16+
// eslint-disable-next-line camelcase
17+
display_name: string;
18+
category: string;
19+
type: string;
20+
importance: number;
21+
icon?: string;
22+
address?: any;
23+
extratags?: any;
24+
namedetails?: any;
25+
geojson: Geometry;
26+
licence: string;
27+
};
28+
29+
export type NominatimArgs = {
30+
/**
31+
* The Nominatim Base URL. See https://wiki.openstreetmap.org/wiki/Nominatim
32+
*/
33+
nominatimBaseUrl?: string;
34+
/**
35+
* The preferred area to find search results in [left],[top],[right],[bottom].
36+
*/
37+
viewBox?: string;
38+
/**
39+
* Restrict the results to only items contained with the bounding box.
40+
* Restricting the results to the bounding box also enables searching by
41+
* amenity only. For example a search query of just "[pub]" would normally be
42+
* rejected but with bounded=1 will result in a list of items matching within
43+
* the bounding box.
44+
*/
45+
bounded?: number;
46+
/**
47+
* Include a breakdown of the address into elements.
48+
*/
49+
addressDetails?: number;
50+
/**
51+
* Limit the number of returned results.
52+
*/
53+
limit?: number;
54+
/**
55+
* Limit search results to a specific country (or a list of countries).
56+
* [countrycode] should be the ISO 3166-1alpha2 code, e.g. gb for the United
57+
* Kingdom, de for Germany, etc.
58+
*/
59+
countryCodes?: string;
60+
/**
61+
* Preferred language order for showing search results, overrides the value
62+
* specified in the "Accept-Language" HTTP header. Either use a standard RFC2616
63+
* accept-language string or a simple comma-separated list of language codes.
64+
*/
65+
searchResultLanguage?: string;
66+
};
67+
68+
export const createNominatimSearchFunction = ({
69+
addressDetails = 1,
70+
bounded = 1,
71+
countryCodes = 'de',
72+
limit = 10,
73+
nominatimBaseUrl = 'https://nominatim.openstreetmap.org/search?',
74+
searchResultLanguage,
75+
viewBox = '-180,90,180,-90'
76+
}: NominatimArgs = {}): SearchFunction<Geometry, NominatimPlace> => {
77+
return async (searchTerm) => {
78+
const baseParams = {
79+
format: 'json',
80+
viewbox: viewBox,
81+
bounded: bounded,
82+
// eslint-disable-next-line camelcase
83+
polygon_geojson: '1',
84+
addressdetails: addressDetails,
85+
limit: limit,
86+
countrycodes: countryCodes,
87+
q: searchTerm
88+
};
89+
90+
const getRequestParams = UrlUtil.objectToRequestString(baseParams);
91+
92+
let fetchOpts: RequestInit = {};
93+
if (searchResultLanguage) {
94+
fetchOpts = {
95+
headers: {
96+
'accept-language': searchResultLanguage
97+
}
98+
};
99+
}
100+
const response = await fetch(`${nominatimBaseUrl}${getRequestParams}`, fetchOpts);
101+
if (!response.ok) {
102+
throw new Error(`Return code: ${response.status}`);
103+
}
104+
const places = await response.json() as NominatimPlace[];
105+
return {
106+
type: 'FeatureCollection',
107+
features: places.map(p => ({
108+
type: 'Feature',
109+
geometry: p.geojson,
110+
properties: p
111+
}))
112+
};
113+
};
114+
};
115+
116+
export const createNominatimGetValueFunction = () =>
117+
(feature: Feature<Geometry, NominatimPlace>) => feature.properties.display_name;
118+
119+
export const createNominatimGetExtentFunction = () =>
120+
(feature: Feature<Geometry, NominatimPlace>) => {
121+
const bbox: number[] = feature.properties.boundingbox.map(parseFloat);
122+
return [
123+
bbox[2],
124+
bbox[0],
125+
bbox[3],
126+
bbox[1]
127+
] as Extent;
128+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import WfsFilterUtil, { SearchConfig } from '@terrestris/ol-util/dist/WfsFilterUtil/WfsFilterUtil';
2+
import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson';
3+
import _isNil from 'lodash/isNil';
4+
import OlFormatGeoJSON from 'ol/format/GeoJSON';
5+
import OlFormatGml3 from 'ol/format/GML3';
6+
7+
import { SearchFunction } from './useSearch/useSearch';
8+
9+
export type WfsArgs = {
10+
additionalFetchOptions?: Partial<RequestInit>;
11+
baseUrl: string;
12+
} & Omit<SearchConfig, 'olFilterOnly'|'filter'|'wfsFormatOptions'>;
13+
14+
export const createWfsSearchFunction = <
15+
G extends Geometry = Geometry,
16+
T extends GeoJsonProperties = Record<string, any>,
17+
C extends FeatureCollection<G, T> = FeatureCollection<G, T>
18+
>({
19+
additionalFetchOptions = {},
20+
baseUrl,
21+
featureNS,
22+
featurePrefix,
23+
featureTypes,
24+
geometryName,
25+
maxFeatures,
26+
outputFormat = 'application/json',
27+
srsName = 'EPSG:4326',
28+
attributeDetails,
29+
propertyNames
30+
}: WfsArgs): SearchFunction<G, T, C> => {
31+
32+
return async searchTerm => {
33+
const request = WfsFilterUtil.getCombinedRequests({
34+
featureNS,
35+
featurePrefix,
36+
featureTypes,
37+
geometryName,
38+
maxFeatures,
39+
outputFormat,
40+
srsName,
41+
attributeDetails,
42+
propertyNames
43+
}, searchTerm) as Element;
44+
const requestBody = (new XMLSerializer()).serializeToString(request);
45+
if (!_isNil(request)) {
46+
const response = await fetch(`${baseUrl}`, {
47+
method: 'POST',
48+
credentials: additionalFetchOptions?.credentials ?? 'same-origin',
49+
body: requestBody,
50+
...additionalFetchOptions
51+
});
52+
53+
if (outputFormat.includes('json')) {
54+
return response.json() as unknown as C;
55+
} else {
56+
const responseString = await response.text();
57+
58+
const formatGml = new OlFormatGml3({
59+
featureNS,
60+
srsName
61+
});
62+
63+
const formatGeoJson = new OlFormatGeoJSON();
64+
return formatGeoJson.writeFeaturesObject(formatGml.readFeatures(responseString)) as C;
65+
}
66+
} else {
67+
throw new Error('WFS request is empty/null');
68+
}
69+
};
70+
};
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson';
2+
import _isNil from 'lodash/isNil';
3+
import { useEffect, useState } from 'react';
4+
5+
export type SearchFunction<
6+
G extends Geometry = Geometry,
7+
T extends GeoJsonProperties = Record<string, any>,
8+
C extends FeatureCollection<G, T> = FeatureCollection<G, T>
9+
> =
10+
(searchTerm: string) => Promise<C|undefined>;
11+
12+
export type SearchOptions<
13+
G extends Geometry = Geometry,
14+
T extends NonNullable<GeoJsonProperties> = Record<string, any>,
15+
C extends FeatureCollection<G, T> = FeatureCollection<G, T>
16+
> = {
17+
minChars?: number;
18+
debounceTime?: number;
19+
onFetchError?: (error: any) => void;
20+
onFetchSuccess?: (featureCollection: C|undefined) => void;
21+
};
22+
23+
export const useSearch = <
24+
G extends Geometry = Geometry,
25+
T extends NonNullable<GeoJsonProperties> = Record<string, any>,
26+
C extends FeatureCollection<G, T> = FeatureCollection<G, T>
27+
>(
28+
searchFunction: SearchFunction<G, T, C>,
29+
searchTerm: string | undefined,
30+
{
31+
minChars = 3,
32+
debounceTime = 100,
33+
onFetchError = () => {},
34+
onFetchSuccess = () => {}
35+
}: SearchOptions<G, T, C>
36+
) => {
37+
const [featureCollection, setFeatureCollection] = useState<C>();
38+
const [loading, setLoading] = useState<boolean>(false);
39+
40+
useEffect(() => {
41+
if (!_isNil(searchTerm) && searchTerm.length >= minChars) {
42+
setLoading(true);
43+
44+
const timeout = setTimeout(async () => {
45+
try {
46+
const collection = await searchFunction(searchTerm);
47+
setFeatureCollection(collection);
48+
onFetchSuccess(collection);
49+
} catch (error) {
50+
onFetchError(error);
51+
} finally {
52+
setLoading(false);
53+
}
54+
}, debounceTime);
55+
56+
return () => {
57+
clearTimeout(timeout);
58+
};
59+
60+
} else {
61+
setFeatureCollection(undefined);
62+
setLoading(false);
63+
64+
return undefined;
65+
}
66+
}, [searchFunction, searchTerm]);
67+
68+
return {
69+
loading,
70+
featureCollection
71+
};
72+
};

src/Hooks/useNominatim/useNominatim.spec.tsx

-7
This file was deleted.

0 commit comments

Comments
 (0)