From 40c12c342110589aaf9b144fd7feeb68f20eef86 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: - variables: "total value", "economic use", "population use", "total risk", EAD, EAEL. - hazards: "none", cyclone, "all flooding". 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/color-maps.ts | 24 +++++++ frontend/src/config/interaction-groups.ts | 4 ++ frontend/src/config/risks/domains.ts | 68 ++++++++++++++++++ frontend/src/config/risks/metadata.ts | 18 +++++ frontend/src/config/risks/risk-view-layer.ts | 72 +++++++++++++++++++ frontend/src/config/risks/source.ts | 10 +++ frontend/src/config/sections.ts | 1 + frontend/src/lib/recoil/grouped-family.ts | 2 +- frontend/src/map/legend/RasterLegend.tsx | 47 +++++++----- frontend/src/sidebar/SidebarContent.tsx | 2 + frontend/src/sidebar/risks/RisksControl.tsx | 67 +++++++++++++++++ frontend/src/sidebar/risks/RisksSection.tsx | 18 +++++ frontend/src/state/data-params.ts | 2 + .../src/state/layers/interaction-groups.ts | 1 + frontend/src/state/layers/risks.ts | 20 ++++++ frontend/src/state/layers/view-layers.ts | 5 ++ frontend/src/state/risk-mapping/risk-map.ts | 25 +++++++ frontend/src/state/risks/risk-selection.ts | 10 +++ frontend/src/state/risks/risk-visibility.ts | 13 ++++ 19 files changed, 392 insertions(+), 17 deletions(-) create mode 100644 frontend/src/config/risks/domains.ts create mode 100644 frontend/src/config/risks/metadata.ts 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/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/risks/risk-selection.ts create mode 100644 frontend/src/state/risks/risk-visibility.ts diff --git a/frontend/src/config/color-maps.ts b/frontend/src/config/color-maps.ts index 8f5eb366..f355d456 100644 --- a/frontend/src/config/color-maps.ts +++ b/frontend/src/config/color-maps.ts @@ -20,6 +20,30 @@ export const RASTER_COLOR_MAPS = { scheme: 'reds', range: [0, 75], }, + totalValue: { + scheme: 'reds', + range: [0, 10], + }, + economicUse: { + scheme: 'blues', + range: [0, 10], + }, + populationUse: { + scheme: 'purples', + range: [0, 10], + }, + totalRisk: { + scheme: 'greens', + range: [0, 10], + }, + ead: { + scheme: 'oranges', + range: [0, 10], + }, + eael: { + scheme: 'purples', + range: [0, 10], + }, }; function invertColorScale(colorScale: (t: number) => T) { diff --git a/frontend/src/config/interaction-groups.ts b/frontend/src/config/interaction-groups.ts index 8c6b41a4..f289875a 100644 --- a/frontend/src/config/interaction-groups.ts +++ b/frontend/src/config/interaction-groups.ts @@ -14,6 +14,10 @@ export const INTERACTION_GROUPS = makeConfig([ type: 'raster', pickMultiple: true, }, + { + id: 'risks', + type: 'raster', + }, { id: 'regions', type: 'vector', diff --git a/frontend/src/config/risks/domains.ts b/frontend/src/config/risks/domains.ts new file mode 100644 index 00000000..c10946da --- /dev/null +++ b/frontend/src/config/risks/domains.ts @@ -0,0 +1,68 @@ +import { DataParamGroupConfig } from 'lib/controls/data-params'; + +export interface RiskParams { + riskSource?: string; + returnPeriod: number; + epoch: number; + rcp: string; + confidence: string | number; +} + +export const RISK_DOMAINS: Record> = { + none: { + paramDomains: { + returnPeriod: [0], + epoch: [2010], + rcp: ['baseline'], + confidence: ['None'], + }, + paramDefaults: { + returnPeriod: 0, + epoch: 2010, + rcp: 'baseline', + confidence: 'None', + }, + }, + fluvial: { + paramDomains: { + returnPeriod: [100], + rcp: ['baseline'], + epoch: [2010], + confidence: ['None'], + }, + paramDefaults: { + returnPeriod: 100, + rcp: 'baseline', + epoch: 2010, + confidence: 'None', + }, + paramDependencies: { + rcp: ({ epoch }) => { + if (epoch === 2010) return ['baseline']; + else if (epoch === 2050 || epoch === 2080) return ['2.6', '4.5', '8.5']; + return []; + }, + }, + }, + cyclone: { + paramDomains: { + returnPeriod: [100], + epoch: [2010], + rcp: ['baseline'], + confidence: [5, 50, 95], + }, + paramDefaults: { + returnPeriod: 100, + epoch: 2010, + rcp: 'baseline', + confidence: 50, + }, + paramDependencies: { + rcp: ({ epoch }) => { + if (epoch === 2010) return ['baseline']; + if (epoch === 2050 || epoch === 2100) return ['4.5', '8.5']; + return []; + }, + }, + }, +}; diff --git a/frontend/src/config/risks/metadata.ts b/frontend/src/config/risks/metadata.ts new file mode 100644 index 00000000..be4677d7 --- /dev/null +++ b/frontend/src/config/risks/metadata.ts @@ -0,0 +1,18 @@ +export const HAZARDS_METADATA = { + none: { label: 'None' }, + cyclone: { label: 'Cyclone' }, + fluvial: { label: 'All Flooding' }, +}; + +export const HAZARDS = Object.keys(HAZARDS_METADATA); + +export const RISKS_METADATA = { + totalValue: { label: 'Total value' }, + economicUse: { label: 'Economic use' }, + populationUse: { label: 'Population use' }, + totalRisk: { label: 'Total risk' }, + ead: { label: 'Expected Annual Damages (EAD)' }, + eael: { label: 'Expected Annual Economic Losses (EAEL)' }, +}; + +export const RISKS = Object.keys(RISKS_METADATA); 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..14f82ce9 --- /dev/null +++ b/frontend/src/config/risks/risk-view-layer.ts @@ -0,0 +1,72 @@ +import GL from '@luma.gl/constants'; +import { RiskParams } from 'config/risks/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, + riskSource, + returnPeriod, + rcp, + epoch, + confidence, +}: { + riskType: F; + riskSource: string; + returnPeriod: RP; + rcp: RCP; + epoch: E; + confidence: C; +}) { + return `${riskType}__${riskSource}__rp_${returnPeriod}__rcp_${rcp}__epoch_${epoch}__conf_${confidence}` as const; +} + +export function riskViewLayer(riskType: string, riskParams: RiskParams): ViewLayer { + const { riskSource, returnPeriod, rcp, epoch, confidence } = riskParams; + + const deckId = getRiskId({ riskType, riskSource, 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]; + const dataURL = RISK_SOURCE.getDataUrl( + { + riskType, + riskParams: { riskSource, returnPeriod, rcp, epoch, confidence }, + }, + { scheme, range }, + ); + + return rasterTileLayer( + { + textureParameters: { + [GL.TEXTURE_MAG_FILTER]: GL.LINEAR, + // [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: dataURL, + 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..ebdd7acf --- /dev/null +++ b/frontend/src/config/risks/source.ts @@ -0,0 +1,10 @@ +export const RISK_SOURCE = { + getDataUrl( + { riskType, riskParams: { riskSource, returnPeriod, rcp, epoch, confidence } }, + { scheme, range }, + ) { + const sanitisedRcp = rcp?.replace('.', 'x'); + + return `/raster/singleband/${riskType}/${riskSource}/${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..e3ab6a3e 100644 --- a/frontend/src/config/sections.ts +++ b/frontend/src/config/sections.ts @@ -13,6 +13,7 @@ export const SECTIONS_CONFIG: Record( (group) => ({ get }) => { const groupParams = get(paramsFamily(group)); - const deps = fromPairs(groupParams.map((param) => [param, family(paramFn(group, param))])); + const deps = fromPairs(groupParams?.map((param) => [param, family(paramFn(group, param))])); return get(waitForAll(deps)); }, }); diff --git a/frontend/src/map/legend/RasterLegend.tsx b/frontend/src/map/legend/RasterLegend.tsx index 9c89553d..9b617776 100644 --- a/frontend/src/map/legend/RasterLegend.tsx +++ b/frontend/src/map/legend/RasterLegend.tsx @@ -1,5 +1,6 @@ import { RASTER_COLOR_MAPS } from 'config/color-maps'; import { HAZARDS_METADATA } from 'config/hazards/metadata'; +import { RISKS_METADATA } from 'config/risks/metadata'; import { ViewLayer } from 'lib/data-map/view-layers'; import { FC, useCallback } from 'react'; import { GradientLegend } from './GradientLegend'; @@ -15,24 +16,38 @@ 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]; - + 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 ( - - ); + return ( + + ); + } + if (riskType) { + const { label } = RISKS_METADATA[riskType]; + return ( + `${value.toLocaleString()}`} + /> + ); + } + return

Unknown parameter type

; }; diff --git a/frontend/src/sidebar/SidebarContent.tsx b/frontend/src/sidebar/SidebarContent.tsx index 7b74b3cf..403e80cf 100644 --- a/frontend/src/sidebar/SidebarContent.tsx +++ b/frontend/src/sidebar/SidebarContent.tsx @@ -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'; @@ -29,6 +30,7 @@ const SidebarContent: FC = () => { return ( <> + diff --git a/frontend/src/sidebar/risks/RisksControl.tsx b/frontend/src/sidebar/risks/RisksControl.tsx new file mode 100644 index 00000000..7554665c --- /dev/null +++ b/frontend/src/sidebar/risks/RisksControl.tsx @@ -0,0 +1,67 @@ +import { + FormControl, + FormControlLabel, + FormLabel, + Menu, + 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 { riskSelectionState } from 'state/risks/risk-selection'; +import { riskSourceState, riskSourceStateEffect } from 'state/risk-mapping/risk-map'; +import { HAZARDS, HAZARDS_METADATA, RISKS, RISKS_METADATA } from 'config/risks/metadata'; + +export const RisksControl = () => { + const [riskSource, setRiskSource] = useRecoilState(riskSourceState); + const [riskSelection, setRiskSelection] = useRecoilState(riskSelectionState); + console.log({ riskSource, riskSelection }); + + return ( + <> + + + + Display + + variant="standard" + value={riskSelection} + onChange={(e) => setRiskSelection(e.target.value)} + > + {RISKS.map( + (risk) => {RISKS_METADATA[risk].label} + )} + + + + + + Hazard + setRiskSource(value)}> + {HAZARDS.map((hazard) => ( + } + /> + ))} + + + + + + + + + + + ); +}; diff --git a/frontend/src/sidebar/risks/RisksSection.tsx b/frontend/src/sidebar/risks/RisksSection.tsx new file mode 100644 index 00000000..2d3f0017 --- /dev/null +++ b/frontend/src/sidebar/risks/RisksSection.tsx @@ -0,0 +1,18 @@ +import { FC } from 'react'; + +import { RisksControl } from './RisksControl'; +import { SidebarPanel } from 'sidebar/SidebarPanel'; +import { SidebarPanelSection } from 'sidebar/ui/SidebarPanelSection'; +import { ErrorBoundary } from 'lib/react/ErrorBoundary'; + +export const RisksSection: FC = () => { + return ( + + + + + + + + ); +}; diff --git a/frontend/src/state/data-params.ts b/frontend/src/state/data-params.ts index a7fafc4c..32fa563b 100644 --- a/frontend/src/state/data-params.ts +++ b/frontend/src/state/data-params.ts @@ -1,4 +1,5 @@ import { HAZARD_DOMAINS } from 'config/hazards/domains'; +import { RISK_DOMAINS } from 'config/risks/domains'; import { totalDamagesConfig } from 'config/domains/total-damages'; import { DataParamGroupConfig, @@ -21,6 +22,7 @@ export type DataParamParam = Readonly<{ export const dataParamConfig: Record = { ...HAZARD_DOMAINS, + ...RISK_DOMAINS, all: totalDamagesConfig, adaptation: adaptationDomainsConfig, }; diff --git a/frontend/src/state/layers/interaction-groups.ts b/frontend/src/state/layers/interaction-groups.ts index 0ea20f75..6f0301f4 100644 --- a/frontend/src/state/layers/interaction-groups.ts +++ b/frontend/src/state/layers/interaction-groups.ts @@ -10,6 +10,7 @@ export const interactionGroupsState = selector({ return [ INTERACTION_GROUPS.assets, INTERACTION_GROUPS.hazards, + INTERACTION_GROUPS.risks, { ...INTERACTION_GROUPS.regions, usesAutoHighlight: regionDataShown, diff --git a/frontend/src/state/layers/risks.ts b/frontend/src/state/layers/risks.ts new file mode 100644 index 00000000..8b1d7c15 --- /dev/null +++ b/frontend/src/state/layers/risks.ts @@ -0,0 +1,20 @@ +import { RiskParams } from 'config/risks/domains'; +import { riskViewLayer } from 'config/risks/risk-view-layer'; +import { ViewLayer } from 'lib/data-map/view-layers'; +import { selector } from 'recoil'; +import { dataParamsByGroupState } from 'state/data-params'; +import { riskSourceState } from 'state/risk-mapping/risk-map'; +import { riskSelectionState } from 'state/risks/risk-selection'; +import { sectionVisibilityState } from 'state/sections'; + +export const riskLayerState = selector({ + key: 'riskLayerState', + get: ({ get }) => { + const riskSource = get(riskSourceState); + const riskSelection = get(riskSelectionState); + const dataParams = get(dataParamsByGroupState(riskSource)); + return get(sectionVisibilityState('risks')) + ? [riskViewLayer(riskSelection, { ...dataParams, riskSource } as RiskParams)] + : []; + }, +}); diff --git a/frontend/src/state/layers/view-layers.ts b/frontend/src/state/layers/view-layers.ts index 49bb6f5e..24ca2350 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'; @@ -53,6 +54,7 @@ export const viewLayersState = selector>({ const regionLevel = get(regionLevelState); const background = get(backgroundState); const showLabels = get(showLabelsState); + console.trace(get(riskLayerState), get(hazardLayerState)); return [ // administrative region boundaries or population density @@ -69,6 +71,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..75218f34 --- /dev/null +++ b/frontend/src/state/risk-mapping/risk-map.ts @@ -0,0 +1,25 @@ +import { atom } from 'recoil'; + +import { dataParamOptionsState, dataParamState } from 'state/data-params'; + +export const riskSourceState = atom({ + key: 'riskSourceState', + default: 'none', +}); + +export const riskTypeState = atom({ + key: 'riskTypeState', + default: 'totalValue', +}); + +export const riskSourceStateEffect = ({ get, 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); + } +}; diff --git a/frontend/src/state/risks/risk-selection.ts b/frontend/src/state/risks/risk-selection.ts new file mode 100644 index 00000000..c9f9bf53 --- /dev/null +++ b/frontend/src/state/risks/risk-selection.ts @@ -0,0 +1,10 @@ +import { atom, RecoilValue } from 'recoil'; + +export const riskSelectionState = atom({ + key: 'riskSelectionState', + default: 'totalValue', +}); + +interface TransactionGetterInterface { + get(a: RecoilValue): T; +} diff --git a/frontend/src/state/risks/risk-visibility.ts b/frontend/src/state/risks/risk-visibility.ts new file mode 100644 index 00000000..c8a40d9d --- /dev/null +++ b/frontend/src/state/risks/risk-visibility.ts @@ -0,0 +1,13 @@ +import { selector } from 'recoil'; + +import { riskSelectionState } from 'state/risks/risk-selection'; + +export const riskVisibilityState = selector({ + key: 'riskVisibilityState', + get: ({ get }) => { + const selectedRiskSource = get(riskSelectionState); + return { + [selectedRiskSource]: true, + }; + }, +});