import * as React from 'react' ; import { AutoComplete , Spin } from 'antd' ; import { AutoCompleteProps } from 'antd/lib/auto-complete' ; const Option = AutoComplete . Option ; import { OptionProps } from 'antd/lib/select' ; import OlMap from 'ol/Map' ; import OlFormatGeoJSON from 'ol/format/GeoJSON' ; import OlSimpleGeometry from 'ol/geom/SimpleGeometry' ; import _isFunction from 'lodash/isFunction' ; import _debounce from 'lodash/debounce' ; import Logger from '@terrestris/base-util/dist/Logger' ; import WfsFilterUtil from '@terrestris/ol-util/dist/WfsFilterUtil/WfsFilterUtil' ; import { CSS_PREFIX } from '../../constants' ; import './WfsSearch.less' ; interface DefaultProps { /**
     * A nested object mapping feature types to an object of attribute details,
     * which are also mapped by search attribute name.
     *
     * Example:
     *   ```
     *   attributeDetails: {
     *    featType1: {
     *      attr1: {
     *        matchCase: true,
     *        type: 'number',
     *        exactSearch: false
     *      },
     *      attr2: {
     *        matchCase: false,
     *        type: 'string',
     *        exactSearch: true
     *      }
     *    },
     *    featType2: {...}
     *   }
     *   ```
     */ attributeDetails : { [ featureType ] : { [ attributeName ] : { matchCase : boolean ; type : string ; exactSearch : boolean ; } ; } ; } ; /**
     * SRS name. No srsName attribute will be set on geometries when this is not
     * provided.
     */ srsName : string ; /**
     * The output format of the response.
     */ outputFormat : string ; /**
     * Options which are added to the fetch-POST-request. credentials is set to
     * 'same-origin' as default but can be overwritten. See also
     * https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
     */ additionalFetchOptions : any ; /**
     * The minimal amount of characters entered in the input to start a search.
     */ minChars : number ; /**
     * Which prop value of option will render as content of select.
     */ displayValue : string ; /**
     * The id property of the feature. Default is to `id`.
     */ idProperty : string ; /**
     * Delay in ms before actually sending requests.
     */ delay : number ; /**
     * A render function which gets called with the selected item as it is
     * returned by the server. It must return an `AutoComplete.Option` with
     * `key={feature.id}`.
     * The default will display the property `name` if existing or the
     * property defined in `props.idProperty` (default is to `id`).
     */ renderOption : ( feature : any , props : WfsSearchProps ) => React . ReactElement < OptionProps > ; /**
     * An onSelect function which gets called with the selected feature as it is
     * returned by server.
     * The default function will create a searchlayer, adds the feature and will
     * zoom to its extend.
     */ onSelect : ( feature : any , olMap : OlMap ) => void ; } interface BaseProps { /**
     * An optional CSS class which should be added.
     */ className ? : string ; /**
     * The base URL. Please make sure that the WFS-Server supports CORS.
     */ baseUrl : string ; /**
     * An object mapping feature types to an array of attributes that should be searched through.
     */ searchAttributes : { [ featureType ] : string [ ] ; } ; /**
     * The namespace URI used for features.
     */ featureNS ? : string ; /**
     * The prefix for the feature namespace.
     */ featurePrefix ? : string ; /**
     * The feature type names.
     */ featureTypes : string [ ] ; /**
     * Maximum number of features to fetch.
     */ maxFeatures ? : number ; /**
     * Geometry name to use in a BBOX filter.
     */ geometryName ? : string ; /**
     * Optional list of property names to serialize.
     */ propertyNames ? : string [ ] ; /**
     * Filter condition. See https://openlayers.org/en/latest/apidoc/module-ol_format_filter.html
     * for more information.
     */ filter ? : any ; /**
     * The ol.map to interact with on selection.
     */ map : OlMap ; /**
     * An onChange function which gets called with the current value of the
     * field.
     */ onChange ? : ( value : string ) => void ; /**
     * Optional callback function, that will be called before WFS search starts.
     * Can be useful if input value manipulation is needed (e.g. umlaut
     * replacement `ä => oa` etc.)
     */ onBeforeSearch ? : ( value : string ) => string ; /**
     * Options which are passed to the constructor of the ol.format.WFS.
     * compare: https://openlayers.org/en/latest/apidoc/module-ol_format_WFS.html
     */ wfsFormatOptions ? : any ; } interface WfsSearchState { searchTerm : string ; data : Array < any > ; fetching : boolean ; } export type WfsSearchProps = BaseProps & Partial < DefaultProps > & AutoCompleteProps ; class extends React . Component export class WfsSearch extends React . Component < WfsSearchProps , WfsSearchState > { static defaultProps : DefaultProps = { srsName : 'EPSG:3857' , outputFormat : 'application/json' , minChars : 3 , additionalFetchOptions : { } , displayValue : 'name' , idProperty : 'id' , attributeDetails : { } , delay : 300 , param feature param props return renderOption : ( feature , props ) : React . ReactElement < OptionProps > => { const { displayValue , idProperty } = props ; const display = feature . properties [ displayValue ] ? feature . properties [ displayValue ] : feature [ idProperty ] ; return ( < Option value = { display } key = { feature [ idProperty ] } title = { display } > { display } < /Option>); } , param feature param olMap onSelect : ( feature , olMap ) => { if ( feature ) { const olView = olMap . getView ( ) ; const geoJsonFormat = new OlFormatGeoJSON ( ) ; const olFeature = geoJsonFormat . readFeature ( feature ) ; const geometry = olFeature . getGeometry ( ) as OlSimpleGeometry ; if ( geometry ) { olView . fit ( geometry , { duration : 500 } ) ; } } } } ; private className = `${ CSS_PREFIX }wfssearch` ; param props constructs constructor ( props ) { super ( props ) ; this . state = { searchTerm : '' , data : [ ] , fetching : false } ; this . onUpdateInput = this . onUpdateInput . bind ( this ) ; this . onMenuItemSelected = this . onMenuItemSelected . bind ( this ) ; this . doSearch = _debounce ( this . doSearch , this . props . delay ) ; } param value onUpdateInput ( value ? ) { const { minChars , onChange , onBeforeSearch } = this . props ; this . setState ( { data : [ ] } ) ; if ( _isFunction ( onBeforeSearch ) ) { value = onBeforeSearch ( value ) ; } this . setState ( { searchTerm : value } , ( ) => { if ( this . state . searchTerm && this . state . searchTerm . length >= minChars ) { this . doSearch ( ) ; } } ) ; if ( _isFunction ( onChange ) ) { onChange ( value ) ; } } private doSearch ( ) { const { additionalFetchOptions , baseUrl , featureNS , featurePrefix , featureTypes , geometryName , maxFeatures , outputFormat , propertyNames , srsName , wfsFormatOptions , searchAttributes , attributeDetails } = this . props ; const searchOpts = { featureNS , featurePrefix , featureTypes , geometryName , maxFeatures , outputFormat , propertyNames , srsName , wfsFormatOptions , searchAttributes , attributeDetails } ; const request = WfsFilterUtil . getCombinedRequests ( searchOpts , this . state . searchTerm ) ; if ( request ) { this . setState ( { fetching : true } , ( ) => { fetch ( `${ baseUrl }` , { method : 'POST' , credentials : additionalFetchOptions . credentials ? additionalFetchOptions . credentials : 'same-origin' , body : new XMLSerializer ( ) . serializeToString ( request ) , ... additionalFetchOptions } ) . then ( response => response . json ( ) ) . then ( this . onFetchSuccess . bind ( this ) ) . catch ( this . onFetchError . bind ( this ) ) ; } ) ; } else { this . onFetchError ( 'Missing GetFeature request parameters' ) ; } } param response onFetchSuccess ( response ) { const data = response . features ? response . features : [ ] ; data . forEach ( ( feature ) => feature . searchTerm = this . state . searchTerm ) ; this . setState ( { data , fetching : false } ) ; } param error onFetchError ( error ) { Logger . error ( `Error while requesting WFS GetFeature: ${ error }` ) ; this . setState ( { fetching : false } ) ; } param value param option onMenuItemSelected ( value , option ) { const { map , idProperty } = this . props ; const selectedFeature = this . state . data . filter ( feat => feat [ idProperty ] === option . key ) [ 0 ] ; this . props . onSelect ( selectedFeature , map ) ; } /**
     * The render function.
     */ render ( ) { const { data , fetching } = this . state ; const { additionalFetchOptions , baseUrl , className , featureNS , featurePrefix , featureTypes , filter , geometryName , map , maxFeatures , minChars , outputFormat , onChange , onSelect , propertyNames , renderOption , searchAttributes , attributeDetails , srsName , wfsFormatOptions , displayValue , idProperty , ... passThroughProps } = this . props ; const finalClassName = className ? `${ className } ${ this . className }` : this . className ; return ( < AutoComplete className = { finalClassName } defaultActiveFirstOption = { false } allowClear = { true } onChange = { this . onUpdateInput } onSelect = { this . onMenuItemSelected } notFoundContent = { fetching ? < Spin size = "small" / > : null } filterOption = { false } showArrow = { false } { ... passThroughProps } > { data . map ( d => renderOption ( d , this . props ) ) } < /AutoComplete>); } } export default WfsSearch ;