Skip to content

Commit

Permalink
Add fuel types and critical hours to advisory
Browse files Browse the repository at this point in the history
  • Loading branch information
dgboss committed Sep 11, 2024
1 parent fac4b2f commit 4b7126e
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 85 deletions.
2 changes: 1 addition & 1 deletion web/src/app/rootReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const selectFireCenters = (state: RootState) => state.fireCenters
export const selectFireShapeAreas = (state: RootState) => state.fireShapeAreas
export const selectRunDates = (state: RootState) => state.runDates
export const selectValueAtCoordinate = (state: RootState) => state.valueAtCoordinate
export const selectFireCentreHFIFuelTypes = (state: RootState) => state.fireCentreHFIFuelStats
export const selectFireCentreHFIFuelStats = (state: RootState) => state.fireCentreHFIFuelStats
export const selectFireZoneElevationInfo = (state: RootState) => state.fireZoneElevationInfo
export const selectFireCentreTPIStats = (state: RootState) => state.fireCentreTPIStats
export const selectHFIDailiesLoading = (state: RootState): boolean => state.hfiCalculatorDailies.fireCentresLoading
Expand Down
5 changes: 5 additions & 0 deletions web/src/features/fba/calculateZoneStatus.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FireShapeAreaDetail } from '@/api/fbaAPI'
import { ADVISORY_ORANGE_FILL, ADVISORY_RED_FILL } from '@/features/fba/components/map/featureStylers'
import { AdvisoryStatus } from '@/utils/constants'
import { isUndefined } from 'lodash'

