Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WMS popup functionality #173

Merged
merged 26 commits into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
447082d
WMS popup functionality
JorgeMartinezG Apr 23, 2021
7c4018a
Include data parsing for wms getfeatureinfo request
JorgeMartinezG Apr 24, 2021
e618c0c
Merge branch 'master' into feature/134-wms-feature-info
ericboucher Apr 28, 2021
fe105db
Merge branch 'master' into feature/134-wms-feature-info
ericboucher Apr 28, 2021
75e0a2d
Merge branch 'master' into feature/134-wms-feature-info
JorgeMartinezG Apr 29, 2021
434bf4e
Move comment to function
JorgeMartinezG Apr 29, 2021
480a555
Refactor getExtent function
JorgeMartinezG Apr 29, 2021
917c9da
Remove undefined return within getActiveFeatureInfoLayers function
JorgeMartinezG Apr 29, 2021
a385448
use PascalCase for enum type
JorgeMartinezG Apr 29, 2021
f4b9979
Modify featureInfo response format function
JorgeMartinezG Apr 30, 2021
075838b
Include interface for wms feature info request params
JorgeMartinezG Apr 30, 2021
b1d8b95
fetch featureInfoRequest as async function
JorgeMartinezG Apr 30, 2021
e9b782b
Create utils file for MapView component
JorgeMartinezG Apr 30, 2021
77b11ce
Merge branch 'master' into feature/134-wms-feature-info
ericboucher May 4, 2021
f37d4fe
Change to PascalCase
JorgeMartinezG May 5, 2021
4972eb3
Merge branch 'feature/134-wms-feature-info' of github.com:WFP-VAM/pri…
JorgeMartinezG May 5, 2021
ca25a0d
Missing save
JorgeMartinezG May 5, 2021
30953dd
Validate undefined and null for map onclick event
JorgeMartinezG May 5, 2021
73987d1
use SnakeCase function from lodash
JorgeMartinezG May 5, 2021
c55874f
Use moment lib to parse selected date from calendar
JorgeMartinezG May 5, 2021
edf0c32
Merge branch 'master' into feature/134-wms-feature-info
Ry-DS May 11, 2021
20f56b2
Merge branch 'master' into feature/134-wms-feature-info
ericboucher May 17, 2021
d6c3797
Merge branch 'master' into feature/134-wms-feature-info
JorgeMartinezG May 17, 2021
1d5057e
Modify variable name
JorgeMartinezG May 17, 2021
e8d0e89
Include LinearProgress within tooltip component
JorgeMartinezG May 17, 2021
25c2bd7
Merge branch 'feature/134-wms-feature-info' of github.com:WFP-VAM/pri…
JorgeMartinezG May 17, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/components/MapView/DateSelector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ const Input = forwardRef(
);
},
);
function DateSelector({ availableDates = [], classes }: DateSelectorProps) {
function DateSelector({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use React.forwardRef, but in the future if we need a ref to the selector we'll run into issues. Up to you 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used a ref just to know current value from DateSelector. In both cases we would need to create the ref within the Mapview component and pass it to the DateSelector one.

Another option would be to use Localstorage, but some additional change will be needed within the addLayer reducer function

availableDates = [],
classes,
selectedDateRef,
}: DateSelectorProps) {
const dispatch = useDispatch();
const { startDate: stateStartDate } = useSelector(dateRangeSelector);

Expand Down Expand Up @@ -215,6 +219,7 @@ function DateSelector({ availableDates = [], classes }: DateSelectorProps) {
includeDates={availableDates.map(
d => new Date(d + USER_DATE_OFFSET),
)}
ref={selectedDateRef}
/>

<Hidden smUp>
Expand Down Expand Up @@ -354,6 +359,7 @@ const styles = (theme: Theme) =>

export interface DateSelectorProps extends WithStyles<typeof styles> {
availableDates?: number[];
selectedDateRef: React.RefObject<DatePicker>;
}

export default withStyles(styles)(DateSelector);
14 changes: 4 additions & 10 deletions src/components/MapView/Layers/ImpactLayer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { GeoJSONLayer } from 'react-mapbox-gl';
import { FillPaint, LinePaint } from 'mapbox-gl';
import { get } from 'lodash';
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core';
import { Extent } from '../raster-utils';
import { getExtent, Extent } from '../raster-utils';
import { legendToStops } from '../layer-utils';
import { ImpactLayerProps } from '../../../../config/types';
import { LayerDefinitions } from '../../../../config/utils';
Expand Down Expand Up @@ -41,27 +41,21 @@ const ImpactLayer = ({ classes, layer }: ComponentProps) => {
>) || {};
const dispatch = useDispatch();

// TODO - Use bbox on the admin boundaries instead.
const bounds = map && map.getBounds();
const minX = bounds ? bounds.getWest() : 0;
const maxX = bounds ? bounds.getEast() : 0;
const minY = bounds ? bounds.getSouth() : 0;
const maxY = bounds ? bounds.getNorth() : 0;

const extent: Extent = getExtent(map);
useEffect(() => {
// For now, assume that if we have layer data, we don't need to refetch. This could change down the line if we
// want to dynamically re-fetch data based on changing map bounds.
// Only fetch once we actually know the extent
const [minX, , maxX] = extent;
if (
selectedDate &&
(!data || date !== selectedDate) &&
minX !== 0 &&
maxX !== 0
) {
const extent: Extent = [minX, minY, maxX, maxY];
dispatch(loadLayerData({ layer, extent, date: selectedDate }));
}
}, [dispatch, layer, maxX, maxY, minX, minY, data, selectedDate, date]);
}, [dispatch, layer, extent, data, selectedDate, date]);

if (!data) {
return selectedDate ? null : (
Expand Down
13 changes: 13 additions & 0 deletions src/components/MapView/Layers/raster-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import bbox from '@turf/bbox';
import booleanPointInPolygon from '@turf/boolean-point-in-polygon';
import * as GeoTIFF from 'geotiff';
import { buffer } from 'd3-fetch';
import { Map as MapBoxMap } from 'mapbox-gl';
import { formatUrl } from '../../../utils/server-utils';

export type TransformMatrix = [number, number, number, number, number, number];
Expand Down Expand Up @@ -322,3 +323,15 @@ export function pixelsInFeature(
);
}, [] as number[]);
}

