Skip to content

Commit

Permalink
Displaying inundation maps as geojson polygon layer
Browse files Browse the repository at this point in the history
  • Loading branch information
gislawill committed Oct 7, 2024
1 parent 62d47df commit 850cc51
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 1 deletion.
61 changes: 61 additions & 0 deletions frontend/src/components/MapView/Layers/GeojsonDataLayer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { memo, useEffect } from 'react';
import { Layer, Source } from 'react-map-gl/maplibre';

import { useDispatch, useSelector } from 'react-redux';
import { GeojsonDataLayerProps, LegendDefinition } from 'config/types';

import { LayerData, loadLayerData } from 'context/layers/layer-data';
import { layerDataSelector } from 'context/mapStateSlice/selectors';
import { getLayerMapId } from 'utils/map-utils';
import { opacitySelector } from 'context/opacityStateSlice';
import { FillLayerSpecification } from 'maplibre-gl';

const paintProps: (
legend: LegendDefinition,
opacity: number | undefined,
) => FillLayerSpecification['paint'] = (
legend: LegendDefinition,
opacity?: number,
) => ({
'fill-opacity': opacity || 1,
'fill-color': {
property: 'level',
type: 'categorical',
stops: legend.map(({ value, color }) => [value, color]),
},
});

// Polygon Data, takes any GeoJSON of polygons and shows it.
const GeojsonDataLayer = memo(({ layer, before }: LayersProps) => {
const dispatch = useDispatch();
const layerId = getLayerMapId(layer.id);
const opacityState = useSelector(opacitySelector(layer.id));

const layerData = useSelector(layerDataSelector(layer.id)) as
| LayerData<GeojsonDataLayerProps>
| undefined;

const { data } = layerData || {};

useEffect(() => {
dispatch(loadLayerData({ layer }));
}, [dispatch, layer]);

return (
<Source data={data} type="geojson">
<Layer
beforeId={before}
id={layerId}
type="fill"
paint={paintProps(layer.legend || [], opacityState || layer.opacity)}
/>
</Source>
);
});

export interface LayersProps {
layer: GeojsonDataLayerProps;
before?: string;
}

export default GeojsonDataLayer;
3 changes: 3 additions & 0 deletions frontend/src/components/MapView/Map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { MapSourceDataEvent, Map as MaplibreMap } from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import { Panel, leftPanelTabValueSelector } from 'context/leftPanelStateSlice';
import { mapStyle } from './utils';
import GeojsonDataLayer from '../Layers/GeojsonDataLayer';

