diff --git a/web/src/features/fba/components/infoPanel/AdvisoryReport.tsx b/web/src/features/fba/components/infoPanel/AdvisoryReport.tsx index 6d267b8680..3d96e12853 100644 --- a/web/src/features/fba/components/infoPanel/AdvisoryReport.tsx +++ b/web/src/features/fba/components/infoPanel/AdvisoryReport.tsx @@ -61,7 +61,7 @@ const AdvisoryReport = ({ - + diff --git a/web/src/features/fba/components/infoPanel/AdvisoryText.tsx b/web/src/features/fba/components/infoPanel/AdvisoryText.tsx index 027d4a53e9..ca2e3d3cb7 100644 --- a/web/src/features/fba/components/infoPanel/AdvisoryText.tsx +++ b/web/src/features/fba/components/infoPanel/AdvisoryText.tsx @@ -6,7 +6,7 @@ import { useSelector } from 'react-redux' import { selectProvincialSummary } from 'features/fba/slices/provincialSummarySlice' import { selectFireCentreHFIFuelStats } from '@/app/rootReducer' import { AdvisoryStatus } from 'utils/constants' -import { isEmpty, isNil, isUndefined, take } from 'lodash' +import { isEmpty, isNil, isUndefined } from 'lodash' import { calculateStatusText } from '@/features/fba/calculateZoneStatus' interface AdvisoryTextProps { @@ -30,6 +30,7 @@ const AdvisoryText = ({ const [minStartTime, setMinStartTime] = useState(undefined) const [maxEndTime, setMaxEndTime] = useState(undefined) + const [highHFIFuelsByProportion, setHighHFIFuelsByProportion] = useState([]) const sortByArea = (a: FireZoneFuelStats, b: FireZoneFuelStats) => { if (a.area > b.area) { @@ -41,6 +42,31 @@ const AdvisoryText = ({ return 0 } + // Return a list of fuel stats for which greater than 90% of the area of each fuel type has high HFI. + const getTopFuelsByProportion = (zoneUnitFuelStats: FireZoneFuelStats[]): FireZoneFuelStats[] => { + const topFuelsByProportion = zoneUnitFuelStats.filter(fuelStats => { + return fuelStats.area / fuelStats.fuel_area >= 0.9 + }) + return topFuelsByProportion + } + + // Return a list of fuel types that cumulatively account for more than 75% of total area with high HFI. + const getTopFuelsByArea = (zoneUnitFuelStats: FireZoneFuelStats[]): FireZoneFuelStats[] => { + const totalHighHFIArea = zoneUnitFuelStats.reduce((total, stats) => total + stats.area, 0) + const sortedFuelStats = [...zoneUnitFuelStats].sort(sortByArea) + const topFuelsByArea = [] + let highHFIArea = 0 + for (const stats of sortedFuelStats) { + highHFIArea += stats.area + if (highHFIArea / totalHighHFIArea > 0.75) { + topFuelsByArea.push(stats) + return topFuelsByArea + } + topFuelsByArea.push(stats) + } + return topFuelsByArea + } + useEffect(() => { if ( isUndefined(fireCentreHFIFuelStats) || @@ -51,14 +77,17 @@ const AdvisoryText = ({ setSelectedFireZoneUnitTopFuels([]) setMinStartTime(undefined) setMaxEndTime(undefined) + setHighHFIFuelsByProportion([]) return } const allZoneUnitFuelStats = fireCentreHFIFuelStats?.[selectedFireCenter.name] const selectedZoneUnitFuelStats = allZoneUnitFuelStats?.[selectedFireZoneUnit.fire_shape_id] ?? [] const sortedFuelStats = [...selectedZoneUnitFuelStats].sort(sortByArea) - let topFuels = take(sortedFuelStats, 3) + const topFuels = getTopFuelsByArea(sortedFuelStats) setSelectedFireZoneUnitTopFuels(topFuels) - }, [fireCentreHFIFuelStats]) + const topFuelsByProportion = getTopFuelsByProportion(selectedZoneUnitFuelStats) + setHighHFIFuelsByProportion(topFuelsByProportion) + }, [fireCentreHFIFuelStats, selectedFireZoneUnit]) useEffect(() => { let startTime: number | undefined = undefined @@ -79,17 +108,39 @@ const AdvisoryText = ({ setMaxEndTime(endTime) }, [selectedFireZoneUnitTopFuels]) + const getCommaSeparatedString = (array: string[]): string => { + // Slice off the last two items and join then with ' and ' to create a new string. Then take the first n-2 items and + // deconstruct them into a new array along with the new string. Finally, join the items in the new array with ', '. + const joinedFuelTypes = [...array.slice(0, -2), array.slice(-2).join(' and ')].join(', ') + return joinedFuelTypes + } + const getTopFuelsString = () => { const topFuelCodes = selectedFireZoneUnitTopFuels.map(topFuel => topFuel.fuel_type.fuel_type_code) + const zoneStatus = getZoneStatus()?.toLowerCase() switch (topFuelCodes.length) { + case 0: + return '' case 1: - return `fuel type ${topFuelCodes[0]}` + return `Fuel type ${topFuelCodes[0]} accounts for >=75% of the area under ${zoneStatus}` case 2: - return `fuel types ${topFuelCodes[0]} and ${topFuelCodes[1]}` - case 3: - return `fuel types ${topFuelCodes[0]}, ${topFuelCodes[1]} and ${topFuelCodes[2]}` + return `Fuel types ${topFuelCodes[0]} and ${topFuelCodes[1]} account for >=75% of the area under ${zoneStatus}.` default: + return `Fuel types ${getCommaSeparatedString(topFuelCodes)} account for >=75% of the area under ${zoneStatus}.` + } + } + + const getHighProportionFuelsString = (): string => { + const array = highHFIFuelsByProportion.map(fuel_type => fuel_type.fuel_type.fuel_type_code) + switch (array.length) { + case 0: return '' + case 1: + return `Fuel type ${array[0]} is under advisory in >=90% of the areas it occurs in the fire zone.` + case 2: + return `Fuel types ${array[0]} and ${array[1]} are under advisory in >=90% of the areas they occur in the fire zone.` + default: + return `Fuel types ${getCommaSeparatedString(array)} are under advisory in >=90% of the areas they occur in the fire zone.` } } @@ -100,24 +151,28 @@ const AdvisoryText = ({ Please select a fire center. ) : ( No advisory data available for the selected date. - )}{' '} + )} ) } - const renderAdvisoryText = () => { - const forToday = forDate.toISODate() === DateTime.now().toISODate() - const displayForDate = forToday ? 'today' : forDate.toLocaleString({ month: 'short', day: 'numeric' }) - + const getZoneStatus = () => { const fireCenterSummary = provincialSummary[selectedFireCenter!.name] const fireZoneUnitInfos = fireCenterSummary?.filter(fc => fc.fire_shape_id === selectedFireZoneUnit?.fire_shape_id) const zoneStatus = calculateStatusText(fireZoneUnitInfos, advisoryThreshold) + return zoneStatus + } + + const renderAdvisoryText = () => { + const forToday = forDate.toISODate() === DateTime.now().toISODate() + const displayForDate = forToday ? 'today' : forDate.toLocaleString({ month: 'short', day: 'numeric' }) + const zoneStatus = getZoneStatus() const hasCriticalHours = !isNil(minStartTime) && !isNil(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()}.` + message = `There is a fire behaviour ${zoneStatus} in effect for ${selectedFireZoneUnit?.mof_fire_zone_name} between ${minStartTime}:00 and ${maxEndTime}:00. ${getTopFuelsString()}.\n\n` } else { - message = `There is a fire behaviour ${zoneStatus} in effect for ${selectedFireZoneUnit?.mof_fire_zone_name}.` + message = `There is a fire behaviour ${zoneStatus} in effect for ${selectedFireZoneUnit?.mof_fire_zone_name}.\n\n` } return ( @@ -128,16 +183,23 @@ const AdvisoryText = ({ >{`Issued on ${issueDate?.toLocaleString(DateTime.DATE_MED)} for ${displayForDate}.\n\n`} )} {!isUndefined(zoneStatus) && zoneStatus === AdvisoryStatus.ADVISORY && ( - {message} + + {message} + )} {!isUndefined(zoneStatus) && zoneStatus === AdvisoryStatus.WARNING && ( - {message} + + {message} + )} - {!hasCriticalHours && !isUndefined(zoneStatus) && ( - - No critical hours available. + {!isUndefined(zoneStatus) && ( + + {getHighProportionFuelsString()} )} + {!hasCriticalHours && !isUndefined(zoneStatus) && ( + No critical hours available. + )} {isUndefined(zoneStatus) && ( No advisories or warnings issued for the selected fire zone unit. diff --git a/web/src/features/fba/components/infoPanel/advisoryText.test.tsx b/web/src/features/fba/components/infoPanel/advisoryText.test.tsx index 918ffce5ea..6c9a5309e3 100644 --- a/web/src/features/fba/components/infoPanel/advisoryText.test.tsx +++ b/web/src/features/fba/components/infoPanel/advisoryText.test.tsx @@ -187,9 +187,11 @@ describe('AdvisoryText', () => { ) const warningMessage = queryByTestId('advisory-message-warning') const advisoryMessage = queryByTestId('advisory-message-advisory') + const proportionMessage = queryByTestId('advisory-message-proportion') const noAdvisoryMessage = queryByTestId('no-advisory-message') expect(advisoryMessage).not.toBeInTheDocument() expect(warningMessage).not.toBeInTheDocument() + expect(proportionMessage).not.toBeInTheDocument() expect(noAdvisoryMessage).toBeInTheDocument() }) @@ -211,7 +213,9 @@ describe('AdvisoryText', () => { ) const advisoryMessage = queryByTestId('advisory-message-advisory') const warningMessage = queryByTestId('advisory-message-warning') + const proportionMessage = queryByTestId('advisory-message-proportion') expect(advisoryMessage).not.toBeInTheDocument() + expect(proportionMessage).toBeInTheDocument() expect(warningMessage).toBeInTheDocument() }) @@ -229,7 +233,9 @@ describe('AdvisoryText', () => { ) const advisoryMessage = queryByTestId('advisory-message-advisory') const warningMessage = queryByTestId('advisory-message-warning') + const proportionMessage = queryByTestId('advisory-message-proportion') expect(advisoryMessage).toBeInTheDocument() + expect(proportionMessage).toBeInTheDocument() expect(warningMessage).not.toBeInTheDocument() })