-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: unify functionality of useWfs and useNominatim into on useSearc…
…h 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); ```
- Loading branch information
1 parent
270bbad
commit 9d37d20
Showing
10 changed files
with
297 additions
and
323 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// See https://nominatim.org/release-docs/develop/api/Output/ for some more information | ||
import { UrlUtil } from '@terrestris/base-util'; | ||
import { Feature, Geometry } from 'geojson'; | ||
import { Extent } from 'ol/extent'; | ||
|
||
import { SearchFunction } from './useSearch/useSearch'; | ||
|
||
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: Geometry; | ||
licence: string; | ||
}; | ||
|
||
export type NominatimArgs = { | ||
/** | ||
* The Nominatim Base URL. See https://wiki.openstreetmap.org/wiki/Nominatim | ||
*/ | ||
nominatimBaseUrl?: 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; | ||
/** | ||
* 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; | ||
}; | ||
|
||
export const createNominatimSearchFunction = ({ | ||
addressDetails = 1, | ||
bounded = 1, | ||
countryCodes = 'de', | ||
limit = 10, | ||
nominatimBaseUrl = 'https://nominatim.openstreetmap.org/search?', | ||
searchResultLanguage, | ||
viewBox = '-180,90,180,-90' | ||
}: NominatimArgs = {}): SearchFunction<Geometry, NominatimPlace> => { | ||
return async (searchTerm) => { | ||
const baseParams = { | ||
format: 'json', | ||
viewbox: viewBox, | ||
bounded: bounded, | ||
// eslint-disable-next-line camelcase | ||
polygon_geojson: '1', | ||
addressdetails: addressDetails, | ||
limit: limit, | ||
countrycodes: countryCodes, | ||
q: searchTerm | ||
}; | ||
|
||
const getRequestParams = UrlUtil.objectToRequestString(baseParams); | ||
|
||
let fetchOpts: RequestInit = {}; | ||
if (searchResultLanguage) { | ||
fetchOpts = { | ||
headers: { | ||
'accept-language': searchResultLanguage | ||
} | ||
}; | ||
} | ||
const response = await fetch(`${nominatimBaseUrl}${getRequestParams}`, fetchOpts); | ||
if (!response.ok) { | ||
throw new Error(`Return code: ${response.status}`); | ||
} | ||
const places = await response.json() as NominatimPlace[]; | ||
return { | ||
type: 'FeatureCollection', | ||
features: places.map(p => ({ | ||
type: 'Feature', | ||
geometry: p.geojson, | ||
properties: p | ||
})) | ||
}; | ||
}; | ||
}; | ||
|
||
export const createNominatimGetValueFunction = () => | ||
(feature: Feature<Geometry, NominatimPlace>) => feature.properties.display_name; | ||
|
||
export const createNominatimGetExtentFunction = () => | ||
(feature: Feature<Geometry, NominatimPlace>) => { | ||
const bbox: number[] = feature.properties.boundingbox.map(parseFloat); | ||
return [ | ||
bbox[2], | ||
bbox[0], | ||
bbox[3], | ||
bbox[1] | ||
] as Extent; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import WfsFilterUtil, { SearchConfig } from '@terrestris/ol-util/dist/WfsFilterUtil/WfsFilterUtil'; | ||
import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; | ||
import _isNil from 'lodash/isNil'; | ||
import OlFormatGeoJSON from 'ol/format/GeoJSON'; | ||
import OlFormatGml3 from 'ol/format/GML3'; | ||
|
||
import { SearchFunction } from './useSearch/useSearch'; | ||
|
||
export type WfsArgs = { | ||
additionalFetchOptions?: Partial<RequestInit>; | ||
baseUrl: string; | ||
} & Omit<SearchConfig, 'olFilterOnly'|'filter'|'wfsFormatOptions'>; | ||
|
||
export const createWfsSearchFunction = < | ||
G extends Geometry = Geometry, | ||
T extends GeoJsonProperties = Record<string, any>, | ||
C extends FeatureCollection<G, T> = FeatureCollection<G, T> | ||
>({ | ||
additionalFetchOptions = {}, | ||
baseUrl, | ||
featureNS, | ||
featurePrefix, | ||
featureTypes, | ||
geometryName, | ||
maxFeatures, | ||
outputFormat = 'application/json', | ||
srsName = 'EPSG:4326', | ||
attributeDetails, | ||
propertyNames | ||
}: WfsArgs): SearchFunction<G, T, C> => { | ||
|
||
return async searchTerm => { | ||
const request = WfsFilterUtil.getCombinedRequests({ | ||
featureNS, | ||
featurePrefix, | ||
featureTypes, | ||
geometryName, | ||
maxFeatures, | ||
outputFormat, | ||
srsName, | ||
attributeDetails, | ||
propertyNames | ||
}, searchTerm) as Element; | ||
const requestBody = (new XMLSerializer()).serializeToString(request); | ||
if (!_isNil(request)) { | ||
const response = await fetch(`${baseUrl}`, { | ||
method: 'POST', | ||
credentials: additionalFetchOptions?.credentials ?? 'same-origin', | ||
body: requestBody, | ||
...additionalFetchOptions | ||
}); | ||
|
||
if (outputFormat.includes('json')) { | ||
return response.json() as unknown as C; | ||
} else { | ||
const responseString = await response.text(); | ||
|
||
const formatGml = new OlFormatGml3({ | ||
featureNS, | ||
srsName | ||
}); | ||
|
||
const formatGeoJson = new OlFormatGeoJSON(); | ||
return formatGeoJson.writeFeaturesObject(formatGml.readFeatures(responseString)) as C; | ||
} | ||
} else { | ||
throw new Error('WFS request is empty/null'); | ||
} | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; | ||
import _isNil from 'lodash/isNil'; | ||
import { useEffect, useState } from 'react'; | ||
|
||
export type SearchFunction< | ||
G extends Geometry = Geometry, | ||
T extends GeoJsonProperties = Record<string, any>, | ||
C extends FeatureCollection<G, T> = FeatureCollection<G, T> | ||
> = | ||
(searchTerm: string) => Promise<C|undefined>; | ||
|
||
export type SearchOptions< | ||
G extends Geometry = Geometry, | ||
T extends NonNullable<GeoJsonProperties> = Record<string, any>, | ||
C extends FeatureCollection<G, T> = FeatureCollection<G, T> | ||
> = { | ||
minChars?: number; | ||
debounceTime?: number; | ||
onFetchError?: (error: any) => void; | ||
onFetchSuccess?: (featureCollection: C|undefined) => void; | ||
}; | ||
|
||
export const useSearch = < | ||
G extends Geometry = Geometry, | ||
T extends NonNullable<GeoJsonProperties> = Record<string, any>, | ||
C extends FeatureCollection<G, T> = FeatureCollection<G, T> | ||
>( | ||
searchFunction: SearchFunction<G, T, C>, | ||
searchTerm: string | undefined, | ||
{ | ||
minChars = 3, | ||
debounceTime = 100, | ||
onFetchError = () => {}, | ||
onFetchSuccess = () => {} | ||
}: SearchOptions<G, T, C> | ||
) => { | ||
const [featureCollection, setFeatureCollection] = useState<C>(); | ||
const [loading, setLoading] = useState<boolean>(false); | ||
|
||
useEffect(() => { | ||
if (!_isNil(searchTerm) && searchTerm.length >= minChars) { | ||
setLoading(true); | ||
|
||
const timeout = setTimeout(async () => { | ||
try { | ||
const collection = await searchFunction(searchTerm); | ||
setFeatureCollection(collection); | ||
onFetchSuccess(collection); | ||
} catch (error) { | ||
onFetchError(error); | ||
} finally { | ||
setLoading(false); | ||
} | ||
}, debounceTime); | ||
|
||
return () => { | ||
clearTimeout(timeout); | ||
}; | ||
|
||
} else { | ||
setFeatureCollection(undefined); | ||
setLoading(false); | ||
|
||
return undefined; | ||
} | ||
}, [searchFunction, searchTerm]); | ||
|
||
return { | ||
loading, | ||
featureCollection | ||
}; | ||
}; |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.