From e3867a38202397aadd218eb4be8934a6404a2418 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/modules/risks`. - New config in `src/config/risks`. Parameters defined in `src/config/risks/domains`. - New sidebar controls in `src/sidebar/risks`. - New legends and tooltips in `src/config/risks`. --- frontend/src/app/config/interaction-groups.ts | 8 ++ frontend/src/app/config/sections.ts | 4 + frontend/src/app/config/view-layers.ts | 1 + frontend/src/app/config/views.ts | 16 ++++ frontend/src/app/sidebar/SidebarContent.tsx | 2 + frontend/src/app/state/data-params.ts | 2 + .../risks/RiskHoverDescription.tsx | 21 +++++ frontend/src/data-layers/risks/RiskLegend.tsx | 14 +++ frontend/src/data-layers/risks/color-maps.ts | 28 ++++++ frontend/src/data-layers/risks/domains.ts | 66 ++++++++++++++ frontend/src/data-layers/risks/metadata.ts | 36 ++++++++ .../src/data-layers/risks/risk-view-layer.ts | 85 +++++++++++++++++++ .../risks/sidebar/RisksControl.tsx | 43 ++++++++++ .../risks/sidebar/RisksSection.tsx | 21 +++++ frontend/src/data-layers/risks/source.ts | 12 +++ frontend/src/data-layers/risks/state/layer.ts | 19 +++++ frontend/src/data-layers/risks/styles.ts | 28 ++++++ 17 files changed, 406 insertions(+) create mode 100644 frontend/src/data-layers/risks/RiskHoverDescription.tsx create mode 100644 frontend/src/data-layers/risks/RiskLegend.tsx create mode 100644 frontend/src/data-layers/risks/color-maps.ts create mode 100644 frontend/src/data-layers/risks/domains.ts create mode 100644 frontend/src/data-layers/risks/metadata.ts create mode 100644 frontend/src/data-layers/risks/risk-view-layer.ts create mode 100644 frontend/src/data-layers/risks/sidebar/RisksControl.tsx create mode 100644 frontend/src/data-layers/risks/sidebar/RisksSection.tsx create mode 100644 frontend/src/data-layers/risks/source.ts create mode 100644 frontend/src/data-layers/risks/state/layer.ts create mode 100644 frontend/src/data-layers/risks/styles.ts diff --git a/frontend/src/app/config/interaction-groups.ts b/frontend/src/app/config/interaction-groups.ts index dbdf4c52..8a4173ee 100644 --- a/frontend/src/app/config/interaction-groups.ts +++ b/frontend/src/app/config/interaction-groups.ts @@ -19,6 +19,14 @@ export const INTERACTION_GROUPS = new Map([ pickMultiple: true, }, ], + [ + 'risks', + { + id: 'risks', + type: 'raster', + pickMultiple: false, + }, + ], [ 'regions', { diff --git a/frontend/src/app/config/sections.ts b/frontend/src/app/config/sections.ts index 41c094d7..e40a6cd2 100644 --- a/frontend/src/app/config/sections.ts +++ b/frontend/src/app/config/sections.ts @@ -5,6 +5,7 @@ import { NETWORK_STYLES } from 'data-layers/networks/styles'; import { REGION_STYLES } from 'data-layers/regions/styles'; import { MARINE_STYLES } from 'data-layers/marine/styles'; import { TERRESTRIAL_STYLES } from 'data-layers/terrestrial/styles'; +import { RISK_STYLES } from 'data-layers/risks/styles'; export const SECTIONS_CONFIG: Record }> = { assets: { @@ -14,6 +15,9 @@ export const SECTIONS_CONFIG: Record> = styles: ['type'], defaultStyle: 'type', }, + risks: { + expanded: true, + visible: true, + + styles: Object.keys(RISKS_METADATA), + defaultStyle: Object.keys(RISKS_METADATA)[0], + }, hazards: { expanded: true, visible: true, @@ -52,6 +61,13 @@ export const VIEW_SECTIONS: Record> = styles: ['type', 'damages'], defaultStyle: 'damages', }, + risks: { + expanded: true, + visible: true, + + styles: Object.keys(RISKS_METADATA), + defaultStyle: Object.keys(RISKS_METADATA)[0], + }, hazards: { expanded: false, visible: true, diff --git a/frontend/src/app/sidebar/SidebarContent.tsx b/frontend/src/app/sidebar/SidebarContent.tsx index ed2dfb9b..758c08ee 100644 --- a/frontend/src/app/sidebar/SidebarContent.tsx +++ b/frontend/src/app/sidebar/SidebarContent.tsx @@ -9,6 +9,7 @@ import { DroughtsSection } from 'data-layers/droughtRisks/sidebar/DroughtsSectio import { HazardsSection } from 'data-layers/hazards/sidebar/HazardsSection'; import { NetworksSection } from 'data-layers/networks/sidebar/NetworksSection'; import { RegionsSection } from 'data-layers/regions/sidebar/RegionsSection'; +import { RisksSection } from 'data-layers/risks/sidebar/RisksSection'; import { MarineSection } from 'data-layers/marine/sidebar/MarineSection'; import { TerrestrialSection } from 'data-layers/terrestrial/sidebar/TerrestrialSection'; import { ErrorBoundary } from 'lib/react/ErrorBoundary'; @@ -29,6 +30,7 @@ const SidebarContent: FC = () => { return ( <> + diff --git a/frontend/src/app/state/data-params.ts b/frontend/src/app/state/data-params.ts index 9241d588..3076e0f5 100644 --- a/frontend/src/app/state/data-params.ts +++ b/frontend/src/app/state/data-params.ts @@ -1,5 +1,6 @@ import { HAZARD_DOMAINS } from 'data-layers/hazards/domains'; import { NETWORK_DOMAINS } from 'data-layers/networks/domains'; +import { RISK_DOMAINS } from 'data-layers/risks/domains'; import { DataParamGroupConfig, Param, @@ -21,6 +22,7 @@ export type DataParamParam = Readonly<{ export const dataParamConfig: Record = { ...HAZARD_DOMAINS, ...NETWORK_DOMAINS, + risks: RISK_DOMAINS, }; export const dataParamNamesByGroup = mapValues(dataParamConfig, (groupConfig) => diff --git a/frontend/src/data-layers/risks/RiskHoverDescription.tsx b/frontend/src/data-layers/risks/RiskHoverDescription.tsx new file mode 100644 index 00000000..daf3edc4 --- /dev/null +++ b/frontend/src/data-layers/risks/RiskHoverDescription.tsx @@ -0,0 +1,21 @@ +import { FC } from 'react'; + +import { RasterHoverDescription } from 'lib/data-map/types'; +import { RasterHoverDescription as RasterTooltip } from 'app/map/tooltip/content/RasterHoverDescription'; + +import * as RISKS_COLOR_MAPS from './color-maps'; +import { RISKS_METADATA } from './metadata'; + +export const RiskHoverDescription: FC = ({ target, viewLayer }) => { + const { label, dataUnit } = RISKS_METADATA[viewLayer.id]; + const { scheme, range } = RISKS_COLOR_MAPS[viewLayer.id]; + return ( + + ); +}; diff --git a/frontend/src/data-layers/risks/RiskLegend.tsx b/frontend/src/data-layers/risks/RiskLegend.tsx new file mode 100644 index 00000000..06cd4109 --- /dev/null +++ b/frontend/src/data-layers/risks/RiskLegend.tsx @@ -0,0 +1,14 @@ +import { FC } from 'react'; + +import { RasterLegend } from 'app/map/legend/RasterLegend'; +import { ViewLayer } from 'lib/data-map/view-layers'; + +import * as RISKS_COLOR_MAPS from './color-maps'; +import { RISKS_METADATA } from './metadata'; + +export const RiskLegend: FC<{ viewLayer: ViewLayer }> = ({ viewLayer }) => { + const { id } = viewLayer; + const { label, dataUnit } = RISKS_METADATA[id]; + const { scheme, range } = RISKS_COLOR_MAPS[id]; + return ; +}; diff --git a/frontend/src/data-layers/risks/color-maps.ts b/frontend/src/data-layers/risks/color-maps.ts new file mode 100644 index 00000000..8757ff82 --- /dev/null +++ b/frontend/src/data-layers/risks/color-maps.ts @@ -0,0 +1,28 @@ +export const totalValue = { + scheme: 'reds', + range: [0, 10], +}; +export const economicUse = { + scheme: 'blues', + range: [0, 10], +}; + +export const populationUse = { + scheme: 'purples', + range: [0, 10], +}; + +export const totalRisk = { + scheme: 'greens', + range: [0, 10], +}; + +export const ead = { + scheme: 'oranges', + range: [0, 10], +}; + +export const eael = { + scheme: 'purples', + range: [0, 10], +}; diff --git a/frontend/src/data-layers/risks/domains.ts b/frontend/src/data-layers/risks/domains.ts new file mode 100644 index 00000000..01b9a1a7 --- /dev/null +++ b/frontend/src/data-layers/risks/domains.ts @@ -0,0 +1,66 @@ +import { DataParamGroupConfig } from 'lib/controls/data-params'; + +export interface RiskParams { + hazard: string; + returnPeriod: number; + epoch: number; + rcp: string; + confidence: string | number; +} + +/* + Default parameter ranges for each hazard type. + These are used to define ranges for input controls in the sidebar. +*/ +const hazardParamDomains = { + none: { + returnPeriod: [0], + epoch: [2010], + rcp: ['baseline'], + confidence: ['None'], + }, + cyclone: { + returnPeriod: [0], + epoch: [2010], + rcp: ['baseline'], + confidence: ['None'], + }, + fluvial: { + returnPeriod: [0], + epoch: [2010], + rcp: ['baseline'], + confidence: ['None'], + }, +}; + +export const RISK_DOMAINS: DataParamGroupConfig = { + /* + Default parameter ranges for each risk type. + */ + paramDomains: { + hazard: ['none', 'cyclone', 'fluvial'], + returnPeriod: [0], + epoch: [2010], + rcp: ['baseline'], + confidence: ['None'], + }, + /* + Default parameter values for each risk type. + */ + paramDefaults: { + hazard: 'none', + returnPeriod: 0, + epoch: 2010, + rcp: 'baseline', + confidence: 'None', + }, + /* + Callback functions to define custom parameter ranges based on selected hazard etc. + */ + paramDependencies: { + rcp: ({ hazard }) => hazardParamDomains[hazard].rcp, + epoch: ({ hazard }) => hazardParamDomains[hazard].epoch, + returnPeriod: ({ hazard }) => hazardParamDomains[hazard].returnPeriod, + confidence: ({ hazard }) => hazardParamDomains[hazard].confidence, + }, +}; diff --git a/frontend/src/data-layers/risks/metadata.ts b/frontend/src/data-layers/risks/metadata.ts new file mode 100644 index 00000000..6e0fec0d --- /dev/null +++ b/frontend/src/data-layers/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/data-layers/risks/risk-view-layer.ts b/frontend/src/data-layers/risks/risk-view-layer.ts new file mode 100644 index 00000000..781c5c9d --- /dev/null +++ b/frontend/src/data-layers/risks/risk-view-layer.ts @@ -0,0 +1,85 @@ +import { createElement } from 'react'; +import GL from '@luma.gl/constants'; +import { RiskParams } from './domains'; + +import { rasterTileLayer } from 'lib/deck/layers/raster-tile-layer'; +import { ViewLayer } from 'lib/data-map/view-layers'; +import { RasterTarget } from 'lib/data-map/types'; + +import { RiskLegend } from './RiskLegend'; +import { RiskHoverDescription } from './RiskHoverDescription'; +import * as RISKS_COLOR_MAPS from './color-maps'; +import { RISK_SOURCE } from './source'; + +export function getRiskId< + F extends string, // risk variable + RP extends number, + RCP extends string, + E extends number, + C extends number | string, +>({ + riskType, + hazard, + returnPeriod, + rcp, + epoch, + confidence, +}: { + riskType: F; + hazard: string; + returnPeriod: RP; + rcp: RCP; + epoch: E; + confidence: C; +}) { + return `${riskType}__${hazard}__rp_${returnPeriod}__rcp_${rcp}__epoch_${epoch}__conf_${confidence}` as const; +} + +export function riskViewLayer(riskType: string, riskParams: RiskParams): ViewLayer { + const { hazard, returnPeriod, rcp, epoch, confidence } = riskParams; + + const deckId = getRiskId({ riskType, hazard, returnPeriod, rcp, epoch, confidence }); + + return { + id: riskType, + group: 'risks', + spatialType: 'raster', + interactionGroup: 'risks', + params: { riskType, riskParams }, + fn: ({ deckProps }) => { + const { scheme, range } = RISKS_COLOR_MAPS[riskType]; + const dataURL = RISK_SOURCE.getDataUrl( + { + riskType, + riskParams: { hazard, 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', + }, + ); + }, + renderLegend() { + return createElement(RiskLegend, { + key: riskType, + viewLayer: this, + }); + }, + renderTooltip({ target }: { target: RasterTarget }) { + return createElement(RiskHoverDescription, { key: this.id, target, viewLayer: this }); + }, + }; +} diff --git a/frontend/src/data-layers/risks/sidebar/RisksControl.tsx b/frontend/src/data-layers/risks/sidebar/RisksControl.tsx new file mode 100644 index 00000000..688e6072 --- /dev/null +++ b/frontend/src/data-layers/risks/sidebar/RisksControl.tsx @@ -0,0 +1,43 @@ +import { FormControl, FormControlLabel, FormLabel, Radio, RadioGroup } from '@mui/material'; +import { useRecoilValue } from 'recoil'; + +import { dataParamState, useUpdateDataParam } from 'app/state/data-params'; +import { InputSection } from 'app/sidebar/ui/InputSection'; +import { InputRow } from 'app/sidebar/ui/InputRow'; +import { EpochControl } from 'app/sidebar/ui/params/EpochControl'; +import { RCPControl } from 'app/sidebar/ui/params/RCPControl'; + +import { HAZARDS, HAZARDS_METADATA } from '../metadata'; + +export const RisksControl = () => { + const hazard = useRecoilValue(dataParamState({ group: 'risks', param: 'hazard' })); + const updateHazard = useUpdateDataParam('risks', 'hazard'); + function selectHazard(event, value) { + updateHazard(value); + } + + return ( + <> + + + Hazard + + {HAZARDS.map((hazardOption) => ( + } + /> + ))} + + + + + + + + + + + ); +}; diff --git a/frontend/src/data-layers/risks/sidebar/RisksSection.tsx b/frontend/src/data-layers/risks/sidebar/RisksSection.tsx new file mode 100644 index 00000000..6c4497ce --- /dev/null +++ b/frontend/src/data-layers/risks/sidebar/RisksSection.tsx @@ -0,0 +1,21 @@ +import { FC } from 'react'; + +import { SidebarPanel } from 'app/sidebar/SidebarPanel'; +import { SidebarPanelSection } from 'app/sidebar/ui/SidebarPanelSection'; +import { StyleSelection } from 'app/sidebar/StyleSelection'; +import { ErrorBoundary } from 'lib/react/ErrorBoundary'; + +import { RisksControl } from './RisksControl'; + +export const RisksSection: FC = () => { + return ( + + + + + + + + + ); +}; diff --git a/frontend/src/data-layers/risks/source.ts b/frontend/src/data-layers/risks/source.ts new file mode 100644 index 00000000..dff1e362 --- /dev/null +++ b/frontend/src/data-layers/risks/source.ts @@ -0,0 +1,12 @@ +export const RISK_SOURCE = { + getDataUrl( + { riskType, riskParams: { hazard, returnPeriod, rcp, epoch, confidence } }, + { scheme, range }, + ) { + const sanitisedRcp = rcp?.replace('.', 'x'); + console.log({ riskType, hazard, returnPeriod, sanitisedRcp, epoch, confidence }); + + return `/raster/singleband/fluvial/100/baseline/2010/None/{z}/{x}/{y}.png?colormap=${scheme}&stretch_range=[${range[0]},${range[1]}]`; + return `/raster/singleband/${riskType}/${hazard}/${returnPeriod}/${sanitisedRcp}/${epoch}/${confidence}/{z}/{x}/{y}.png?colormap=${scheme}&stretch_range=[${range[0]},${range[1]}]`; + }, +}; diff --git a/frontend/src/data-layers/risks/state/layer.ts b/frontend/src/data-layers/risks/state/layer.ts new file mode 100644 index 00000000..9298df35 --- /dev/null +++ b/frontend/src/data-layers/risks/state/layer.ts @@ -0,0 +1,19 @@ +import { ViewLayer } from 'lib/data-map/view-layers'; +import { selector } from 'recoil'; +import { dataParamsByGroupState } from 'app/state/data-params'; +import { sectionVisibilityState } from 'app/state/sections'; +import { sectionStyleValueState } from 'app/state/sections'; + +import { RiskParams } from '../domains'; +import { riskViewLayer } from '../risk-view-layer'; + +export const risksLayerState = selector({ + key: 'risksLayerState', + get: ({ get }) => { + const risksStyle = get(sectionStyleValueState('risks')); + const dataParams = get(dataParamsByGroupState('risks')); + return get(sectionVisibilityState('risks')) + ? [riskViewLayer(risksStyle, dataParams as RiskParams)] + : []; + }, +}); diff --git a/frontend/src/data-layers/risks/styles.ts b/frontend/src/data-layers/risks/styles.ts new file mode 100644 index 00000000..e6819a0f --- /dev/null +++ b/frontend/src/data-layers/risks/styles.ts @@ -0,0 +1,28 @@ +import { makeConfig } from 'lib/helpers'; + +export const RISK_STYLES = makeConfig([ + { + id: 'totalValue', + label: 'Total Value', + }, + { + id: 'economicUse', + label: 'Economic Use', + }, + { + id: 'populationUse', + label: 'Population Use', + }, + { + id: 'totalRisk', + label: 'Total Risk', + }, + { + id: 'ead', + label: 'Expected Annual Damages (EAD)', + }, + { + id: 'eael', + label: 'Expected Annual Economic Losses (EAEL)', + }, +]);