From a217931eb36f09f52fefadb33f447ee22d86aea6 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 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/layers/risks`, `src/state/risks` and `src/state/risk-mapping`. - New config in `src/config/risks`. - New sidebar controls in `src/sidebar/risks`. - New legends and tooltips in `src/map/legend` and `src/map/tooltip`. --- frontend/src/config/color-maps.ts | 24 +++++++ frontend/src/config/interaction-groups.ts | 5 ++ frontend/src/config/risks/domains.ts | 68 ++++++++++++++++++ frontend/src/config/risks/metadata.ts | 36 ++++++++++ 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/map/DeckGLOverlay.tsx | 8 ++- frontend/src/lib/recoil/grouped-family.ts | 2 +- frontend/src/map/legend/GradientLegend.tsx | 60 ++++++++-------- frontend/src/map/legend/RasterLegend.tsx | 14 ++-- frontend/src/map/tooltip/TooltipContent.tsx | 17 ++++- .../content/RasterHoverDescription.tsx | 14 ++-- frontend/src/sidebar/SidebarContent.tsx | 2 + frontend/src/sidebar/risks/RisksControl.tsx | 68 ++++++++++++++++++ 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 | 4 ++ frontend/src/state/risk-mapping/risk-map.ts | 6 ++ frontend/src/state/risks/risk-selection.ts | 10 +++ frontend/src/state/risks/risk-visibility.ts | 13 ++++ 23 files changed, 426 insertions(+), 49 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..5edbe099 100644 --- a/frontend/src/config/interaction-groups.ts +++ b/frontend/src/config/interaction-groups.ts @@ -14,6 +14,11 @@ export const INTERACTION_GROUPS = makeConfig([ type: 'raster', pickMultiple: true, }, + { + id: 'risks', + type: 'raster', + pickMultiple: false, + }, { 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..6e0fec0d --- /dev/null +++ b/frontend/src/config/risks/metadata.ts @@ -0,0 +1,36 @@ +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', + dataUnit: '', + }, + economicUse: { + label: 'Economic use', + dataUnit: '', + }, + populationUse: { + label: 'Population use', + dataUnit: '', + }, + totalRisk: { + label: 'Total risk', + dataUnit: '', + }, + ead: { + label: 'Expected Annual Damages (EAD)', + dataUnit: '', + }, + eael: { + label: 'Expected Annual Economic Losses (EAEL)', + dataUnit: '', + }, +}; + +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((props, ref) => { - const overlay = useControl(() => new MapboxOverlay(props)); +const DeckGLOverLayWithRef = (props, ref) => { + const overlay = useControl(() => new MapboxOverlay(props)); overlay.setProps(props); useImperativeHandle(ref, () => overlay); return null; -}); +} + +export const DeckGLOverlay = forwardRef(DeckGLOverLayWithRef); diff --git a/frontend/src/lib/recoil/grouped-family.ts b/frontend/src/lib/recoil/grouped-family.ts index 4ff586d3..19701eff 100644 --- a/frontend/src/lib/recoil/grouped-family.ts +++ b/frontend/src/lib/recoil/grouped-family.ts @@ -14,7 +14,7 @@ export function groupedFamily( (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/GradientLegend.tsx b/frontend/src/map/legend/GradientLegend.tsx index a984f1f9..f5938434 100644 --- a/frontend/src/map/legend/GradientLegend.tsx +++ b/frontend/src/map/legend/GradientLegend.tsx @@ -26,34 +26,34 @@ export interface GradientLegendProps { getValueLabel: (value: number) => string; } -export const GradientLegend: FC = memo( - ({ label, range, colorMapValues, getValueLabel }) => ( - - {label} - - {colorMapValues && ( - - )} - - - {colorMapValues && ( - <> - - {getValueLabel(range[0])} - - - {getValueLabel(range[1])} - - - )} - +const GradientLegendComponent: FC = ({ label, range, colorMapValues, getValueLabel }) => ( + + {label} + + {colorMapValues && ( + + )} - ), -); + + {colorMapValues && ( + <> + + {getValueLabel(range[0])} + + + {getValueLabel(range[1])} + + + )} + + +) + +export const GradientLegend = memo(GradientLegendComponent); diff --git a/frontend/src/map/legend/RasterLegend.tsx b/frontend/src/map/legend/RasterLegend.tsx index 9c89553d..c2661e6a 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'; @@ -14,12 +15,13 @@ export interface RasterColorMapValues { } export const RasterLegend: FC<{ viewLayer: ViewLayer }> = ({ viewLayer }) => { - const { - params: { hazardType }, - } = viewLayer; - const { label, dataUnit } = HAZARDS_METADATA[hazardType]; - const { scheme, range } = RASTER_COLOR_MAPS[hazardType]; - + const { id } = viewLayer; + const metadata = { + ...HAZARDS_METADATA, + ...RISKS_METADATA, + }; + const { label, dataUnit } = metadata[id]; + const { scheme, range } = RASTER_COLOR_MAPS[id]; const { error, loading, colorMapValues } = useRasterColorMapValues(scheme, range); const getValueLabel = useCallback( diff --git a/frontend/src/map/tooltip/TooltipContent.tsx b/frontend/src/map/tooltip/TooltipContent.tsx index ec61c1f2..8b22996c 100644 --- a/frontend/src/map/tooltip/TooltipContent.tsx +++ b/frontend/src/map/tooltip/TooltipContent.tsx @@ -20,7 +20,8 @@ const TooltipSection = ({ children }) => ( export const TooltipContent: FC = () => { const hoveredVector = useRecoilValue(hoverState('assets')) as InteractionTarget; - const hoveredRasters = useRecoilValue(hoverState('hazards')) as InteractionTarget[]; + const hoveredHazards = useRecoilValue(hoverState('hazards')) as InteractionTarget[]; + const hoveredRisk = useRecoilValue(hoverState('risks')) as InteractionTarget; const hoveredRegion = useRecoilValue(hoverState('regions')) as InteractionTarget; const hoveredSolution = useRecoilValue(hoverState('solutions')) as InteractionTarget; const hoveredDrought = useRecoilValue(hoverState('drought')) as InteractionTarget; @@ -28,13 +29,15 @@ export const TooltipContent: FC = () => { const regionDataShown = useRecoilValue(showPopulationState); const assetsHovered = hasHover(hoveredVector); - const hazardsHovered = hasHover(hoveredRasters); + const hazardsHovered = hasHover(hoveredHazards); + const risksHovered = hasHover(hoveredRisk); const regionsHovered = hasHover(hoveredRegion); const solutionsHovered = hasHover(hoveredSolution); const droughtHovered = hasHover(hoveredDrought); const doShow = assetsHovered || hazardsHovered || + risksHovered || (regionDataShown && regionsHovered) || solutionsHovered || droughtHovered; @@ -62,7 +65,7 @@ export const TooltipContent: FC = () => { ) : null} {hazardsHovered ? ( - {hoveredRasters.map((hr) => ( + {hoveredHazards.map((hr) => ( { ))} ) : null} + {risksHovered ? ( + + + + ) : null} {regionsHovered ? ( diff --git a/frontend/src/map/tooltip/content/RasterHoverDescription.tsx b/frontend/src/map/tooltip/content/RasterHoverDescription.tsx index 756935e5..cae815ee 100644 --- a/frontend/src/map/tooltip/content/RasterHoverDescription.tsx +++ b/frontend/src/map/tooltip/content/RasterHoverDescription.tsx @@ -4,6 +4,7 @@ import { InteractionTarget, RasterTarget } from 'lib/data-map/interactions/use-i import { RASTER_COLOR_MAPS } from 'config/color-maps'; import { HAZARDS_METADATA } from 'config/hazards/metadata'; +import { RISKS_METADATA } from 'config/risks/metadata'; import { useRasterColorMapValues } from '../../legend/use-color-map-values'; import { ColorBox } from './ColorBox'; @@ -34,13 +35,14 @@ export const RasterHoverDescription: FC<{ hoveredObject: InteractionTarget { return ( <> + diff --git a/frontend/src/sidebar/risks/RisksControl.tsx b/frontend/src/sidebar/risks/RisksControl.tsx new file mode 100644 index 00000000..30233bee --- /dev/null +++ b/frontend/src/sidebar/risks/RisksControl.tsx @@ -0,0 +1,68 @@ +import { + FormControl, + FormControlLabel, + FormLabel, + MenuItem, + Radio, + RadioGroup, + Select, +} from '@mui/material'; +import { useRecoilState } from 'recoil'; + +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 } 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); + + function selectType({ target }) { + setRiskSelection(target?.value); + } + + function selectHazard(event, value) { + setRiskSource(value); + } + + return ( + <> + + + Display + variant="standard" value={riskSelection} onChange={selectType}> + {RISKS.map((risk) => ( + + {RISKS_METADATA[risk].label} + + ))} + + + + + + Hazard + + {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..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..339dadc1 --- /dev/null +++ b/frontend/src/state/risk-mapping/risk-map.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const riskSourceState = atom({ + key: 'riskSourceState', + default: 'none', +}); 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, + }; + }, +});