interface MapComponentProps {
setIsAlertFormOpen: Dispatch<SetStateAction<boolean>>;
Expand All @@ -58,6 +59,7 @@ const componentTypes: LayerComponentsMap<LayerType> = {
admin_level_data: { component: AdminLevelDataLayer },
impact: { component: ImpactLayer },
point_data: { component: PointDataLayer },
geojson_polygon: { component: GeojsonDataLayer },
static_raster: { component: StaticRasterLayer },
composite: { component: CompositeLayer },
anticipatory_action: {
Expand All @@ -76,6 +78,7 @@ const MapComponent = memo(({ setIsAlertFormOpen }: MapComponentProps) => {

const { selectedLayers, boundaryLayerId } = useLayers();

console.log(selectedLayers);

Check warning on line 81 in frontend/src/components/MapView/Map/index.tsx

View workflow job for this annotation

GitHub Actions / frontend_tests (ubuntu-latest)

Unexpected console statement
const selectedMap = useSelector(mapSelector);
const tabValue = useSelector(leftPanelTabValueSelector);

Expand Down
87 changes: 87 additions & 0 deletions frontend/src/config/cambodia/layers.json
Original file line number Diff line number Diff line change
Expand Up @@ -1988,5 +1988,92 @@
}
],
"legend_text": "Index meausring a household’s social and economic capacities and resilience to cope with, adapt to and recover from the floods and droughts. Aggregated at Commune level"
},
"google_flood_status_at_gauges": {
"title": "Flood Status at Gauges (Google AI)",
"type": "point_data",
"loader": "google_flood",
"hex_display": false,
"data": "http://0.0.0.0:80/google-floods/gauges/?region_codes=KH",
"detail_url": "http://0.0.0.0:80/google-floods/gauges/forecasts",
"data_field": "severity",
"data_field_type": "text",
"opacity": 0.9,
"legend_text": "Current flood status at verified gauges. Visit [Google Research](https://sites.research.google/floodforecasting/) about Google's AI Forecasting models.",
"legend": [
{ "label": "Extreme", "value": "EXTREME", "color": "#a70606" },
{ "label": "Danger", "value": "SEVERE", "color": "#ea250a" },
{ "label": "Warning", "value": "ABOVE_NORMAL", "color": "#fba705" },
{ "label": "Normal", "value": "NO_FLOODING", "color": "#089180" },
{ "label": "Unknown", "value": "UNKNOWN", "color": "#858585" }
],
"feature_info_title": {
"siteName": {
"type": "text",
"template": "Site: {{siteName}}",
"visibility": "if-defined"
},
"riverName": {
"type": "text",
"template": "River: {{riverName}}",
"visibility": "if-defined"
},
"gaugeId": {
"type": "text",
"template": "Gauge ID: {{gaugeId}}",
"visibility": "if-defined"
}
},
"feature_info_props": {
"siteName": {
"type": "text",
"dataTitle": "Site",
"visibility": "if-defined"
},
"river": {
"type": "text",
"dataTitle": "River",
"visibility": "if-defined"
},
"severity": {
"type": "labelMapping",
"dataTitle": "Flood Status",
"labelMap": {
"EXTREME": "Extreme",
"SEVERE": "Danger",
"ABOVE_NORMAL": "Warning",
"NO_FLOODING": "Normal",
"UNKNOWN": "Unknown"
}
},
"gaugeId": {
"type": "text",
"dataTitle": "Gauge ID"
},
"source": {
"type": "text",
"dataTitle": "Data source"
},
"issuedTime": {
"type": "date",
"dataTitle": "Status issued"
}
}
},
"google_flood_inundation_maps": {
"title": "Current Flood Inundation Map (Google AI)",
"type": "geojson_polygon",
"loader": "google_flood",
"hex_display": false,
"data": "http://0.0.0.0:80/google-floods/inundations/?region_codes=KH",
"data_field": "level",
"data_field_type": "text",
"opacity": 0.9,
"legend_text": "Current predicted flood maps. Visit [Google Research](https://sites.research.google/floodforecasting/) about Google's AI Forecasting models.",
"legend": [
{ "label": "High", "value": "HIGH", "color": "#00408A" },
{ "label": "Medium", "value": "MEDIUM", "color": "#5296E5" },
{ "label": "Low", "value": "LOW", "color": "#B5D2F5" }
]
}
}
19 changes: 19 additions & 0 deletions frontend/src/config/cambodia/prism.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,25 @@
},
"field_reports": {
"field_reports": ["flood_report", "drought_report", "kh_incident_report"]
},
"flooding": {
"flood_status": [
{
"group_title": "Flood Status",
"activate_all": true,
"layers": [
{
"id": "google_flood_status_at_gauges",
"label": "Flood Gauges",
"main": true
},
{
"id": "google_flood_inundation_maps",
"label": "Inundation Maps"
}
]
}
]
}
}
}
27 changes: 26 additions & 1 deletion frontend/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export type LayerType =
| PointDataLayerProps
| CompositeLayerProps
| StaticRasterLayerProps
| AnticipatoryActionLayerProps;
| AnticipatoryActionLayerProps
| GeojsonDataLayerProps;

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I,
Expand Down Expand Up @@ -740,6 +741,29 @@ export class PointDataLayerProps extends CommonLayerProps {
detailUrl?: string;
}

export class GeojsonDataLayerProps extends CommonLayerProps {
type: 'geojson_polygon' = 'geojson_polygon';
data: string;

@makeRequired
dataField: string;

@optional // if legend_label, uses the label from legend to display in feature info. if not, uses dataField
displaySource?: 'legend_label' | 'data_field';

@makeRequired
declare title: string;

@makeRequired
declare legend: LegendDefinition;

@makeRequired
declare legendText: string;

@optional
additionalQueryParams?: { [key: string]: string | { [key: string]: string } };
}

