diff --git a/frontend/src/config/interaction-groups.ts b/frontend/src/config/interaction-groups.ts index ecddeb42..86dc1085 100644 --- a/frontend/src/config/interaction-groups.ts +++ b/frontend/src/config/interaction-groups.ts @@ -4,6 +4,7 @@ import { AssetHoverDescription } from './assets/AssetHoverDescription'; import { SolutionHoverDescription } from './solutions/SolutionHoverDescription'; import { RegionHoverDescription } from './regions/RegionHoverDescription'; import { DroughtHoverDescription } from './drought/DroughtHoverDescription'; +import { RiskHoverDescription } from './risks/RiskHoverDescription'; export const INTERACTION_GROUPS = new Map([ [ @@ -25,6 +26,15 @@ export const INTERACTION_GROUPS = new Map([ pickMultiple: true, }, ], + [ + 'risks', + { + id: 'risks', + type: 'raster', + pickMultiple: false, + Component: RiskHoverDescription, + }, + ], [ 'regions', { diff --git a/frontend/src/config/risks/RiskHoverDescription.tsx b/frontend/src/config/risks/RiskHoverDescription.tsx new file mode 100644 index 00000000..d79ccfac --- /dev/null +++ b/frontend/src/config/risks/RiskHoverDescription.tsx @@ -0,0 +1,20 @@ +import { FC } from 'react'; + +import { RasterHoverDescription } from 'lib/data-map/types'; +import { RasterHoverDescription as RasterTooltip } from 'map/tooltip/content/RasterHoverDescription'; + +import { RISKS_METADATA, RISKS_COLOR_MAPS } 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/config/risks/RiskLegend.tsx b/frontend/src/config/risks/RiskLegend.tsx new file mode 100644 index 00000000..20f63f16 --- /dev/null +++ b/frontend/src/config/risks/RiskLegend.tsx @@ -0,0 +1,13 @@ +import { FC } from 'react'; + +import { RasterLegend } from 'map/legend/RasterLegend'; +import { ViewLayer } from 'lib/data-map/view-layers'; + +import { RISKS_COLOR_MAPS, 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/config/risks/domains.ts b/frontend/src/config/risks/domains.ts new file mode 100644 index 00000000..01b9a1a7 --- /dev/null +++ b/frontend/src/config/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/config/risks/metadata.ts b/frontend/src/config/risks/metadata.ts new file mode 100644 index 00000000..16540e06 --- /dev/null +++ b/frontend/src/config/risks/metadata.ts @@ -0,0 +1,63 @@ +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_COLOR_MAPS = { + 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], + }, +}; + +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..10534ada --- /dev/null +++ b/frontend/src/config/risks/risk-view-layer.ts @@ -0,0 +1,93 @@ +import { createElement } from 'react'; +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 { RasterTarget } from 'lib/data-map/types'; + +import { RiskLegend } from './RiskLegend'; +import { RiskHoverDescription } from './RiskHoverDescription'; +import { RISKS_COLOR_MAPS } from './metadata'; +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({ + key, + target, + viewLayer, + }: { + key?: string; + target: RasterTarget; + viewLayer: ViewLayer; + }) { + return createElement(RiskHoverDescription, { key, target, viewLayer }); + }, + }; +} diff --git a/frontend/src/config/risks/source.ts b/frontend/src/config/risks/source.ts new file mode 100644 index 00000000..dff1e362 --- /dev/null +++ b/frontend/src/config/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/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 { return ( <> + diff --git a/frontend/src/sidebar/risks/RisksControl.tsx b/frontend/src/sidebar/risks/RisksControl.tsx new file mode 100644 index 00000000..7c8ed53e --- /dev/null +++ b/frontend/src/sidebar/risks/RisksControl.tsx @@ -0,0 +1,69 @@ +import { + FormControl, + FormControlLabel, + FormLabel, + MenuItem, + Radio, + RadioGroup, + Select, +} from '@mui/material'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import { dataParamState, useUpdateDataParam } from 'state/data-params'; +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 { risksSelectionState } from 'state/layers/modules/risks'; +import { HAZARDS, HAZARDS_METADATA, RISKS, RISKS_METADATA } from 'config/risks/metadata'; + +export const RisksControl = () => { + const [risksSelection, setRisksSelection] = useRecoilState(risksSelectionState); + + function selectType({ target }) { + setRisksSelection(target?.value); + } + + const hazard = useRecoilValue(dataParamState({ group: 'risks', param: 'hazard' })); + const updateHazard = useUpdateDataParam('risks', 'hazard'); + function selectHazard(event, value) { + updateHazard(value); + } + + return ( + <> + + + Display + variant="standard" value={risksSelection} onChange={selectType}> + {RISKS.map((risk) => ( + + {RISKS_METADATA[risk].label} + + ))} + + + + + + Hazard + + {HAZARDS.map((hazardOption) => ( + } + /> + ))} + + + + + + + + + + + ); +}; 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..b405a245 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, + risks: RISK_DOMAINS, all: totalDamagesConfig, adaptation: adaptationDomainsConfig, }; diff --git a/frontend/src/state/layers/modules/risks.ts b/frontend/src/state/layers/modules/risks.ts new file mode 100644 index 00000000..f84e1386 --- /dev/null +++ b/frontend/src/state/layers/modules/risks.ts @@ -0,0 +1,22 @@ +import { RiskParams } from 'config/risks/domains'; +import { riskViewLayer } from 'config/risks/risk-view-layer'; +import { ViewLayer } from 'lib/data-map/view-layers'; +import { selector, atom } from 'recoil'; +import { dataParamsByGroupState } from 'state/data-params'; +import { sectionVisibilityState } from 'state/sections'; + +export const risksSelectionState = atom({ + key: 'riskSelectionState', + default: 'totalValue', +}); + +export const risksLayerState = selector({ + key: 'risksLayerState', + get: ({ get }) => { + const risksSelection = get(risksSelectionState); + const dataParams = get(dataParamsByGroupState('risks')); + return get(sectionVisibilityState('risks')) + ? [riskViewLayer(risksSelection, dataParams as RiskParams)] + : []; + }, +});