Skip to content

Commit

Permalink
feat(frontend): aggregated risk map layer (risk hotspots)
Browse files Browse the repository at this point in the history
New raster layer showing aggregated risk values (direct damages or economic losses) for combinations of sector (power, transport, water) and hazard (fluvial, surface, coastal flooding and cyclones.)

Includes:
- New aggregated risk map layer and params in `src/state`.
- New config in `src/config/risks`.
- New sidebar controls in `src/sidebar/risks`.
  • Loading branch information
eatyourgreens committed Jun 13, 2024
1 parent a1fdaff commit 1e3b8f4
Show file tree
Hide file tree
Showing 15 changed files with 364 additions and 0 deletions.
4 changes: 4 additions & 0 deletions frontend/src/config/interaction-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export const INTERACTION_GROUPS = makeConfig<InteractionGroupConfig, string>([
type: 'raster',
pickMultiple: true,
},
{
id: 'risks',
type: 'raster',
},
{
id: 'regions',
type: 'vector',
Expand Down
65 changes: 65 additions & 0 deletions frontend/src/config/risks/risk-view-layer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import GL from '@luma.gl/constants';
import { HazardParams } from 'config/hazards/domains';

import { rasterTileLayer } from 'lib/deck/layers/raster-tile-layer';
import { ViewLayer } from 'lib/data-map/view-layers';

import { RASTER_COLOR_MAPS } from '../color-maps';
import { RISK_SOURCE } from './source';

export function getRiskId<
F extends string, //'fluvial' | 'surface' | 'coastal' | 'cyclone',
RP extends number,
RCP extends string,
E extends number,
C extends number | string,
>({
riskType,
returnPeriod,
rcp,
epoch,
confidence,
}: {
riskType: F;
returnPeriod: RP;
rcp: RCP;
epoch: E;
confidence: C;
}) {
return `${riskType}__rp_${returnPeriod}__rcp_${rcp}__epoch_${epoch}__conf_${confidence}` as const;
}

export function riskViewLayer(riskType: string, riskParams: HazardParams): ViewLayer {
const magFilter = riskType === 'cyclone' ? GL.NEAREST : GL.LINEAR;

const { returnPeriod, rcp, epoch, confidence } = riskParams;

const deckId = getRiskId({ riskType, returnPeriod, rcp, epoch, confidence });

return {
id: riskType,
group: 'risks',
spatialType: 'raster',
interactionGroup: 'risks',
params: { riskType, riskParams },
fn: ({ deckProps }) => {
const { scheme, range } = RASTER_COLOR_MAPS[riskType];

return rasterTileLayer(
{
textureParameters: {
[GL.TEXTURE_MAG_FILTER]: magFilter,
// [GL.TEXTURE_MAG_FILTER]: zoom < 12 ? GL.NEAREST : GL.NEAREST_MIPMAP_LINEAR,
},
opacity: riskType === 'cyclone' ? 0.6 : 1,
},
deckProps,
{
id: `${riskType}@${deckId}`, // follow the convention viewLayerId@deckLayerId
data: RISK_SOURCE.getDataUrl({ riskType, riskParams }, { scheme, range }),
refinementStrategy: 'no-overlap',
},
);
},
};
}
10 changes: 10 additions & 0 deletions frontend/src/config/risks/source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const RISK_SOURCE = {
getDataUrl(
{ riskType, riskParams: { returnPeriod, rcp, epoch, confidence } },
{ scheme, range },
) {
const sanitisedRcp = rcp.replace('.', 'x');

return `/raster/singleband/${riskType}/${returnPeriod}/${sanitisedRcp}/${epoch}/${confidence}/{z}/{x}/{y}.png?colormap=${scheme}&stretch_range=[${range[0]},${range[1]}]`;
},
};
1 change: 1 addition & 0 deletions frontend/src/config/sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const SECTIONS_CONFIG: Record<string, { styles?: Record<string, StyleSele
styles: DROUGHT_STYLES,
},
hazards: {},
risks: {},
buildings: {
styles: BUILDING_STYLES,
},
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/sidebar/SidebarContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { NetworksSection } from './networks/NetworksSection';
import { RegionsSection } from './regions/RegionsSection';
import { MarineSection } from './solutions/MarineSection';
import { TerrestrialSection } from './solutions/TerrestrialSection';
import { RisksSection } from './risks/RisksSection';
import { ErrorBoundary } from 'lib/react/ErrorBoundary';
import { MobileTabContentWatcher } from 'pages/map/layouts/mobile/tab-has-content';

Expand All @@ -29,6 +30,7 @@ const SidebarContent: FC = () => {
return (
<>
<NetworksSection />
<RisksSection />
<HazardsSection />
<BuildingsSection />
<RegionsSection />
Expand Down
71 changes: 71 additions & 0 deletions frontend/src/sidebar/risks/DamageSourceControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
FormControl,
FormControlLabel,
FormLabel,
MenuItem,
Radio,
RadioGroup,
Select,
} from '@mui/material';
import { useRecoilState } from 'recoil';

import { StateEffectRoot } from 'lib/recoil/state-effects/StateEffectRoot';

import { InputSection } from 'sidebar/ui/InputSection';
import { InputRow } from 'sidebar/ui/InputRow';
import { EpochControl } from 'sidebar/ui/params/EpochControl';
import { RCPControl } from 'sidebar/ui/params/RCPControl';
import {
damageSourceState,
damageSourceStateEffect,
damageTypeState,
} from 'state/damage-mapping/damage-map';
import { HAZARDS_METADATA, HAZARDS_UI_ORDER } from 'config/hazards/metadata';
import { LayerStylePanel } from 'sidebar/ui/LayerStylePanel';

export const DamageSourceControl = () => {
const [damageSource, setDamageSource] = useRecoilState(damageSourceState);
const [damageType, setDamageType] = useRecoilState(damageTypeState);

return (
<>
<StateEffectRoot state={damageSourceState} effect={damageSourceStateEffect} />
<LayerStylePanel>
<InputSection>
<FormControl fullWidth>
<FormLabel>Damage type</FormLabel>
<Select<string>
variant="standard"
value={damageType}
onChange={(e) => setDamageType(e.target.value)}
>
<MenuItem value="direct">Direct Damages</MenuItem>
<MenuItem value="indirect">Economic Losses</MenuItem>
</Select>
</FormControl>
</InputSection>
<InputSection>
<FormControl>
<FormLabel>Hazard</FormLabel>
<RadioGroup value={damageSource} onChange={(e, value) => setDamageSource(value)}>
<FormControlLabel label="All Hazards" control={<Radio value="all" />} />
{HAZARDS_UI_ORDER.map((hazard) => (
<FormControlLabel
key={hazard}
label={HAZARDS_METADATA[hazard].label}
control={<Radio value={hazard} />}
/>
))}
</RadioGroup>
</FormControl>
</InputSection>
<InputSection>
<InputRow>
<EpochControl group={damageSource} />
<RCPControl group={damageSource} />
</InputRow>
</InputSection>
</LayerStylePanel>
</>
);
};
30 changes: 30 additions & 0 deletions frontend/src/sidebar/risks/RisksControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FormControl, FormControlLabel, FormLabel, Radio, RadioGroup } from '@mui/material';
import { useRecoilState } from 'recoil';
import { riskSelectionState } from 'state/risks/risk-selection';

const SECTORS = {
all: 'All sectors',
power: 'Power',
transport: 'Transport',
water: 'Water',
}

export const RisksControl = () => {
const [riskSelection, setRiskSelection] = useRecoilState(riskSelectionState);
return (
<>
<FormControl>
<FormLabel>Sector</FormLabel>
<RadioGroup value={riskSelection} onChange={(e, value) => setRiskSelection(value)}>
{Object.entries(SECTORS).map(([sector, label]) => (
<FormControlLabel
key={sector}
label={label}
control={<Radio value={sector} />}
/>
))}
</RadioGroup>
</FormControl>
</>
);
};
36 changes: 36 additions & 0 deletions frontend/src/sidebar/risks/RisksSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { FC } from 'react';

import { Collapse } from '@mui/material';
import { TransitionGroup } from 'react-transition-group';

import { StateEffectRoot } from 'lib/recoil/state-effects/StateEffectRoot';

import { RisksControl } from './RisksControl';
import { SidebarPanel } from 'sidebar/SidebarPanel';
import { DamageSourceControl } from './DamageSourceControl';
import { SidebarPanelSection } from 'sidebar/ui/SidebarPanelSection';
import { risksStyleStateEffect, sectionStyleValueState } from 'state/sections';
import { ErrorBoundary } from 'lib/react/ErrorBoundary';

export const RisksSection: FC = () => {
return (
<SidebarPanel id="risks" title="Aggregated Risk">
<ErrorBoundary message="There was a problem displaying this section.">
<StateEffectRoot
state={sectionStyleValueState('risks')}
effect={risksStyleStateEffect}
/>
<SidebarPanelSection>
<RisksControl />
</SidebarPanelSection>
<SidebarPanelSection variant="style">
<TransitionGroup>
<Collapse>
<DamageSourceControl />
</Collapse>
</TransitionGroup>
</SidebarPanelSection>
</ErrorBoundary>
</SidebarPanel>
);
};
18 changes: 18 additions & 0 deletions frontend/src/state/layers/risks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { HazardParams } from 'config/hazards/domains';
import { riskViewLayer } from 'config/risks/risk-view-layer';
import { ViewLayer } from 'lib/data-map/view-layers';
import { truthyKeys } from 'lib/helpers';
import { selector } from 'recoil';
import { dataParamsByGroupState } from 'state/data-params';
import { riskVisibilityState } from 'state/risks/risk-visibility';
import { sectionVisibilityState } from 'state/sections';

export const riskLayerState = selector<ViewLayer[]>({
key: 'riskLayerState',
get: ({ get }) =>
get(sectionVisibilityState('risks'))
? truthyKeys(get(riskVisibilityState)).map((risk) =>
riskViewLayer(risk, get(dataParamsByGroupState(risk)) as HazardParams),
)
: [],
});
5 changes: 5 additions & 0 deletions frontend/src/state/layers/view-layers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { buildingsViewLayer } from 'config/buildings/buildings-view-layer';
import { buildingSelectionState } from 'state/buildings';
import { networkLayersState } from './networks';
import { hazardLayerState } from './hazards';
import { riskLayerState } from './risks';
import { hoveredAdaptationFeatureState } from 'details/adaptations/FeatureAdaptationsTable';
import bboxPolygon from '@turf/bbox-polygon';
import { extendBbox } from 'lib/bounding-box';
Expand Down Expand Up @@ -53,6 +54,7 @@ export const viewLayersState = selector<ConfigTree<ViewLayer>>({
const regionLevel = get(regionLevelState);
const background = get(backgroundState);
const showLabels = get(showLabelsState);
console.log(get(riskLayerState), get(hazardLayerState))

return [
// administrative region boundaries or population density
Expand All @@ -69,6 +71,9 @@ export const viewLayersState = selector<ConfigTree<ViewLayer>>({
// hazard data layers
get(hazardLayerState),

// aggregated risk raster layers
get(riskLayerState),

get(buildingLayersState),

// network data layers
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/state/risk-mapping/risk-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import forEach from 'lodash/forEach';
import { atom, selector } from 'recoil';

import { HAZARD_DOMAINS } from 'config/hazards/domains';
import { dataParamOptionsState, dataParamState } from 'state/data-params';
import { hazardSelectionState } from 'state/hazards/hazard-selection';

export const riskSectorState = atom({
key: 'riskSectorState',
default: 'all',
});

export const riskSourceState = atom({
key: 'riskSourceState',
default: 'all',
});

export const riskTypeState = atom({
key: 'riskTypeState',
default: 'direct',
});

export const riskSourceStateEffect = ({ get, set }, riskSource) => {
syncHazardsWithRiskSourceStateEffect({ set }, riskSource);

if (riskSource !== 'all') {
const riskSourceReturnPeriodDomain = get(
dataParamOptionsState({ group: riskSource, param: 'returnPeriod' }),
);
const topReturnPeriod =
riskSourceReturnPeriodDomain[riskSourceReturnPeriodDomain.length - 1];

// CAUTION: this won't resolve the dependencies between data params if any depend on the return period
set(dataParamState({ group: riskSource, param: 'returnPeriod' }), topReturnPeriod);
}
};

function syncHazardsWithRiskSourceStateEffect({ set }, riskSource) {
forEach(HAZARD_DOMAINS, (groupConfig, group) => {
set(hazardSelectionState(group), group === riskSource);
});
}
41 changes: 41 additions & 0 deletions frontend/src/state/risk-mapping/risk-style-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { riskSourceState, riskTypeState } from './risk-map';
import { dataParamsByGroupState } from '../data-params';
import { selector } from 'recoil';
import { FieldSpec, StyleParams } from 'lib/data-map/view-layers';
import { VECTOR_COLOR_MAPS } from 'config/color-maps';

export const risksFieldState = selector<FieldSpec>({
key: 'eadAccessorState',
get: ({ get }) => {
const riskSource = get(riskSourceState);
if (riskSource == null) return null;
const riskType = get(riskTypeState);
const riskParams = get(dataParamsByGroupState(riskSource));

return {
fieldGroup: 'damages_expected',
fieldDimensions: {
hazard: riskSource,
rcp: riskParams.rcp,
epoch: riskParams.epoch,
protection_standard: 0,
},
field: riskType === 'direct' ? 'ead_mean' : 'eael_mean',
};
},
});

export const riskMapStyleParamsState = selector<StyleParams>({
key: 'riskMapStyleParamsState',
get: ({ get }) => {
const eadFieldSpec = get(risksFieldState);
if (eadFieldSpec == null) return {};

return {
colorMap: {
colorSpec: VECTOR_COLOR_MAPS.damages,
fieldSpec: eadFieldSpec,
},
};
},
});
11 changes: 11 additions & 0 deletions frontend/src/state/risks/risk-selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import fromPairs from 'lodash/fromPairs';
import { atom, RecoilValue } from 'recoil';

export const riskSelectionState = atom({
key: 'riskSelectionState',
default: 'all',
});

interface TransactionGetterInterface {
get<T>(a: RecoilValue<T>): T;
}
Loading

0 comments on commit 1e3b8f4

Please sign in to comment.