export type RequiredKeys<T> = {
[k in keyof T]: undefined extends T[k] ? never : k;
}[keyof T];
Expand Down Expand Up @@ -890,6 +914,7 @@ export type PointData = {
};

export type PointLayerData = FeatureCollection;
export type GeojsonLayerData = FeatureCollection;

export interface BaseLayer {
name: string;
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/config/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
BoundaryLayerProps,
checkRequiredKeys,
CompositeLayerProps,
GeojsonDataLayerProps,
ImpactLayerProps,
LayerKey,
LayersMap,
Expand Down Expand Up @@ -130,6 +131,11 @@ export const getLayerByKey = (layerKey: LayerKey): LayerType => {
return throwInvalidLayer();
}
return definition;
case 'geojson_polygon':
if (!checkRequiredKeys(GeojsonDataLayerProps, definition, true)) {
return throwInvalidLayer();
}
return definition;
default:
// doesn't do anything, but it helps catch any layer type cases we forgot above compile time via TS.
// https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript
Expand Down
38 changes: 38 additions & 0 deletions frontend/src/context/layers/geojson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import GeoJSON from 'geojson';
import { GeojsonDataLayerProps } from 'config/types';
import { queryParamsToString } from 'utils/url-utils';
import { fetchWithTimeout } from 'utils/fetch-with-timeout';
import { HTTPError } from 'utils/error-utils';
import { setUserAuthGlobal } from 'context/serverStateSlice';
import type { LazyLoader } from './layer-data';

export const fetchGeojsonLayerData: LazyLoader<GeojsonDataLayerProps> =
() =>
async ({ layer: { data: dataUrl, additionalQueryParams } }, { dispatch }) => {
const requestUrl = `${dataUrl}${
dataUrl.includes('?') ? '&' : '?'
}&${queryParamsToString(additionalQueryParams)}`;
let data;
let response: Response;
// TODO - Better error handling, esp. for unauthorized requests.
try {
// eslint-disable-next-line fp/no-mutation
response = await fetchWithTimeout(
requestUrl,
dispatch,
{
mode: 'cors',
},
`Request failed for fetching point layer data at ${requestUrl}`,
);
// eslint-disable-next-line fp/no-mutation
data = (await response.json()) as GeoJSON.FeatureCollection;
} catch (error) {
if ((error as HTTPError)?.statusCode === 401) {
dispatch(setUserAuthGlobal(undefined));
}
throw error;
}

return data as any as GeoJSON.FeatureCollection;
};
4 changes: 4 additions & 0 deletions frontend/src/context/layers/layer-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
AnticipatoryActionLayerProps,
DateItem,
DiscriminateUnion,
GeojsonLayerData,
LayerType,
PointLayerData,
StaticRasterLayerProps,
Expand All @@ -20,6 +21,7 @@ import { BoundaryLayerData, fetchBoundaryLayerData } from './boundary';
import { fetchImpactLayerData, ImpactLayerData } from './impact';
import type { CompositeLayerData } from './composite_data';
import { fetchCompositeLayerData } from './composite_data';
import { fetchGeojsonLayerData } from './geojson';

export type LayerAcceptingDataType = Exclude<
LayerType,
Expand All @@ -35,6 +37,7 @@ type LayerSpecificDataTypes = {
// eslint-disable-next-line camelcase
point_data: PointLayerData | AdminLevelDataLayerData;
composite: CompositeLayerData;
geojson_polygon: GeojsonLayerData;
};

export interface LayerData<L extends LayerAcceptingDataType> {
Expand Down Expand Up @@ -97,6 +100,7 @@ export const loadLayerData: LoadLayerDataFuncType = createAsyncThunk<
admin_level_data: fetchAdminLevelDataLayerData,
point_data: fetchPointLayerData,
composite: fetchCompositeLayerData,
geojson_polygon: fetchGeojsonLayerData,
};
const lazyLoad: LazyLoader<any> = layerLoaders[layer.type];
try {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/context/opacityStateSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const opacityStateSlice = createSlice({
case 'admin_level_data':
case 'composite':
case 'impact':
case 'geojson_polygon':
return [getLayerMapId(layerId), 'fill-opacity'];
case 'point_data':
// This is a hacky way to support opacity change for Kobo data.
Expand Down

0 comments on commit 850cc51

Please sign in to comment.