Skip to content

Commit

Permalink
Fire Center Advisory Report (#3850)
Browse files Browse the repository at this point in the history
- Advisory bulletins by fire center
- Fixes zoom to province extent
  • Loading branch information
brettedw authored and conbrad committed Aug 19, 2024
1 parent f36cb61 commit 4e651ee
Show file tree
Hide file tree
Showing 17 changed files with 633 additions and 221 deletions.
4 changes: 2 additions & 2 deletions web/src/app/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ export const DARK_GREY = '#A7A7A7'
export const LIGHT_GREY = '#DADADA'
export const MEDIUM_GREY = '#B5B5B5'

export const INFO_PANEL_HEADER_BACKGORUND = '#e4e4e5'
export const INFO_PANEL_CONTENT_BACKGORUND = '#f0f0f0'
export const INFO_PANEL_HEADER_BACKGROUND = '#e4e4e5'
export const INFO_PANEL_CONTENT_BACKGROUND = '#f0f0f0'
export const TRANSPARENT_COLOUR = '#0000'

interface WeatherParams {
Expand Down
6 changes: 5 additions & 1 deletion web/src/components/FireCenterDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { TextField, Autocomplete } from '@mui/material'
import { FireCenter } from 'api/fbaAPI'
import { FireCenter, FireShape } from 'api/fbaAPI'
import { isEqual } from 'lodash'
import React from 'react'

interface FireCenterDropdownProps {
selectedFireCenter?: FireCenter
fireCenterOptions: FireCenter[]
setSelectedFireCenter: React.Dispatch<React.SetStateAction<FireCenter | undefined>>
setSelectedFireShape: React.Dispatch<React.SetStateAction<FireShape | undefined>>
setZoomSource: React.Dispatch<React.SetStateAction<'fireCenter' | 'fireShape' | undefined>>
}

const FireCenterDropdown = (props: FireCenterDropdownProps) => {
// eslint-disable-next-line
const changeHandler = (_: React.ChangeEvent<{}>, value: any | null) => {
if (!isEqual(props.selectedFireCenter, value)) {
props.setSelectedFireShape(undefined)
props.setSelectedFireCenter(value)
props.setZoomSource('fireCenter')
}
}

Expand Down
40 changes: 13 additions & 27 deletions web/src/features/fba/components/AdvisoryMetadata.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,31 @@
import { Autocomplete, Box, Grid, TextField, Typography } from '@mui/material'
import { Autocomplete, TextField } from '@mui/material'
import React from 'react'
import { DateTime } from 'luxon'
import { RunType } from 'features/fba/pages/FireBehaviourAdvisoryPage'
import { isNull } from 'lodash'

export interface AdvisoryMetadataProps {
testId?: string
runType: string
setRunType: React.Dispatch<React.SetStateAction<RunType>>
forDate: DateTime
issueDate: DateTime | null
}
const AdvisoryMetadata = ({ runType, setRunType, forDate, issueDate }: AdvisoryMetadataProps) => {
// eslint-disable-next-line
const AdvisoryMetadata = ({ runType, setRunType }: AdvisoryMetadataProps) => {
const changeHandler = (_: React.ChangeEvent<{}>, value: any | null) => {
if (!isNull(value)) {
setRunType(value)
}
}
return (
<Box sx={{ width: 270 }}>
<Grid container spacing={2}>
<Grid item xs>
<Autocomplete
disablePortal
disableClearable
autoComplete
size="small"
id="asa-forecast-actual-select"
options={[RunType.FORECAST, RunType.ACTUAL]}
defaultValue={runType}
onChange={changeHandler}
renderInput={params => <TextField {...params} label="Forecast or Actual" />}
/>{' '}
<Typography variant="subtitle2">is for {forDate.toISODate()}</Typography>
<Typography variant="subtitle2">
issued on {!isNull(issueDate) ? issueDate.toISO({ includeOffset: false }) : ''}
</Typography>
</Grid>
</Grid>
</Box>
<Autocomplete
disablePortal
disableClearable
autoComplete
size="small"
id="asa-forecast-actual-select"
options={[RunType.FORECAST, RunType.ACTUAL]}
defaultValue={runType}
onChange={changeHandler}
renderInput={params => <TextField {...params} label="Forecast or Actual" />}
/>
)
}

Expand Down
66 changes: 66 additions & 0 deletions web/src/features/fba/components/infoPanel/AdvisoryReport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Box, Tabs, Tab, Grid } from '@mui/material'
import { FireCenter } from 'api/fbaAPI'
import { INFO_PANEL_CONTENT_BACKGROUND } from 'app/theme'
import AdvisoryText from 'features/fba/components/infoPanel/AdvisoryText'
import InfoAccordion from 'features/fba/components/infoPanel/InfoAccordion'
import { DateTime } from 'luxon'
import React, { useState } from 'react'

interface AdvisoryReportProps {
issueDate: DateTime | null
forDate: DateTime
advisoryThreshold: number
selectedFireCenter?: FireCenter
}

interface TabPanelProps {
children?: React.ReactNode
index: number
value: number
}

const TabPanel = ({ children, index, value }: TabPanelProps) => {
return (
<div hidden={value !== index} id={`tabpanel-${index}`} data-testid={`tabpanel-${index}`}>
{value === index && <Box paddingBottom={3}>{children}</Box>}
</div>
)
}

const AdvisoryReport = ({ issueDate, forDate, advisoryThreshold, selectedFireCenter }: AdvisoryReportProps) => {
const [tabNumber, setTabNumber] = useState(0)

const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabNumber(newValue)
}

return (
<div data-testid="advisory-report">
<InfoAccordion
defaultExpanded={true}
title={'Advisory Report'}
accordionDetailBackgroundColour={INFO_PANEL_CONTENT_BACKGROUND}
>
<Grid container justifyContent="center">
<Grid item sx={{ width: '90%' }}>
<Box>
<Tabs value={tabNumber} onChange={handleTabChange}>
<Tab label="BULLETIN" />
</Tabs>
</Box>
<TabPanel value={tabNumber} index={0}>
<AdvisoryText
issueDate={issueDate}
forDate={forDate}
advisoryThreshold={advisoryThreshold}
selectedFireCenter={selectedFireCenter}
></AdvisoryText>
</TabPanel>
</Grid>
</Grid>
</InfoAccordion>
</div>
)
}

export default AdvisoryReport
132 changes: 132 additions & 0 deletions web/src/features/fba/components/infoPanel/AdvisoryText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Box, Typography } from '@mui/material'
import { FireCenter, FireShapeAreaDetail } from 'api/fbaAPI'
import { DateTime } from 'luxon'
import React from 'react'
import { useSelector } from 'react-redux'
import { selectProvincialSummary } from 'features/fba/slices/provincialSummarySlice'
import { AdvisoryStatus } from 'utils/constants'
import { groupBy } from 'lodash'

interface AdvisoryTextProps {
issueDate: DateTime | null
forDate: DateTime
selectedFireCenter?: FireCenter
advisoryThreshold: number
}

const AdvisoryText = ({ issueDate, forDate, advisoryThreshold, selectedFireCenter }: AdvisoryTextProps) => {
const provincialSummary = useSelector(selectProvincialSummary)

const calculateStatus = (details: FireShapeAreaDetail[]): AdvisoryStatus | undefined => {
const advisoryThresholdDetail = details.find(detail => detail.threshold == 1)
const warningThresholdDetail = details.find(detail => detail.threshold == 2)
const advisoryPercentage = advisoryThresholdDetail?.elevated_hfi_percentage ?? 0
const warningPercentage = warningThresholdDetail?.elevated_hfi_percentage ?? 0

if (warningPercentage > advisoryThreshold) {
return AdvisoryStatus.WARNING
}

if (advisoryPercentage + warningPercentage > advisoryThreshold) {
return AdvisoryStatus.ADVISORY
}
}

const getZoneStatusMap = (fireZoneUnitDetails: Record<string, FireShapeAreaDetail[]>) => {
const zoneStatusMap: Record<AdvisoryStatus, string[]> = {
[AdvisoryStatus.ADVISORY]: [],
[AdvisoryStatus.WARNING]: []
}

for (const zoneUnit in fireZoneUnitDetails) {
const fireShapeAreaDetails: FireShapeAreaDetail[] = fireZoneUnitDetails[zoneUnit]
const status = calculateStatus(fireShapeAreaDetails)

if (status) {
zoneStatusMap[status].push(zoneUnit)
}
}

return zoneStatusMap
}

const renderDefaultMessage = () => {
return (
<>
{issueDate?.isValid ? (
<Typography data-testid="default-message">Please select a fire center.</Typography>
) : (
<Typography data-testid="no-data-message">No advisory data available for today.</Typography>
)}{' '}
</>
)
}

const renderAdvisoryText = () => {
const forToday = issueDate?.toISODate() === forDate.toISODate()
const displayForDate = forToday ? 'today' : forDate.toLocaleString({ month: 'short', day: 'numeric' })

const fireCenterSummary = provincialSummary[selectedFireCenter!.name]
const groupedFireZoneUnitInfos = groupBy(fireCenterSummary, 'fire_shape_name')
const zoneStatusMap = getZoneStatusMap(groupedFireZoneUnitInfos)

return (
<>
{issueDate?.isValid && (
<Typography
sx={{ whiteSpace: 'pre-wrap' }}
>{`Issued on ${issueDate?.toLocaleString(DateTime.DATE_MED)} for ${displayForDate}.\n\n`}</Typography>
)}
{zoneStatusMap[AdvisoryStatus.WARNING].length > 0 && (
<>
<Typography data-testid="advisory-message-warning">{`There is a fire behaviour ${AdvisoryStatus.WARNING} in effect in the following areas:`}</Typography>
<ul>
{zoneStatusMap[AdvisoryStatus.WARNING].map(zone => (
<li key={zone}>
<Typography>{zone}</Typography>
</li>
))}
</ul>
</>
)}
{zoneStatusMap[AdvisoryStatus.ADVISORY].length > 0 && (
<>
<Typography data-testid="advisory-message-advisory">{`There is a fire behaviour ${AdvisoryStatus.ADVISORY} in effect in the following areas:`}</Typography>
<ul>
{zoneStatusMap[AdvisoryStatus.ADVISORY].map(zone => (
<li key={zone}>
<Typography>{zone}</Typography>
</li>
))}
</ul>
</>
)}
{zoneStatusMap[AdvisoryStatus.WARNING].length === 0 && zoneStatusMap[AdvisoryStatus.ADVISORY].length === 0 && (
<Typography data-testid="no-advisory-message">
No advisories or warnings issued for the selected fire center.
</Typography>
)}
</>
)
}

return (
<div data-testid="advisory-text">
<Box
sx={{
height: 350,
maxWidth: '100%',
overflow: 'auto',
border: '1px solid #ccc',
padding: 2,
borderRadius: 1,
backgroundColor: 'white'
}}
>
{!selectedFireCenter || !issueDate?.isValid ? renderDefaultMessage() : renderAdvisoryText()}
</Box>
</div>
)
}

export default AdvisoryText
6 changes: 3 additions & 3 deletions web/src/features/fba/components/infoPanel/FireCentreInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import FireZoneUnitInfo from 'features/fba/components/infoPanel/FireZoneUnitInfo'
import { groupBy } from 'lodash'
import { FireShapeAreaDetail } from 'api/fbaAPI'
import { INFO_PANEL_CONTENT_BACKGORUND } from 'app/theme'
import { INFO_PANEL_CONTENT_BACKGROUND } from 'app/theme'

interface FireCentreInfoProps {
advisoryThreshold: number
Expand All @@ -16,7 +16,7 @@ interface FireCentreInfoProps {
}

const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
backgroundColor: INFO_PANEL_CONTENT_BACKGORUND,
backgroundColor: INFO_PANEL_CONTENT_BACKGROUND,
flexDirection: 'row-reverse',
fontWeight: 'bold',
margin: '0px',
Expand Down Expand Up @@ -47,7 +47,7 @@ const FireCenterInfo = ({
<StyledAccordionSummary expandIcon={<ExpandMoreIcon />}>{fireCentreName}</StyledAccordionSummary>
<AccordionDetails
sx={{
backgroundColor: INFO_PANEL_CONTENT_BACKGORUND,
backgroundColor: INFO_PANEL_CONTENT_BACKGROUND,
padding: '0',
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react'
import CombustibleAreaViz from 'features/fba/components/viz/CombustibleAreaViz'
import { Grid, Typography } from '@mui/material'
import { isNull, isUndefined } from 'lodash'
import { FireShape, FireShapeArea, FireZoneTPIStats, FireZoneThresholdFuelTypeArea } from 'api/fbaAPI'
Expand Down Expand Up @@ -47,11 +46,6 @@ const FireZoneUnitSummary = ({
direction={'column'}
sx={{ paddingBottom: theme.spacing(2), paddingTop: theme.spacing(2) }}
>
<Grid item>
<CombustibleAreaViz
fireZoneAreas={fireShapeAreas.filter(area => area.fire_shape_id == selectedFireZoneUnit?.fire_shape_id)}
/>
</Grid>

<Grid item sx={{ paddingBottom: theme.spacing(2), width: '95%' }}>
{ Object.keys(fuelTypeInfo).length === 0 ? (
Expand Down
4 changes: 2 additions & 2 deletions web/src/features/fba/components/infoPanel/InfoAccordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react'
import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@mui/material'
import { styled, useTheme } from '@mui/material/styles'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import { INFO_PANEL_HEADER_BACKGORUND } from 'app/theme'
import { INFO_PANEL_HEADER_BACKGROUND } from 'app/theme'

interface InfoAccordionProps {
accordionDetailBackgroundColour?: string
Expand All @@ -13,7 +13,7 @@ interface InfoAccordionProps {
}

const StyledAccordionSummary = styled(AccordionSummary)(() => ({
backgroundColor: INFO_PANEL_HEADER_BACKGORUND,
backgroundColor: INFO_PANEL_HEADER_BACKGROUND,
['& .MuiAccordionSummary-content']: {
margin: 0
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import InfoAccordion from 'features/fba/components/infoPanel/InfoAccordion'
import { isNull, isUndefined } from 'lodash'
import { Typography } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { INFO_PANEL_CONTENT_BACKGORUND } from 'app/theme'
import { INFO_PANEL_CONTENT_BACKGROUND } from 'app/theme'
import { FireCentres } from 'utils/constants'

interface ProvincialSummaryProps {
Expand Down Expand Up @@ -68,7 +68,7 @@ const ProvincialSummary = ({ advisoryThreshold }: ProvincialSummaryProps) => {
return (
<div data-testid="provincial-summary">
<InfoAccordion
accordionDetailBackgroundColour={noProvincialSummary ? undefined : INFO_PANEL_CONTENT_BACKGORUND}
accordionDetailBackgroundColour={noProvincialSummary ? undefined : INFO_PANEL_CONTENT_BACKGROUND}
defaultExpanded={true}
title={'Provincial Summary'}
>
Expand Down
Loading

0 comments on commit 4e651ee

Please sign in to comment.