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 d839554
Show file tree
Hide file tree
Showing 19 changed files with 402 additions and 13 deletions.
12 changes: 12 additions & 0 deletions frontend/src/config/color-maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ export const RASTER_COLOR_MAPS = {
scheme: 'reds',
range: [0, 75],
},
power: {
scheme: 'reds',
range: [0, 10],
},
water: {
scheme: 'blues',
range: [0, 10],
},
transport: {
scheme: 'purples',
range: [0, 10],
},
};

function invertColorScale<T>(colorScale: (t: number) => T) {
Expand Down
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
3 changes: 2 additions & 1 deletion frontend/src/lib/recoil/grouped-family.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export function groupedFamily<FVT, FPT>(
(group) =>
({ get }) => {
const groupParams = get(paramsFamily(group));
const deps = fromPairs(groupParams.map((param) => [param, family(paramFn(group, param))]));
console.log({ group, groupParams });
const deps = fromPairs(groupParams?.map((param) => [param, family(paramFn(group, param))]));
return get(waitForAll(deps));
},
});
Expand Down
36 changes: 24 additions & 12 deletions frontend/src/map/legend/RasterLegend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,36 @@ export interface RasterColorMapValues {

export const RasterLegend: FC<{ viewLayer: ViewLayer }> = ({ viewLayer }) => {
const {
params: { hazardType },
params: { hazardType, riskType },
} = viewLayer;
const { label, dataUnit } = HAZARDS_METADATA[hazardType];
const { scheme, range } = RASTER_COLOR_MAPS[hazardType];

console.log(hazardType, riskType)
const { scheme, range } = RASTER_COLOR_MAPS[hazardType || riskType];
const { error, loading, colorMapValues } = useRasterColorMapValues(scheme, range);

const getValueLabel = useCallback(
(value: number) => `${value.toLocaleString()} ${dataUnit}`,
[dataUnit],
);

if (hazardType) {
const { label, dataUnit } = HAZARDS_METADATA[hazardType];

const getValueLabel = useCallback(
(value: number) => `${value.toLocaleString()} ${dataUnit}`,
[dataUnit],
);

return (
<GradientLegend
label={label}
range={range}
colorMapValues={!(error || loading) ? colorMapValues : null}
getValueLabel={getValueLabel}
/>
);
}
return (
<GradientLegend
label={label}
label="Risk"
range={range}
colorMapValues={!(error || loading) ? colorMapValues : null}
getValueLabel={getValueLabel}
getValueLabel={(value) => `${value.toLocaleString()}`}
/>
);
)

};
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>
);
};
1 change: 1 addition & 0 deletions frontend/src/state/layers/interaction-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const interactionGroupsState = selector({
return [
INTERACTION_GROUPS.assets,
INTERACTION_GROUPS.hazards,
INTERACTION_GROUPS.risks,
{
...INTERACTION_GROUPS.regions,
usesAutoHighlight: regionDataShown,
Expand Down
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
Loading

0 comments on commit d839554

Please sign in to comment.