export const calculateStatusColour = (
details: FireShapeAreaDetail[],
Expand Down Expand Up @@ -33,6 +34,10 @@ export const calculateStatusText = (
details: FireShapeAreaDetail[],
advisoryThreshold: number
): AdvisoryStatus | undefined => {
if (isUndefined(details) || details.length === 0) {
return undefined
}

const advisoryThresholdDetail = details.find(detail => detail.threshold == 1)
const warningThresholdDetail = details.find(detail => detail.threshold == 2)
const advisoryPercentage = advisoryThresholdDetail?.elevated_hfi_percentage ?? 0
Expand Down
6 changes: 4 additions & 2 deletions web/src/features/fba/components/infoPanel/AdvisoryReport.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Box, Tabs, Tab, Grid } from '@mui/material'
import { FireCenter } from 'api/fbaAPI'
import { FireCenter, FireShape } 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'
Expand All @@ -11,6 +11,7 @@ interface AdvisoryReportProps {
forDate: DateTime
advisoryThreshold: number
selectedFireCenter?: FireCenter
selectedFireZoneUnit?: FireShape
}

interface TabPanelProps {
Expand All @@ -27,7 +28,7 @@ const TabPanel = ({ children, index, value }: TabPanelProps) => {
)
}

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

const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
Expand All @@ -54,6 +55,7 @@ const AdvisoryReport = ({ issueDate, forDate, advisoryThreshold, selectedFireCen
forDate={forDate}
advisoryThreshold={advisoryThreshold}
selectedFireCenter={selectedFireCenter}
selectedFireZoneUnit={selectedFireZoneUnit}
></AdvisoryText>
</TabPanel>
</Grid>
Expand Down
143 changes: 102 additions & 41 deletions web/src/features/fba/components/infoPanel/AdvisoryText.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,103 @@
import { Box, Typography } from '@mui/material'
import { FireCenter, FireShapeAreaDetail } from 'api/fbaAPI'
import { FireCenter, FireShape, FireZoneFuelStats } from 'api/fbaAPI'
import { DateTime } from 'luxon'
import React from 'react'
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { selectProvincialSummary } from 'features/fba/slices/provincialSummarySlice'
import { selectFireCentreHFIFuelStats } from '@/app/rootReducer'
import { AdvisoryStatus } from 'utils/constants'
import { groupBy } from 'lodash'
import { isEmpty, isUndefined } from 'lodash'
import { calculateStatusText } from '@/features/fba/calculateZoneStatus'

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

const AdvisoryText = ({ issueDate, forDate, advisoryThreshold, selectedFireCenter }: AdvisoryTextProps) => {
const AdvisoryText = ({
issueDate,
forDate,
selectedFireCenter,
advisoryThreshold,
selectedFireZoneUnit
}: AdvisoryTextProps) => {
const provincialSummary = useSelector(selectProvincialSummary)
const { fireCentreHFIFuelStats } = useSelector(selectFireCentreHFIFuelStats)
const [selectedFireZoneUnitTopFuels, setSelectedFireZoneUnitTopFuels] = useState<FireZoneFuelStats[]>([])

const getZoneStatusMap = (fireZoneUnitDetails: Record<string, FireShapeAreaDetail[]>) => {
const zoneStatusMap: Record<AdvisoryStatus, string[]> = {
[AdvisoryStatus.ADVISORY]: [],
[AdvisoryStatus.WARNING]: []
const [minStartTime, setMinStartTime] = useState<number | undefined>(undefined)
const [maxEndTime, setMaxEndTime] = useState<number | undefined>(undefined)

const sortByArea = (a: FireZoneFuelStats, b: FireZoneFuelStats) => {
if (a.area > b.area) {
return -1
}
if (a.area < b.area) {
return 1
}
return 0
}

for (const zoneUnit in fireZoneUnitDetails) {
const fireShapeAreaDetails: FireShapeAreaDetail[] = fireZoneUnitDetails[zoneUnit]
const status = calculateStatusText(fireShapeAreaDetails, advisoryThreshold)
useEffect(() => {
if (
isUndefined(fireCentreHFIFuelStats) ||
isEmpty(fireCentreHFIFuelStats) ||
isUndefined(selectedFireCenter) ||
isUndefined(selectedFireZoneUnit)
) {
setSelectedFireZoneUnitTopFuels([])
setMinStartTime(undefined)
setMaxEndTime(undefined)
return
}
const allZoneUnitFuelStats = fireCentreHFIFuelStats?.[selectedFireCenter.name]
const selectedZoneUnitFuelStats = allZoneUnitFuelStats?.[selectedFireZoneUnit.fire_shape_id] ?? []
const sortedFuelStats = [...selectedZoneUnitFuelStats].sort(sortByArea)
let topFuels: FireZoneFuelStats[] = []
for (let i = 0; i < 3 && i < sortedFuelStats.length; i++) {
topFuels.push(sortedFuelStats[i])
}
setSelectedFireZoneUnitTopFuels(topFuels)
}, [fireCentreHFIFuelStats])

if (status) {
zoneStatusMap[status].push(zoneUnit)
useEffect(() => {
let startTime: number | undefined = undefined
let endTime: number | undefined = undefined
for (const fuel of selectedFireZoneUnitTopFuels) {
if (!isUndefined(fuel.critical_hours.start_time)) {
if (isUndefined(startTime)) {
startTime = fuel.critical_hours.start_time
} else if (fuel.critical_hours.start_time < startTime) {
startTime = fuel.critical_hours.start_time
}
}
if (!isUndefined(fuel.critical_hours.end_time)) {
if (isUndefined(endTime)) {
endTime = fuel.critical_hours.end_time
} else if (fuel.critical_hours.end_time > endTime) {
endTime = fuel.critical_hours.end_time
}
}
}
setMinStartTime(startTime)
setMaxEndTime(endTime)
}, [selectedFireZoneUnitTopFuels])

return zoneStatusMap
const getTopFuelsString = () => {
const topFuelCodes = selectedFireZoneUnitTopFuels.map(topFuel => topFuel.fuel_type.fuel_type_code)
switch (topFuelCodes.length) {
case 1:
return `fuel type ${topFuelCodes[0]}`
case 2:
return `fuel types ${topFuelCodes[0]} and ${topFuelCodes[1]}`
case 3:
return `fuel types ${topFuelCodes[0]}, ${topFuelCodes[1]} and ${topFuelCodes[2]}`
default:
return ''
}
}

const renderDefaultMessage = () => {
Expand All @@ -53,8 +117,16 @@ const AdvisoryText = ({ issueDate, forDate, advisoryThreshold, selectedFireCente
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)
const fireZoneUnitInfos = fireCenterSummary?.filter(fc => fc.fire_shape_id === selectedFireZoneUnit?.fire_shape_id)
const zoneStatus = calculateStatusText(fireZoneUnitInfos, advisoryThreshold)
const hasCriticalHours =
!isUndefined(minStartTime) && !isUndefined(maxEndTime) && selectFireCentreHFIFuelStats.length > 0
let message = ''
if (hasCriticalHours) {
message = `There is a fire behaviour ${zoneStatus} in effect for ${selectedFireZoneUnit?.mof_fire_zone_name} between ${minStartTime}:00 and ${maxEndTime}:00 for ${getTopFuelsString()}.`
} else {
message = `There is a fire behaviour ${zoneStatus} in effect for ${selectedFireZoneUnit?.mof_fire_zone_name}.`
}

return (
<>
Expand All @@ -63,33 +135,20 @@ const AdvisoryText = ({ issueDate, forDate, advisoryThreshold, selectedFireCente
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>
</>
{!isUndefined(zoneStatus) && zoneStatus === AdvisoryStatus.ADVISORY && (
<Typography data-testid="advisory-message-advisory">{message}</Typography>
)}
{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>
</>
{!isUndefined(zoneStatus) && zoneStatus === AdvisoryStatus.WARNING && (
<Typography data-testid="advisory-message-warning">{message}</Typography>
)}
{!hasCriticalHours && (
<Typography data-testid="advisory-message-no-critical-hours" sx={{ paddingTop: '1rem' }}>
No critical hours available.
</Typography>
)}
{zoneStatusMap[AdvisoryStatus.WARNING].length === 0 && zoneStatusMap[AdvisoryStatus.ADVISORY].length === 0 && (
{isUndefined(zoneStatus) && (
<Typography data-testid="no-advisory-message">
No advisories or warnings issued for the selected fire center.
No advisories or warnings issued for the selected fire zone unit.
</Typography>
)}
</>
Expand All @@ -109,7 +168,9 @@ const AdvisoryText = ({ issueDate, forDate, advisoryThreshold, selectedFireCente
backgroundColor: 'white'
}}
>
{!selectedFireCenter || !issueDate?.isValid ? renderDefaultMessage() : renderAdvisoryText()}
{!selectedFireCenter || !issueDate?.isValid || !selectedFireZoneUnit
? renderDefaultMessage()
: renderAdvisoryText()}
</Box>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { selectFireCentreHFIFuelTypes, selectFireCentreTPIStats } from '@/app/rootReducer'
import { selectFireCentreHFIFuelStats, selectFireCentreTPIStats } from '@/app/rootReducer'
import { calculateStatusColour } from '@/features/fba/calculateZoneStatus'
import { Box, Grid, Tab, Tabs, Tooltip } from '@mui/material'
import { FireCenter, FireShape } from 'api/fbaAPI'
Expand Down Expand Up @@ -27,7 +27,7 @@ const FireZoneUnitTabs = ({
setSelectedFireShape
}: FireZoneUnitTabs) => {
const { fireCentreTPIStats } = useSelector(selectFireCentreTPIStats)
const { fireCentreHFIFuelStats } = useSelector(selectFireCentreHFIFuelTypes)
const { fireCentreHFIFuelStats } = useSelector(selectFireCentreHFIFuelStats)
const [tabNumber, setTabNumber] = useState(0)

const sortedGroupedFireZoneUnits = useFireCentreDetails(selectedFireCenter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import provincialSummarySlice, {
initialState,
ProvincialSummaryState
} from 'features/fba/slices/provincialSummarySlice'
import fireCentreHFIFuelStatsSlice from '@/features/fba/slices/fireCentreHFIFuelStatsSlice'

import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { Provider } from 'react-redux'

const buildTestStore = (initialState: ProvincialSummaryState) => {
const rootReducer = combineReducers({ provincialSummary: provincialSummarySlice })
const rootReducer = combineReducers({
provincialSummary: provincialSummarySlice,
fireCentreHFIFuelStats: fireCentreHFIFuelStatsSlice
})
const testStore = configureStore({
reducer: rootReducer,
preloadedState: {
Expand Down
Loading

0 comments on commit 4b7126e

Please sign in to comment.