export function getExtent(map?: MapBoxMap): Extent {
// TODO - Use bbox on the admin boundaries instead.
const bounds = map?.getBounds();

const minX = bounds?.getWest();
const maxX = bounds?.getEast();
const minY = bounds?.getSouth();
const maxY = bounds?.getNorth();

return [minX, minY, maxX, maxY].map(val => val || 0) as Extent;
}
61 changes: 57 additions & 4 deletions src/components/MapView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { ComponentType, createElement, useEffect, useMemo } from 'react';
import React, {
ComponentType,
createElement,
useEffect,
useMemo,
useRef,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
CircularProgress,
Expand All @@ -11,6 +17,7 @@ import { countBy, pickBy } from 'lodash';
import moment from 'moment';
// map
import ReactMapboxGl from 'react-mapbox-gl';
import DatePicker from 'react-datepicker';
import { Map } from 'mapbox-gl';
import MapTooltip from './MapTooltip';
import Legends from './Legends';
Expand All @@ -36,7 +43,11 @@ import {
layersSelector,
} from '../../context/mapStateSlice/selectors';
import { addLayer, setMap, updateDateRange } from '../../context/mapStateSlice';
import { hidePopup } from '../../context/tooltipStateSlice';
import {
showPopup,
hidePopup,
addPopupData,
} from '../../context/tooltipStateSlice';
import {
availableDatesSelector,
isLoading as areDatesLoading,
Expand All @@ -50,8 +61,10 @@ import AnalysisLayer from './Layers/AnalysisLayer';
import {
DateCompatibleLayer,
getPossibleDatesForLayer,
makeFeatureInfoRequest,
} from '../../utils/server-utils';
import { addNotification } from '../../context/notificationStateSlice';
import { getActiveFeatureInfoLayers, getFeatureInfoParams } from './utils';
import AlertForm from './AlertForm';

const MapboxMap = ReactMapboxGl({
Expand Down Expand Up @@ -86,6 +99,8 @@ function MapView({ classes }: MapViewProps) {

const dispatch = useDispatch();

const selectedDateRef = useRef<DatePicker>(null);

const { startDate: selectedDate } = useSelector(dateRangeSelector);
const serverAvailableDates = useSelector(availableDatesSelector);
const selectedLayersWithDateSupport = selectedLayers
Expand Down Expand Up @@ -212,8 +227,43 @@ function MapView({ classes }: MapViewProps) {
containerStyle={{
height: '100%',
}}
onClick={() => {
onClick={(map: Map, evt: any) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance the library comes with a type for this? Would be nice to not use any. If it doesn't we could make an alias to any called MapEvent, or a bare bones type which has the fields we need.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to use the any type, since many mapbox handlers use it as type across the project. It would be better to use any to keep consistency. If not, a change is needed for all mapbox event handlers with their respective type. But this should be addressed on another issue

dispatch(hidePopup());
// Get layers that have getFeatureInfo option.
const featureInfoLayers = getActiveFeatureInfoLayers(map);
if (featureInfoLayers.length === 0) {
return;
}

const dateFromRef = (selectedDateRef.current?.props.selected as Date)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we use moment.format for this? Also, I thought we were storing the date in a redux slice. Is the dateRangeSelector not up to date?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Included in c55874f

.toISOString()
.split('T')[0];

const params = getFeatureInfoParams(map, evt, dateFromRef);
makeFeatureInfoRequest(featureInfoLayers, params).then(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that there's a delay to add the popup data. Maybe to address this delay, you can add Loading... to the tooltip, then add the details when its available?
For example:

Zambezia, Morrumbala
Loading...

turns to

Zambezia, Morrumbala
Event Name: ...
Publication date: ....

(result: any) => {
if (result === null) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

falsey check instead just in case the value is undefined?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Included in 30953dd

return;
}
Object.keys(result).forEach(k => {
dispatch(
addPopupData({
[k]: {
data: result[k],
coordinates: evt.lngLat,
},
}),
);
});

dispatch(
showPopup({
coordinates: evt.lngLat,
locationName: '',
}),
);
},
);
}}
>
<>
Expand Down Expand Up @@ -247,7 +297,10 @@ function MapView({ classes }: MapViewProps) {
</Grid>
</Grid>
{selectedLayerDates.length > 0 && (
<DateSelector availableDates={selectedLayerDates} />
<DateSelector
availableDates={selectedLayerDates}
selectedDateRef={selectedDateRef}
/>
)}
</Grid>
);
Expand Down
48 changes: 48 additions & 0 deletions src/components/MapView/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Map } from 'mapbox-gl';
import { LayerDefinitions } from '../../config/utils';
import { getExtent } from './Layers/raster-utils';
import { WMSLayerProps, FeatureInfoType } from '../../config/types';

export const getActiveFeatureInfoLayers = (map: Map): WMSLayerProps[] => {
const matchStr = 'layer-';
const layerIds =
map
.getStyle()
.layers?.filter(l => l.id.startsWith(matchStr))
.map(l => l.id.split(matchStr)[1]) ?? [];

if (layerIds.length === 0) {
return [];
}

const featureInfoLayers = Object.values(LayerDefinitions).filter(
l => layerIds.includes(l.id) && l.type === 'wms' && l.featureInfoProps,
);

if (featureInfoLayers.length === 0) {
return [];
}

return featureInfoLayers as WMSLayerProps[];
};

export const getFeatureInfoParams = (
map: Map,
evt: any,
date: string,
): FeatureInfoType => {
const { x, y } = evt.point;
const bbox = getExtent(map);
const { clientWidth, clientHeight } = map.getContainer();

const params = {
bbox,
x: Math.floor(x),
y: Math.floor(y),
width: clientWidth,
height: clientHeight,
time: date,
};

return params;
};
12 changes: 11 additions & 1 deletion src/config/mozambique/layers.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,17 @@
"name": "Tropical Storms",
"main": false
},
"base_url": "https://geonode.wfp.org/geoserver"
"base_url": "https://geonode.wfp.org/geoserver",
"feature_info_props": {
"event_name": {
"type": "text",
"label": "Event name"
},
"pub_date": {
"type": "date",
"label": "Publication date"
}
}
},
"adamts_nodes": {
"title": "Tropical Storms - Nodes",
Expand Down
36 changes: 36 additions & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,17 @@ export class BoundaryLayerProps extends CommonLayerProps {
adminLevelLocalNames: string[]; // Same as above, local to country
}

export enum LabelType {
Date = 'date',
Text = 'text',
Number = 'number',
}

interface featureInfoProps {
type: LabelType;
label: string;
}

export class WMSLayerProps extends CommonLayerProps {
type: 'wms';
baseUrl: string;
Expand All @@ -185,6 +196,9 @@ export class WMSLayerProps extends CommonLayerProps {

@optional
wcsConfig?: RawDataConfiguration;

@optional
featureInfoProps?: { [key: string]: featureInfoProps };
}

export class NSOLayerProps extends CommonLayerProps {
Expand Down Expand Up @@ -335,3 +349,25 @@ export type DateRangeType = {
month: string;
isFirstDay: boolean;
};

export interface FeatureInfoType {
bbox: number[];
x: number;
y: number;
width: number;
height: number;
}

export interface requestFeatureInfo extends FeatureInfoType {
ericboucher marked this conversation as resolved.
Show resolved Hide resolved
service: string;
request: string;
version: string;
exceptions: string;
infoFormat: string;
layers: string;
srs: string;
queryLayers: string;
featureCount: number;
format: string;
styles: string;
}