From f83539bc7c01265c0fc9a9bdba191c7b25ed8f75 Mon Sep 17 00:00:00 2001 From: Simon Seyock Date: Wed, 29 May 2024 15:43:38 +0200 Subject: [PATCH] feat: unify functionality of WfsField and NominatimField into SearchField BREAKING CHANGE: The `WfsField` and `NominatimField` hooks are removed. Please check the example for `SearchField`. --- package-lock.json | 5 +- package.json | 2 +- .../NominatimSearch.example.md | 58 ---- .../NominatimSearch/NominatimSearch.less | 3 - .../NominatimSearch/NominatimSearch.spec.tsx | 137 ---------- src/Field/NominatimSearch/NominatimSearch.tsx | 190 ------------- src/Field/SearchField/SearchField.example.md | 100 +++++++ src/Field/SearchField/SearchField.less | 3 + src/Field/SearchField/SearchField.tsx | 121 +++++++++ .../WfsSearchField/WfsSearchField.example.md | 120 -------- src/Field/WfsSearchField/WfsSearchField.less | 3 - .../WfsSearchField/WfsSearchField.spec.tsx | 23 -- src/Field/WfsSearchField/WfsSearchField.tsx | 257 ------------------ src/index.ts | 6 +- 14 files changed, 230 insertions(+), 798 deletions(-) delete mode 100644 src/Field/NominatimSearch/NominatimSearch.example.md delete mode 100644 src/Field/NominatimSearch/NominatimSearch.less delete mode 100644 src/Field/NominatimSearch/NominatimSearch.spec.tsx delete mode 100644 src/Field/NominatimSearch/NominatimSearch.tsx create mode 100644 src/Field/SearchField/SearchField.example.md create mode 100644 src/Field/SearchField/SearchField.less create mode 100644 src/Field/SearchField/SearchField.tsx delete mode 100644 src/Field/WfsSearchField/WfsSearchField.example.md delete mode 100644 src/Field/WfsSearchField/WfsSearchField.less delete mode 100644 src/Field/WfsSearchField/WfsSearchField.spec.tsx delete mode 100644 src/Field/WfsSearchField/WfsSearchField.tsx diff --git a/package-lock.json b/package-lock.json index d0feb748cc..a2a99487d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "@terrestris/base-util": "^2.0.0", "@terrestris/ol-util": ">=18", "@terrestris/react-util": "^6.0.2", - "@types/geojson": "^7946.0.14", "@types/lodash": "^4.17.4", "ag-grid-community": "^31.3.2", "ag-grid-react": "^31.3.2", @@ -48,6 +47,7 @@ "@testing-library/jest-dom": "^6.4.5", "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.5.2", + "@types/geojson": "^7946.0.14", "@types/jest": "^29.5.12", "@types/node": "^20.12.10", "@types/react": "^18.3.3", @@ -7688,7 +7688,8 @@ "node_modules/@types/geojson": { "version": "7946.0.14", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", - "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "dev": true }, "node_modules/@types/glob": { "version": "7.2.0", diff --git a/package.json b/package.json index 17cab5d2c4..b5bddbe4e2 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "@terrestris/base-util": "^2.0.0", "@terrestris/ol-util": ">=18", "@terrestris/react-util": "^6.0.2", - "@types/geojson": "^7946.0.14", "@types/lodash": "^4.17.4", "ag-grid-community": "^31.3.2", "ag-grid-react": "^31.3.2", @@ -106,6 +105,7 @@ "@testing-library/jest-dom": "^6.4.5", "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.5.2", + "@types/geojson": "^7946.0.14", "@types/jest": "^29.5.12", "@types/node": "^20.12.10", "@types/react": "^18.3.3", diff --git a/src/Field/NominatimSearch/NominatimSearch.example.md b/src/Field/NominatimSearch/NominatimSearch.example.md deleted file mode 100644 index 65d2203821..0000000000 --- a/src/Field/NominatimSearch/NominatimSearch.example.md +++ /dev/null @@ -1,58 +0,0 @@ -This demonstrates the usage of the NominatimSearch. - -```jsx -import NominatimSearch from '@terrestris/react-geo/dist/Field/NominatimSearch/NominatimSearch'; -import MapComponent from '@terrestris/react-geo/dist/Map/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'; -import OlSourceOSM from 'ol/source/OSM'; -import OlView from 'ol/View'; -import {useEffect, useState} from 'react'; - -const NominatimSearchExample = () => { - const [map, setMap] = useState(); - - useEffect(() => { - const newMap = new OlMap({ - layers: [ - new OlLayerTile({ - name: 'OSM', - source: new OlSourceOSM() - }) - ], - view: new OlView({ - center: fromLonLat([37.40570, 8.81566]), - zoom: 4 - }) - }); - - setMap(newMap); - }, []); - - if (!map) { - return null; - } - - return ( - - -
- -
- - -
- ); -}; - - -``` diff --git a/src/Field/NominatimSearch/NominatimSearch.less b/src/Field/NominatimSearch/NominatimSearch.less deleted file mode 100644 index a0176ba4f8..0000000000 --- a/src/Field/NominatimSearch/NominatimSearch.less +++ /dev/null @@ -1,3 +0,0 @@ -.react-geo-nominatimsearch { - width: 100%; -} diff --git a/src/Field/NominatimSearch/NominatimSearch.spec.tsx b/src/Field/NominatimSearch/NominatimSearch.spec.tsx deleted file mode 100644 index 3e6f195d52..0000000000 --- a/src/Field/NominatimSearch/NominatimSearch.spec.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { actSetTimeout, renderInMapContext } from '@terrestris/react-util/dist/Util/rtlTestUtils'; -import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { enableFetchMocks, FetchMock } from 'jest-fetch-mock'; -import OlLayerTile from 'ol/layer/Tile'; -import OlMap from 'ol/Map'; -import OlSourceOsm from 'ol/source/OSM'; -import OlView from 'ol/View'; -import * as React from 'react'; - -import NominatimSearch from '../NominatimSearch/NominatimSearch'; - -describe('', () => { - let map: OlMap; - - const nominatimBoundingBox = ['52.7076346', '52.7476346', '7.7702617', '7.8102617']; - - const resultMock = [{ - // eslint-disable-next-line camelcase - place_id: 752526, - // eslint-disable-next-line camelcase - display_name: 'Böen, Löningen, Landkreis Cloppenburg, Niedersachsen, Deutschland', - boundingbox: nominatimBoundingBox - }, { - // eslint-disable-next-line camelcase - place_id: 123, - // eslint-disable-next-line camelcase - display_name: 'peter' - }]; - - beforeAll(() => { - enableFetchMocks(); - (fetch as FetchMock).mockResponse(JSON.stringify(resultMock)); - }); - - beforeEach(() => { - map = new OlMap({ - layers: [ - new OlLayerTile({ - properties: { - name: 'OSM' - }, - source: new OlSourceOsm() - }) - ], - view: new OlView({ - projection: 'EPSG:4326', - center: [37.40570, 8.81566], - zoom: 4 - }) - }); - }); - - it('is defined', () => { - expect(NominatimSearch).not.toBeUndefined(); - }); - - it('can be rendered', () => { - renderInMapContext(map, ); - - const button = screen.getByRole('combobox'); - expect(button).toBeVisible(); - }); - - it('performs a search and shows options', async () => { - renderInMapContext(map, ); - - const input = screen.getByRole('combobox'); - - await userEvent.type(input, 'Cl'); - - await actSetTimeout(500); - - let options = screen.queryAllByRole('option'); - - // eslint-disable-next-line jest-dom/prefer-in-document - expect(options).toHaveLength(0); - - await userEvent.type(input, 'oppenburg'); - - await actSetTimeout(500); - - options = screen.queryAllByRole('option'); - - // eslint-disable-next-line jest-dom/prefer-in-document - expect(options).toHaveLength(1); - expect(options[0]).toHaveTextContent(resultMock[0].display_name); - }); - - it('sets the text value of the search to the select item and zooms to the bounding box', async () => { - renderInMapContext(map, ); - - const fitSpy = jest.spyOn(map.getView(), 'fit'); - - const input = screen.getByRole('combobox'); - - await userEvent.type(input, 'Cloppenburg'); - - const option = await screen.findByText(resultMock[0].display_name, { - selector: '.ant-select-item-option-content' - }); - - await userEvent.click(option); - - expect(input).toHaveValue(resultMock[0].display_name); - - expect(fitSpy).toBeCalled(); - - fitSpy.mockRestore(); - }); - - it('calls a custom select callback', async () => { - const selectSpy = jest.fn(); - - renderInMapContext(map, ); - - const fitSpy = jest.spyOn(map.getView(), 'fit'); - - const input = screen.getByRole('combobox'); - - await userEvent.type(input, 'Cloppenburg'); - - const option = await screen.findByText(resultMock[0].display_name, { - selector: '.ant-select-item-option-content' - }); - - await userEvent.click(option); - - expect(fitSpy).toHaveBeenCalledTimes(0); - - expect(selectSpy).toBeCalled(); - expect(selectSpy.mock.calls[0][0]).toStrictEqual(resultMock[0]); - - fitSpy.mockRestore(); - selectSpy.mockRestore(); - }); -}); diff --git a/src/Field/NominatimSearch/NominatimSearch.tsx b/src/Field/NominatimSearch/NominatimSearch.tsx deleted file mode 100644 index 879a40bc47..0000000000 --- a/src/Field/NominatimSearch/NominatimSearch.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import './NominatimSearch.less'; - -import { useMap } from '@terrestris/react-util/dist/Hooks/useMap/useMap'; -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 { transformExtent } from 'ol/proj'; -import { DefaultOptionType } from 'rc-select/lib/Select'; -import React, { FC, useCallback, useState } from 'react'; - -import { CSS_PREFIX } from '../../constants'; - -const Option = AutoComplete.Option; - -interface OwnProps { - /** - * A render function which gets called with the selected item as it is - * returned by nominatim. It must return an `AutoComplete.Option`. - */ - renderOption?: (item: NominatimPlace) => React.ReactElement; - /** - * An onSelect function which gets called with the selected item as it is - * returned by nominatim. - */ - 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 - */ - visible?: boolean; - /** - * The searchTerm may be given as prop. This is useful when setting - * `visible` to `false`. - */ - searchTerm?: string; - /** - * An optional CSS class which should be added. - */ - className?: string; - /** - * A function that gets called when the clear Button is pressed or the input - * value is empty. - */ - onClear?: () => void; -} - -export type NominatimSearchProps = OwnProps & - Omit & Omit; - -/** - * The NominatimSearch. - */ -export const NominatimSearch: FC = ({ - addressDetails, - bounded, - className = `${CSS_PREFIX}nominatimsearch`, - countryCodes, - debounceTime, - format, - limit, - minChars, - nominatimBaseUrl, - onChange = () => undefined, - onClear, - onFetchError, - onFetchSuccess, - onSelect, - polygonGeoJSON, - renderOption, - searchResultLanguage, - viewBox, - visible = true, - ...passThroughProps -}) => { - - const [searchTerm, setSearchTerm] = useState(''); - const map = useMap(); - - 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); - } else if (selected && selected.boundingbox) { - const olView = map?.getView(); - const bbox: number[] = selected.boundingbox.map(parseFloat); - let extent = [ - bbox[2], - bbox[0], - bbox[3], - bbox[1] - ] as OlExtent; - - extent = transformExtent(extent, 'EPSG:4326', - olView?.getProjection().getCode()); - - olView?.fit(extent, { - duration: 500 - }); - } - }, [map, onSelect]); - - const finalRenderOption = useCallback((item: NominatimPlace): React.ReactElement => { - if (_isNil(item)) { - return <>; - } - if (renderOption) { - return renderOption(item); - } else { - return ( - - ); - } - }, [renderOption]); - - /** - * The function describes what to do when an item is selected. - * - * @param option The selected OptionData - */ - const onMenuItemSelected = useCallback((_: any, option: DefaultOptionType) => { - if (!map) { - return; - } - const selected = nominatimResults?.find( - i => `${i.place_id}` === option.key - ); - if (!_isNil(selected)) { - finalOnSelect(selected); - } - }, [finalOnSelect, nominatimResults, map]); - - const onValueChange = (value: string, place: NominatimPlace) => { - setSearchTerm(value); - if (!_isNil(place)) { - onChange(value, { - ...place, - label: place.display_name - }); - } else { - onChange(value, place); - } - }; - - if (!visible) { - return null; - } - - return ( - - className={className} - allowClear={true} - placeholder="Ortsname, Straßenname, Stadtteilname, POI usw." - onChange={onValueChange} - onSelect={onMenuItemSelected} - {...passThroughProps} - > - { - nominatimResults?.map(finalRenderOption) - } - - ); -}; - -export default NominatimSearch; diff --git a/src/Field/SearchField/SearchField.example.md b/src/Field/SearchField/SearchField.example.md new file mode 100644 index 0000000000..4911e4b65d --- /dev/null +++ b/src/Field/SearchField/SearchField.example.md @@ -0,0 +1,100 @@ +This demonstrates the usage of the SearchField with nominatim and wfs examples. + +```jsx +import SearchField from '@terrestris/react-geo/dist/Field/SearchField/SearchField'; +import MapComponent from '@terrestris/react-geo/dist/Map/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'; +import OlSourceOSM from 'ol/source/OSM'; +import OlView from 'ol/View'; +import {useCallback, useEffect, useState} from 'react'; +import { + createNominatimSearchFunction, + createNominatimGetValueFunction, + createNominatimGetExtentFunction, + createWfsSearchFunction +} from "@terrestris/react-util"; + +const SearchFieldExample = () => { + const [map, setMap] = useState(); + + const nominatimSearchFunction = useCallback(createNominatimSearchFunction({}), []); + const nominatimGetValue = useCallback(createNominatimGetValueFunction(), []); + const nominatimGetExtent = useCallback(createNominatimGetExtentFunction(), []); + + const wfsSearchFunction = useCallback(createWfsSearchFunction({ + baseUrl: 'https://ows-demo.terrestris.de/geoserver/osm/wfs', + featureTypes: ['osm:osm-country-borders'], + featureNS: 'osm', + maxFeatures: 3, + attributeDetails: { + 'osm:osm-country-borders': { + name: { + type: 'string', + exactSearch: false, + matchCase: false + } + } + } + }), []); + const wfsGetValue = useCallback(f => f.properties.name, []); + + useEffect(() => { + const newMap = new OlMap({ + layers: [ + new OlLayerTile({ + name: 'OSM', + source: new OlSourceOSM() + }) + ], + view: new OlView({ + center: fromLonLat([37.40570, 8.81566]), + zoom: 4 + }) + }); + + setMap(newMap); + }, []); + + if (!map) { + return null; + } + + return ( + + +
+ +
+ +
+ +
+ + +
+ ); +}; + + +``` diff --git a/src/Field/SearchField/SearchField.less b/src/Field/SearchField/SearchField.less new file mode 100644 index 0000000000..1fd5aab364 --- /dev/null +++ b/src/Field/SearchField/SearchField.less @@ -0,0 +1,3 @@ +.react-geo-search { + width: 100%; +} diff --git a/src/Field/SearchField/SearchField.tsx b/src/Field/SearchField/SearchField.tsx new file mode 100644 index 0000000000..eb826cdb23 --- /dev/null +++ b/src/Field/SearchField/SearchField.tsx @@ -0,0 +1,121 @@ +import './SearchField.less'; + +import { SearchFunction, SearchOptions,useSearch } from '@terrestris/react-util'; +import useMap from '@terrestris/react-util/dist/Hooks/useMap/useMap'; +import { AutoComplete, Spin } from 'antd'; +import { AutoCompleteProps } from 'antd/lib/auto-complete'; +import { Feature,FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; +import { Extent } from 'ol/extent'; +import OlFormatGeoJSON from 'ol/format/GeoJSON'; +import { transformExtent } from 'ol/proj'; +import React, { ReactElement, useCallback, useMemo, useState } from 'react'; + +import { CSS_PREFIX } from '../../constants'; + +export type SearchProps< + G extends Geometry = Geometry, + T extends NonNullable = Record, + C extends FeatureCollection = FeatureCollection +> = { + searchFunction: SearchFunction; + searchOptions?: SearchOptions; + getValue: (feature: Feature) => string; + /** + * An onSelect function which gets called with the selected item as it is + * returned by nominatim. + */ + onSelect?: (feature: Feature) => void; + /** + * An optional CSS class which should be added. + */ + className?: string; + /** + * A function that gets called when the clear Button is pressed or the input + * value is empty. + */ + onClear?: () => void; + zoomToFeature?: boolean; + getExtent?: (feature: Feature) => Extent; +} & Omit; + +/** + * The NominatimSearch. + */ +export function SearchField< + G extends Geometry = Geometry, + T extends NonNullable = Record, + C extends FeatureCollection = FeatureCollection +>({ + className = `${CSS_PREFIX}search`, + onSelect, + getValue, + searchFunction, + searchOptions = {}, + zoomToFeature = true, + getExtent, + ...passThroughProps +}: SearchProps): ReactElement { + + const [searchTerm, setSearchTerm] = useState(''); + const map = useMap(); + + const { + featureCollection, + loading + } = useSearch(searchFunction, searchTerm, searchOptions); + + const options = useMemo( + () => featureCollection?.features.map(f => ({ + label: getValue(f), + value: getValue(f) + })), + [featureCollection, getValue] + ); + + const onMenuItemSelected = useCallback((value: string) => { + const selected = featureCollection?.features.find(f => getValue(f) === value); + if (selected && onSelect) { + onSelect(selected); + } + if (selected && zoomToFeature) { + if (!map) { + return; + } + let extent: Extent; + if (getExtent) { + extent = getExtent(selected); + } else { + const olFormat = new OlFormatGeoJSON(); + const geometry = olFormat.readGeometry(selected.geometry); + extent = geometry.getExtent(); + } + + const olView = map?.getView(); + + extent = transformExtent(extent, 'EPSG:4326', olView.getProjection()); + + olView.fit(extent, { + duration: 500 + }); + } + }, [map, onSelect, getValue, getExtent, featureCollection?.features, zoomToFeature]); + + return ( + + setSearchTerm(text) + } + onClear={() => + setSearchTerm('') + } + onSelect={onMenuItemSelected} + options={options} + notFoundContent={loading ? : null} + {...passThroughProps} + /> + ); +} + +export default SearchField; diff --git a/src/Field/WfsSearchField/WfsSearchField.example.md b/src/Field/WfsSearchField/WfsSearchField.example.md deleted file mode 100644 index 306102bcc5..0000000000 --- a/src/Field/WfsSearchField/WfsSearchField.example.md +++ /dev/null @@ -1,120 +0,0 @@ -This demonstrates the usage of the WfsSearch. -Type a country name in its own language… - -```jsx -import WfsSearch from '@terrestris/react-geo/dist/Field/WfsSearchField/WfsSearchField'; -import AgFeatureGrid from '@terrestris/react-geo/dist/Grid/AgFeatureGrid/AgFeatureGrid'; -import MapComponent from '@terrestris/react-geo/dist/Map/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'; -import OlSourceOSM from 'ol/source/OSM'; -import OlView from 'ol/View'; -import { useEffect, useState } from 'react'; - -const WfsSearchFieldExample = () => { - - const [map, setMap] = useState(); - const [inputFeatures, setInputFeatures] = useState([]); - - useEffect(() => { - const newMap = new OlMap({ - layers: [ - new OlLayerTile({ - name: 'OSM', - source: new OlSourceOSM() - }) - ], - view: new OlView({ - center: fromLonLat([37.40570, 8.81566]), - zoom: 4 - }) - }); - setMap(newMap); - }, []); - - if (!map) { - return null; - } - - const onFeaturesChange = f => setInputFeatures(f); - - return ( - -
- -
-
- -
- -
- ); -}; - -``` diff --git a/src/Field/WfsSearchField/WfsSearchField.less b/src/Field/WfsSearchField/WfsSearchField.less deleted file mode 100644 index 98b696eca5..0000000000 --- a/src/Field/WfsSearchField/WfsSearchField.less +++ /dev/null @@ -1,3 +0,0 @@ -.react-geo-wfssearch { - width: 100%; -} diff --git a/src/Field/WfsSearchField/WfsSearchField.spec.tsx b/src/Field/WfsSearchField/WfsSearchField.spec.tsx deleted file mode 100644 index 1d1b53aa46..0000000000 --- a/src/Field/WfsSearchField/WfsSearchField.spec.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { render } from '@testing-library/react'; -import React from 'react'; - -import WfsSearchField from './WfsSearchField'; - -describe('', () => { - it('is defined', () => { - expect(WfsSearchField).not.toBeUndefined(); - }); - - it('can be rendered', () => { - const { container } = render( - - ); - expect(container).not.toBeUndefined(); - }); - -}); diff --git a/src/Field/WfsSearchField/WfsSearchField.tsx b/src/Field/WfsSearchField/WfsSearchField.tsx deleted file mode 100644 index 5d2c700cf0..0000000000 --- a/src/Field/WfsSearchField/WfsSearchField.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import './WfsSearchField.less'; - -import { faCircleNotch, faClose } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import Logger from '@terrestris/base-util/dist/Logger'; -import { SearchConfig } from '@terrestris/ol-util/dist/WfsFilterUtil/WfsFilterUtil'; -import useMap from '@terrestris/react-util/dist/Hooks/useMap/useMap'; -import { useWfs, WfsQueryArgs } from '@terrestris/react-util/dist/Hooks/useWfs/useWfs'; -import { AutoComplete, Input, Spin } from 'antd'; -import { AutoCompleteProps } from 'antd/lib/auto-complete'; -import { DefaultOptionType, OptionProps } from 'antd/lib/select'; -import _get from 'lodash/get'; -import _isNil from 'lodash/isNil'; -import _isString from 'lodash/isString'; -import OlFeature from 'ol/Feature'; -import React, { FC, useEffect, useMemo, useState } from 'react'; - -import { CSS_PREFIX } from '../../constants'; - -const Option = AutoComplete.Option; - -export type WfsSearchFieldProps = { - asInput?: boolean; - className?: string; - displayValue?: string; - idProperty?: string; - onBeforeSearch?: (value: string) => string; - onChange?: (val: OlFeature[] | undefined) => undefined; - value?: OlFeature[] | undefined; - visible?: boolean; -} & SearchConfig & Omit & Omit; - -const defaultClassName = `${CSS_PREFIX}wfssearch`; - -/** - * The WfsSearchField field. - * Implements a field to do a WFS-GetFeature request. - * - * The GetFeature request is created with `ol.format.WFS.writeGetFeature` - * so most of the WFS specific options work like document in the corresponding - * API-docs: https://openlayers.org/en/latest/apidoc/module-ol_format_WFS.html - * - */ -export const WfsSearchField: FC = ({ - additionalFetchOptions, - asInput = false, - attributeDetails = {}, - baseUrl, - className, - displayValue = 'name', - featureNS, - featurePrefix, - featureTypes, - geometryName, - idProperty = 'id', - maxFeatures, - minChars = 3, - onBeforeSearch = v => v, - onChange = () => undefined, - outputFormat = 'application/json', - propertyNames, - srsName = 'EPSG:3857', - value, - wfsFormatOptions, - onFetchError, - onFetchSuccess, - visible, - ...passThroughProps -}) => { - - const [searchTerm, setSearchTerm] = useState(); - - const map = useMap(); - - const searchConfig: SearchConfig = useMemo(() => ({ - attributeDetails, - featureNS, - featurePrefix, - featureTypes, - geometryName, - maxFeatures, - outputFormat, - propertyNames, - srsName, - wfsFormatOptions - }), [ - attributeDetails, - featureNS, - featurePrefix, - featureTypes, - geometryName, - maxFeatures, - outputFormat, - propertyNames, - srsName, - wfsFormatOptions - ]); - - /** - * This function gets called when the WFS GetFeature fetch request returns an error. - * It logs the error to the console. - * - * @param error The error string. - */ - const onFetchErrorInternal = (error: any) => { - const msg = `Error while requesting WFS GetFeature: ${error}`; - Logger.error(msg); - onFetchError?.(msg); - }; - - const { - loading, - features - } = useWfs({ - baseUrl, - minChars, - onFetchError: onFetchErrorInternal, - onFetchSuccess, - searchTerm, - searchConfig - }); - - /** - * Create an AutoComplete.Option from the given data. - * - * @param feature The feature as returned by the server. - * @return The AutoComplete.Option that will be - * rendered for each feature. - */ - const renderOption = (feature?: OlFeature): React.ReactElement => { - if (_isNil(feature)) { - return <>; - } - - const v = _get(feature?.getProperties(), displayValue); - const display = _isNil(v) ? feature.get(idProperty) : v; - return ( - - ); - }; - - /** - * The default onSelect method if no onSelect prop is given. It zooms to the - * selected item. - * - * @param feature The selected feature as returned by the server. - */ - const onSelect = (feature: OlFeature) => { - if (!_isNil(feature) && !_isNil(map)) { - const olView = map.getView(); - const geometry = feature.getGeometry()?.getExtent(); - if (!_isNil(geometry)) { - olView.fit(geometry, { - duration: 500 - }); - } - } - }; - - /** - * Called if the input is being updated. It sets the current inputValue - * as searchTerm and starts a search if the inputValue has - * a length of at least `this.props.minChars` (default 3). - * - * Gets undefined if clear btn is pressed. - * @param val - */ - const onUpdateInput = (val: any) => { - let updatedValue = ''; - if (_isString(val)) { - updatedValue = onBeforeSearch(val); - } else { - if (val?.target?.value) { - updatedValue = onBeforeSearch(val.target.value); - } - } - setSearchTerm(updatedValue); - return; - }; - - /** - * The function describes what to do when an item is selected. - * - * @param _ - * @param option The selected option. - */ - const onMenuItemSelected = (_: string, option: DefaultOptionType): void => { - const selectedFeatures = features?.filter(feat => `${feat.get(idProperty)}` === option.key); - if (!_isNil(selectedFeatures) && Array.isArray(selectedFeatures) && selectedFeatures?.length > 0) { - onSelect(selectedFeatures[0]); - } - }; - - /** - * Resets input field value and previously fetched data on reset button click. - */ - const resetSearch = () => setSearchTerm(undefined); - - useEffect(() => { - onChange(features); - }, [onChange, features]); - - const finalClassName = className ? `${className} ${defaultClassName}` : defaultClassName; - - if (asInput) { - return ( - : - - } - value={searchTerm} - /> - ); - } - - if (!visible) { - return null; - } - - return ( - - allowClear={true} - className={finalClassName} - defaultActiveFirstOption={false} - filterOption={false} - notFoundContent={loading ? : null} - onChange={onUpdateInput} - onSelect={onMenuItemSelected} - suffixIcon={null} - value={searchTerm} - {...passThroughProps} - > - { - features?.map(renderOption) - } -
- ); -}; - -export default WfsSearchField; diff --git a/src/index.ts b/src/index.ts index b55de81fca..39b1acc671 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,9 +18,7 @@ import AddWmsPanel from './Container/AddWmsPanel/AddWmsPanel'; import CoordinateInfo from './CoordinateInfo/CoordinateInfo'; import FeatureLabelModal from './FeatureLabelModal/FeatureLabelModal'; import CoordinateReferenceSystemCombo from './Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo'; -import NominatimSearch from './Field/NominatimSearch/NominatimSearch'; import ScaleCombo from './Field/ScaleCombo/ScaleCombo'; -import WfsSearchField from './Field/WfsSearchField/WfsSearchField'; import AgFeatureGrid from './Grid/AgFeatureGrid/AgFeatureGrid'; import FeatureGrid from './Grid/FeatureGrid/FeatureGrid'; import PropertyGrid from './Grid/PropertyGrid/PropertyGrid'; @@ -33,6 +31,8 @@ import LayerTransparencySlider from './Slider/LayerTransparencySlider/LayerTrans import MultiLayerSlider from './Slider/MultiLayerSlider/MultiLayerSlider'; import TimeSlider from './Slider/TimeSlider/TimeSlider'; +export { SearchField } from './Field/SearchField/SearchField'; + export { AddWmsLayerEntry, AddWmsPanel, @@ -54,7 +54,6 @@ export { MeasureButton, ModifyButton, MultiLayerSlider, - NominatimSearch, PropertyGrid, ScaleCombo, SearchResultsPanel, @@ -65,7 +64,6 @@ export { ToggleButton, ToggleGroup, UploadButton, - WfsSearchField, ZoomButton, ZoomToExtentButton };