From a85b3b5bf2fad826afc017acb9a3eb047f55cb48 Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Thu, 13 Jun 2024 15:33:51 +0100 Subject: [PATCH] feat(frontend): aggregated risk map layer (risk hotspots) 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`. --- frontend/src/config/risks/risk-view-layer.ts | 65 +++++++++++++++++ frontend/src/config/risks/source.ts | 10 +++ frontend/src/config/sections.ts | 1 + frontend/src/sidebar/SidebarContent.tsx | 2 + .../src/sidebar/risks/DamageSourceControl.tsx | 71 +++++++++++++++++++ frontend/src/sidebar/risks/RisksControl.tsx | 30 ++++++++ frontend/src/sidebar/risks/RisksSection.tsx | 36 ++++++++++ frontend/src/state/layers/risks.ts | 18 +++++ frontend/src/state/layers/view-layers.ts | 4 ++ frontend/src/state/risk-mapping/risk-map.ts | 37 ++++++++++ .../state/risk-mapping/risk-style-params.ts | 41 +++++++++++ frontend/src/state/sections.ts | 10 +++ 12 files changed, 325 insertions(+) create mode 100644 frontend/src/config/risks/risk-view-layer.ts create mode 100644 frontend/src/config/risks/source.ts create mode 100644 frontend/src/sidebar/risks/DamageSourceControl.tsx create mode 100644 frontend/src/sidebar/risks/RisksControl.tsx create mode 100644 frontend/src/sidebar/risks/RisksSection.tsx create mode 100644 frontend/src/state/layers/risks.ts create mode 100644 frontend/src/state/risk-mapping/risk-map.ts create mode 100644 frontend/src/state/risk-mapping/risk-style-params.ts diff --git a/frontend/src/config/risks/risk-view-layer.ts b/frontend/src/config/risks/risk-view-layer.ts new file mode 100644 index 00000000..89f57f2d --- /dev/null +++ b/frontend/src/config/risks/risk-view-layer.ts @@ -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', + }, + ); + }, + }; +} diff --git a/frontend/src/config/risks/source.ts b/frontend/src/config/risks/source.ts new file mode 100644 index 00000000..eba3c561 --- /dev/null +++ b/frontend/src/config/risks/source.ts @@ -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]}]`; + }, +}; diff --git a/frontend/src/config/sections.ts b/frontend/src/config/sections.ts index 1c73260c..4497c1c7 100644 --- a/frontend/src/config/sections.ts +++ b/frontend/src/config/sections.ts @@ -13,6 +13,7 @@ export const SECTIONS_CONFIG: Record { return ( <> + diff --git a/frontend/src/sidebar/risks/DamageSourceControl.tsx b/frontend/src/sidebar/risks/DamageSourceControl.tsx new file mode 100644 index 00000000..5e618579 --- /dev/null +++ b/frontend/src/sidebar/risks/DamageSourceControl.tsx @@ -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 ( + <> + + + + + Damage type + + variant="standard" + value={damageType} + onChange={(e) => setDamageType(e.target.value)} + > + Direct Damages + Economic Losses + + + + + + Hazard + setDamageSource(value)}> + } /> + {HAZARDS_UI_ORDER.map((hazard) => ( + } + /> + ))} + + + + + + + + + + + + ); +}; diff --git a/frontend/src/sidebar/risks/RisksControl.tsx b/frontend/src/sidebar/risks/RisksControl.tsx new file mode 100644 index 00000000..21fd2194 --- /dev/null +++ b/frontend/src/sidebar/risks/RisksControl.tsx @@ -0,0 +1,30 @@ +import { FormControl, FormControlLabel, FormLabel, Radio, RadioGroup } from '@mui/material'; +import { useRecoilState } from 'recoil'; +import { riskSourceState } from 'state/risk-mapping/risk-map'; + +const SECTORS = { + all: 'All sectors', + power: 'Power', + transport: 'Transport', + water: 'Water', +} + +export const RisksControl = () => { + const [riskSource, setRiskSource] = useRecoilState(riskSourceState); + return ( + <> + + Sector + setRiskSource(value)}> + {Object.entries(SECTORS).map(([sector, label]) => ( + } + /> + ))} + + + + ); +}; diff --git a/frontend/src/sidebar/risks/RisksSection.tsx b/frontend/src/sidebar/risks/RisksSection.tsx new file mode 100644 index 00000000..d0ea2f59 --- /dev/null +++ b/frontend/src/sidebar/risks/RisksSection.tsx @@ -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 ( + + + + + + + + + + + + + + + + ); +}; diff --git a/frontend/src/state/layers/risks.ts b/frontend/src/state/layers/risks.ts new file mode 100644 index 00000000..38719416 --- /dev/null +++ b/frontend/src/state/layers/risks.ts @@ -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 { hazardVisibilityState } from 'state/hazards/hazard-visibility'; +import { sectionVisibilityState } from 'state/sections'; + +export const riskLayerState = selector({ + key: 'riskLayerState', + get: ({ get }) => + get(sectionVisibilityState('risks')) + ? truthyKeys(get(hazardVisibilityState)).map((hazard) => + riskViewLayer(hazard, get(dataParamsByGroupState(hazard)) as HazardParams), + ) + : [], +}); diff --git a/frontend/src/state/layers/view-layers.ts b/frontend/src/state/layers/view-layers.ts index 49bb6f5e..ec713ff1 100644 --- a/frontend/src/state/layers/view-layers.ts +++ b/frontend/src/state/layers/view-layers.ts @@ -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'; @@ -69,6 +70,9 @@ export const viewLayersState = selector>({ // hazard data layers get(hazardLayerState), + // aggregated risk raster layers + get(riskLayerState), + get(buildingLayersState), // network data layers diff --git a/frontend/src/state/risk-mapping/risk-map.ts b/frontend/src/state/risk-mapping/risk-map.ts new file mode 100644 index 00000000..735cd94d --- /dev/null +++ b/frontend/src/state/risk-mapping/risk-map.ts @@ -0,0 +1,37 @@ +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 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); + }); +} diff --git a/frontend/src/state/risk-mapping/risk-style-params.ts b/frontend/src/state/risk-mapping/risk-style-params.ts new file mode 100644 index 00000000..bdf131d4 --- /dev/null +++ b/frontend/src/state/risk-mapping/risk-style-params.ts @@ -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({ + 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({ + key: 'riskMapStyleParamsState', + get: ({ get }) => { + const eadFieldSpec = get(risksFieldState); + if (eadFieldSpec == null) return {}; + + return { + colorMap: { + colorSpec: VECTOR_COLOR_MAPS.damages, + fieldSpec: eadFieldSpec, + }, + }; + }, +}); diff --git a/frontend/src/state/sections.ts b/frontend/src/state/sections.ts index d0ca584c..6a7d06da 100644 --- a/frontend/src/state/sections.ts +++ b/frontend/src/state/sections.ts @@ -32,6 +32,16 @@ export const networksStyleStateEffect: StateEffect = ({ get, set }, styl } }; +export const risksStyleStateEffect: StateEffect = ({ get, set }, style) => { + if (style === 'damages') { + const hazardSelection = getHazardSelectionAggregate({ get }, HAZARDS_UI_ORDER); + const visibleHazards = truthyKeys(hazardSelection); + const defaultDamageSource = visibleHazards[0] ?? 'all'; + + set(damageSourceState, defaultDamageSource); + } +}; + export interface StyleSelectionOption { id: string; label: string;