Skip to content

Commit

Permalink
feat: use coordinateInfo hook
Browse files Browse the repository at this point in the history
BREAKING CHANGE: refactors CoordinateInfo as funnction compononent
  • Loading branch information
ahennr authored and simonseyock committed May 6, 2024
1 parent 656e9f7 commit 7dddfce
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 275 deletions.
42 changes: 20 additions & 22 deletions src/CoordinateInfo/CoordinateInfo.example.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import {
Tooltip
} from 'antd';
import * as copy from 'copy-to-clipboard';
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';
import OlSourceOSM from 'ol/source/OSM';
import OlSourceTileWMS from 'ol/source/TileWMS';
import OlView from 'ol/View';
import * as React from 'react';
import React, { useEffect,useState } from 'react';

const queryLayer = new OlLayerTile({
name: 'States (USA)',
Expand All @@ -30,14 +32,12 @@ const queryLayer = new OlLayerTile({
})
});

class CoordinateInfoExample extends React.Component {
const CoordinateInfoExample = () => {

constructor(props) {
super(props);
const [map, setMap] = useState();

this.mapDivId = `map-${Math.random()}`;

this.map = new OlMap({
useEffect(() => {
setMap(new OlMap({
layers: [
new OlLayerTile({
name: 'OSM',
Expand All @@ -49,22 +49,21 @@ class CoordinateInfoExample extends React.Component {
center: fromLonLat([-99.4031637, 38.3025695]),
zoom: 4
})
});
}
}));
}, []);

componentDidMount() {
this.map.setTarget(this.mapDivId);
if (!map) {
return null;
}

render() {
return (
<>
<div
id={this.mapDivId}
style={{
height: '400px'
}}
/>
return (
<MapContext.Provider value={map}>
<MapComponent
map={map}
style={{
height: '400px'
}}
/>
<CoordinateInfo
map={this.map}
queryLayers={[queryLayer]}
Expand Down Expand Up @@ -145,9 +144,8 @@ class CoordinateInfoExample extends React.Component {
);
}}
/>
</>
</MapContext.Provider>
);
}
}

<CoordinateInfoExample />
Expand Down
2 changes: 1 addition & 1 deletion src/CoordinateInfo/CoordinateInfo.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('<CoordinateInfo />', () => {
});

it('can be rendered', () => {
const { container } = render(<CoordinateInfo map={map} />);
const { container } = render(<CoordinateInfo />);
expect(container).toBeVisible();
});
});
286 changes: 34 additions & 252 deletions src/CoordinateInfo/CoordinateInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,269 +1,51 @@
import Logger from '@terrestris/base-util/dist/Logger';
import { isWmsLayer, WmsLayer } from '@terrestris/react-util/dist/Util/typeUtils';
import _cloneDeep from 'lodash/cloneDeep';
import _isString from 'lodash/isString';
import { getUid } from 'ol';
import { Coordinate as OlCoordinate } from 'ol/coordinate';
import OlFeature from 'ol/Feature';
import OlFormatGML2 from 'ol/format/GML2';
import OlGeometry from 'ol/geom/Geometry';
import OlBaseLayer from 'ol/layer/Base';
import OlMap from 'ol/Map';
import OlMapBrowserEvent from 'ol/MapBrowserEvent';
import * as React from 'react';

const format = new OlFormatGML2();

export interface CoordinateInfoProps {
/**
* List of (WMS) layers that should be queried.
*/
queryLayers: Array<WmsLayer>;

/**
* The number of max. features that should be returned by the GFI request.
*/
featureCount: number;

/**
* Whether the GFI control should requests all layers at a given coordinate
* or just the uppermost one.
*/
drillDown: boolean;
import useCoordinateInfo, {
CoordinateInfoResult,
UseCoordinateInfoArgs
} from '@terrestris/react-util/dist/Hooks/useCoordinateInfo/useCoordinateInfo';
import _isNil from 'lodash/isNil';
import React, { FC } from 'react';

export type CoordinateInfoProps = {
/**
* The children component that should be rendered. The render prop function
* receives the state of the component (this is the clicked coordinate, the
* list of GFI features if any and the loading state).
*/
resultRenderer: (childrenProps: CoordinateInfoState) => React.ReactNode;
/**
* The ol map.
*/
map: OlMap;
/**
* Optional request options to apply (separated by each query layer, identified
* by its internal ol id or a callback function).
*/
fetchOpts:
| {
[uid: string]: RequestInit;
}
| ((layer: WmsLayer) => RequestInit);
/**
* Callback function that gets called if all features are fetched successfully
* via GetFeatureInfo.
*/
onSuccess: (features: CoordinateInfoState) => void;
/**
* Callback function that gets called if an error occured while fetching the
* features via GetFeatureInfo.
*/
onError: (error: any) => void;
}

export interface CoordinateInfoState {
clickCoordinate: OlCoordinate | null;
features: {
[layerName: string]: OlFeature[];
};
loading: boolean;
}
resultRenderer?: (childrenProps: CoordinateInfoResult) => React.ReactNode;
} & UseCoordinateInfoArgs;

/**
* Constructs a wrapper component for querying features from the clicked
* coordinate. The returned features can be passed to a child component
* to be visualized.
*
* @class The CoordinateInfo
* @extends React.Component
*/
export class CoordinateInfo extends React.Component<CoordinateInfoProps, CoordinateInfoState> {
/**
* The defaultProps.
*/
static defaultProps = {
queryLayers: [],
featureCount: 1,
drillDown: true,
resultRenderer: () => {
return (
<div />
);
},
fetchOpts: {},
onSuccess: () => { },
onError: () => { }
};

/**
* Creates the CoordinateInfo component.
* @constructs CoordinateInfo
*/
constructor(props: CoordinateInfoProps) {
super(props);

this.state = {
clickCoordinate: null,
features: {},
loading: false
};

this.onMapClick = this.onMapClick.bind(this);
this.layerFilter = this.layerFilter.bind(this);
}

componentDidMount() {
const {
map
} = this.props;

map.on('click', this.onMapClick);
export const CoordinateInfo: FC<CoordinateInfoProps> = ({
drillDown = true,
featureCount = 1,
fetchOpts = {},
onError = () => undefined,
queryLayers = [],
resultRenderer = () => <></>
}) => {

const result = useCoordinateInfo({
drillDown,
featureCount,
fetchOpts,
onError,
queryLayers,
});

if (_isNil(result)) {
return null;
}

componentWillUnmount() {
const {
map
} = this.props;

map.un('click', this.onMapClick);
}

onMapClick(olEvt: OlMapBrowserEvent<MouseEvent>) {
const {
map,
featureCount,
drillDown,
fetchOpts,
onSuccess,
onError
} = this.props;

const mapView = map.getView();
const viewResolution = mapView.getResolution();
const viewProjection = mapView.getProjection();
const pixel = map.getEventPixel(olEvt.originalEvent);
const coordinate = olEvt.coordinate;

const promises: Promise<any>[] = [];

const mapLayers =
map.getAllLayers()
.filter(this.layerFilter)
.filter(l => l.getData && l.getData(pixel) && isWmsLayer(l));
mapLayers.forEach(l => {
const layerSource = (l as WmsLayer).getSource();
if (!layerSource) {
return;
}
const featureInfoUrl = layerSource.getFeatureInfoUrl(
coordinate,
viewResolution!,
viewProjection,
{
INFO_FORMAT: 'application/vnd.ogc.gml',
FEATURE_COUNT: featureCount
}
);
if (featureInfoUrl) {
let opts;
if (fetchOpts instanceof Function) {
opts = fetchOpts(l as WmsLayer);
} else {
opts = fetchOpts[getUid(l)];
}
promises.push(fetch(featureInfoUrl, opts));
}

return !drillDown;
});

map.getTargetElement().style.cursor = 'wait';

this.setState({
loading: true
});

Promise.all(promises)
.then((responses: Response[]) => {
this.setState({
clickCoordinate: coordinate
});
const textResponses = responses.map(response => response.text());
return Promise.all(textResponses);
})
.then((textResponses: string[]) => {
const features: {[index: string]: OlFeature[]} = {};

textResponses.forEach((featureCollection: string, idx: number) => {
const fc = format.readFeatures(featureCollection);
fc.forEach((feature: OlFeature<OlGeometry>) => {
const id = feature.getId();
const featureTypeName = _isString(id) ? id.split('.')[0] : id?.toString() ?? `UNKNOWN-${idx}`;

if (!features[featureTypeName]) {
features[featureTypeName] = [];
}

features[featureTypeName].push(feature);
});
});

this.setState({
features: features
}, () => {
onSuccess(this.getCoordinateInfoState());
});
})
.catch((error: any) => {
Logger.error(error);

onError(error);
})
.finally(() => {
map.getTargetElement().style.cursor = '';

this.setState({
loading: false
});
});
}

layerFilter(layerCandidate: OlBaseLayer) {
const {
queryLayers
} = this.props;

return (queryLayers as OlBaseLayer[]).includes(layerCandidate);
}

getCoordinateInfoState(): CoordinateInfoState {
// We're cloning the click coordinate and features to
// not pass the internal state reference to the parent component.
// Also note that we explicitly don't use feature.clone() to
// keep all feature properties (in particular the id) intact.
const coordinateInfoState: CoordinateInfoState = {
clickCoordinate: this.state.clickCoordinate ?
_cloneDeep(this.state.clickCoordinate) :
null,
loading: this.state.loading,
features: _cloneDeep(this.state.features)
};

return coordinateInfoState;
};

render() {
const {
resultRenderer
} = this.props;

return (
<>
{resultRenderer(this.getCoordinateInfoState())}
</>
);
}
}
return (
<>
{resultRenderer(result)}
</>
);
};

export default CoordinateInfo;

0 comments on commit 7dddfce

Please sign in to comment.