From bfebb1e86205bda227678b45944c27360624d57c Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Tue, 27 Aug 2024 10:18:14 -0700 Subject: [PATCH 01/42] initial work --- web/src/api/fbaAPI.ts | 3 +- .../infoPanel/FireZoneUnitSummary.tsx | 44 +++--- .../components/infoPanel/FireZoneUnitTabs.tsx | 148 ++++++++++++++++++ .../fba/components/infoPanel/TabPanel.tsx | 18 +++ .../fba/components/viz/FuelSummary.tsx | 3 +- .../fba/pages/FireBehaviourAdvisoryPage.tsx | 16 +- 6 files changed, 197 insertions(+), 35 deletions(-) create mode 100644 web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx create mode 100644 web/src/features/fba/components/infoPanel/TabPanel.tsx diff --git a/web/src/api/fbaAPI.ts b/web/src/api/fbaAPI.ts index 6e88eb32c..45dd54f33 100644 --- a/web/src/api/fbaAPI.ts +++ b/web/src/api/fbaAPI.ts @@ -17,7 +17,7 @@ export interface FireCenter { export interface FireShape { fire_shape_id: number mof_fire_zone_name: string - mof_fire_centre_name?: string + mof_fire_centre_name: string area_sqm?: number } @@ -169,7 +169,6 @@ export async function getFireZoneTPIStats( return data } - export async function getValueAtCoordinate( layer: string, latitude: number, diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx index ae4a550e2..e8b7cb5e9 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx @@ -2,7 +2,6 @@ import React from 'react' import { Grid, Typography } from '@mui/material' import { isNull, isUndefined } from 'lodash' import { FireShape, FireZoneTPIStats, FireZoneThresholdFuelTypeArea } from 'api/fbaAPI' -import InfoAccordion from 'features/fba/components/infoPanel/InfoAccordion' import ElevationStatus from 'features/fba/components/viz/ElevationStatus' import { useTheme } from '@mui/material/styles' import FuelSummary from 'features/fba/components/viz/FuelSummary' @@ -13,11 +12,7 @@ interface FireZoneUnitSummaryProps { fireZoneTPIStats: FireZoneTPIStats | null } -const FireZoneUnitSummary = ({ - fuelTypeInfo, - fireZoneTPIStats, - selectedFireZoneUnit -}: FireZoneUnitSummaryProps) => { +const FireZoneUnitSummary = ({ fuelTypeInfo, fireZoneTPIStats, selectedFireZoneUnit }: FireZoneUnitSummaryProps) => { const theme = useTheme() if (isUndefined(selectedFireZoneUnit)) { @@ -25,31 +20,28 @@ const FireZoneUnitSummary = ({ } return (
- - - - - - - - { isNull(fireZoneTPIStats) || fireZoneTPIStats.valley_bottom + fireZoneTPIStats.mid_slope + fireZoneTPIStats.upper_slope === 0 ? ( - - No elevation information available. - - ) : ( + + + + + + {isNull(fireZoneTPIStats) || + fireZoneTPIStats.valley_bottom + fireZoneTPIStats.mid_slope + fireZoneTPIStats.upper_slope === 0 ? ( + No elevation information available. + ) : ( )} - + > + )} - +
) } diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx new file mode 100644 index 000000000..555798a51 --- /dev/null +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -0,0 +1,148 @@ +import { Box, Grid, Tab, Tabs } from '@mui/material' +import { FireCenter, FireShape, FireShapeAreaDetail, FireZoneThresholdFuelTypeArea, FireZoneTPIStats } from 'api/fbaAPI' +import { INFO_PANEL_CONTENT_BACKGROUND, theme, TRANSPARENT_COLOUR } from 'app/theme' +import FireZoneUnitSummary from 'features/fba/components/infoPanel/FireZoneUnitSummary' +import InfoAccordion from 'features/fba/components/infoPanel/InfoAccordion' +import TabPanel from 'features/fba/components/infoPanel/TabPanel' +import { ADVISORY_ORANGE_FILL, ADVISORY_RED_FILL } from 'features/fba/components/map/featureStylers' +import { selectProvincialSummary } from 'features/fba/slices/provincialSummarySlice' +import { groupBy, isNull, isUndefined } from 'lodash' +import React, { useEffect, useMemo, useState } from 'react' +import { useSelector } from 'react-redux' + +interface FireZoneUnitTabs { + selectedFireZoneUnit: FireShape | undefined + fuelTypeInfo: Record + fireZoneTPIStats: FireZoneTPIStats | null + selectedFireCenter: FireCenter | undefined + advisoryThreshold: number + setSelectedFireShape: React.Dispatch> +} + +const calculateStatus = (details: FireShapeAreaDetail[], advisoryThreshold: number) => { + let status = 'white' + + if (details.length === 0) { + return status + } + + 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 (advisoryPercentage + warningPercentage > advisoryThreshold) { + status = ADVISORY_ORANGE_FILL + } + + if (warningPercentage > advisoryThreshold) { + status = ADVISORY_RED_FILL + } + + return status +} + +const FireZoneUnitTabs = ({ + fuelTypeInfo, + fireZoneTPIStats, + selectedFireZoneUnit, + selectedFireCenter, + advisoryThreshold, + setSelectedFireShape +}: FireZoneUnitTabs) => { + const provincialSummary = useSelector(selectProvincialSummary) + const [tabNumber, setTabNumber] = useState(0) + + const fireCenterSummary = selectedFireCenter ? provincialSummary[selectedFireCenter.name] : [] + const groupedFireZoneUnitInfos = useMemo(() => groupBy(fireCenterSummary, 'fire_shape_name'), [fireCenterSummary]) + const sortedZoneNames = useMemo(() => Object.keys(groupedFireZoneUnitInfos).sort(), [groupedFireZoneUnitInfos]) + + useEffect(() => { + if (selectedFireZoneUnit) { + const newIndex = sortedZoneNames.indexOf(selectedFireZoneUnit.mof_fire_zone_name) + if (newIndex !== -1) { + setTabNumber(newIndex) + } + } else { + setTabNumber(0) + setSelectedFireShape(getTabFireShape(0)) + } + }, [selectedFireZoneUnit, selectedFireCenter]) + + const getTabFireShape = (tabNumber: number): FireShape | undefined => { + if (sortedZoneNames.length > 0) { + const selectedTabZone = sortedZoneNames[tabNumber] + const selectedFireShapeInfo = groupedFireZoneUnitInfos[selectedTabZone][0] + + const fireShape: FireShape = { + fire_shape_id: selectedFireShapeInfo.fire_shape_id, + mof_fire_centre_name: selectedFireShapeInfo.fire_centre_name, + mof_fire_zone_name: selectedFireShapeInfo.fire_shape_name + } + + return fireShape + } + } + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setTabNumber(newValue) + + const fireShape = getTabFireShape(newValue) + setSelectedFireShape(fireShape) + } + + if (isUndefined(selectedFireCenter) || isNull(selectedFireCenter)) { + return
+ } + + return ( +
+ + + + + + {sortedZoneNames.map((key, index) => { + const isActive = tabNumber === index + return ( + + ) + })} + + + {sortedZoneNames.map((key, index) => ( + + + + ))} + + + +
+ ) +} + +export default FireZoneUnitTabs diff --git a/web/src/features/fba/components/infoPanel/TabPanel.tsx b/web/src/features/fba/components/infoPanel/TabPanel.tsx new file mode 100644 index 000000000..b44cacf3e --- /dev/null +++ b/web/src/features/fba/components/infoPanel/TabPanel.tsx @@ -0,0 +1,18 @@ +import { Box } from '@mui/material' +import React from 'react' + +interface TabPanelProps { + children?: React.ReactNode + index: number + value: number +} + +const TabPanel = ({ children, index, value }: TabPanelProps) => { + return ( + + ) +} + +export default TabPanel diff --git a/web/src/features/fba/components/viz/FuelSummary.tsx b/web/src/features/fba/components/viz/FuelSummary.tsx index dad06b1a5..409d63ab1 100644 --- a/web/src/features/fba/components/viz/FuelSummary.tsx +++ b/web/src/features/fba/components/viz/FuelSummary.tsx @@ -23,7 +23,7 @@ interface FuelSummaryProps { selectedFireZoneUnit: FireShape | undefined } -// Column definitions for fire zone unit fuel summary table +// Column definitions for fire zone unit fuel summary table const columns: GridColDef[] = [ { field: 'code', @@ -112,6 +112,7 @@ const FuelSummary = ({ fuelTypeInfo, selectedFireZoneUnit }: FuelSummaryProps) = showCellVerticalBorder showColumnVerticalBorder sx={{ + backgroundColor: 'white', maxHeight: '147px', minHeight: '100px', overflow: 'hidden', diff --git a/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx b/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx index 02cd03ec9..e63daa58e 100644 --- a/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx +++ b/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx @@ -31,9 +31,9 @@ import { fetchfireZoneTPIStats } from 'features/fba/slices/fireZoneTPIStatsSlice import { StyledFormControl } from 'components/StyledFormControl' import { getMostRecentProcessedSnowByDate } from 'api/snow' import InfoPanel from 'features/fba/components/infoPanel/InfoPanel' -import FireZoneUnitSummary from 'features/fba/components/infoPanel/FireZoneUnitSummary' import { fetchProvincialSummary } from 'features/fba/slices/provincialSummarySlice' import AdvisoryReport from 'features/fba/components/infoPanel/AdvisoryReport' +import FireZoneUnitTabs from 'features/fba/components/infoPanel/FireZoneUnitTabs' export enum RunType { FORECAST = 'FORECAST', @@ -133,7 +133,9 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { dispatch( fetchfireZoneElevationInfo(selectedFireShape.fire_shape_id, runType, doiISODate, mostRecentRunDate.toString()) ) - dispatch(fetchfireZoneTPIStats(selectedFireShape.fire_shape_id, runType, doiISODate, mostRecentRunDate.toString())) + dispatch( + fetchfireZoneTPIStats(selectedFireShape.fire_shape_id, runType, doiISODate, mostRecentRunDate.toString()) + ) } }, [mostRecentRunDate, selectedFireShape]) // eslint-disable-line react-hooks/exhaustive-deps @@ -163,7 +165,6 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { if (fireZoneTPIStats?.fire_zone_id === selectedFireShapeId) { setSelectedFireZoneTPIStats(fireZoneTPIStats) } - }, [fireZoneTPIStats]) useEffect(() => { @@ -236,10 +237,13 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { advisoryThreshold={advisoryThreshold} selectedFireCenter={fireCenter} /> - From 95d79c97093863887771443f6230a7744a6f1244 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Tue, 27 Aug 2024 10:28:44 -0700 Subject: [PATCH 02/42] box colour --- web/src/features/fba/components/infoPanel/TabPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/features/fba/components/infoPanel/TabPanel.tsx b/web/src/features/fba/components/infoPanel/TabPanel.tsx index b44cacf3e..a49259023 100644 --- a/web/src/features/fba/components/infoPanel/TabPanel.tsx +++ b/web/src/features/fba/components/infoPanel/TabPanel.tsx @@ -10,7 +10,7 @@ interface TabPanelProps { const TabPanel = ({ children, index, value }: TabPanelProps) => { return ( ) } From b2903c9ab4e69b301f4e703d2e90a632340b7100 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Tue, 27 Aug 2024 10:36:53 -0700 Subject: [PATCH 03/42] adds tooltip --- .../components/infoPanel/FireZoneUnitTabs.tsx | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx index 555798a51..133402176 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -1,4 +1,4 @@ -import { Box, Grid, Tab, Tabs } from '@mui/material' +import { Box, Grid, Tab, Tabs, Tooltip } from '@mui/material' import { FireCenter, FireShape, FireShapeAreaDetail, FireZoneThresholdFuelTypeArea, FireZoneTPIStats } from 'api/fbaAPI' import { INFO_PANEL_CONTENT_BACKGROUND, theme, TRANSPARENT_COLOUR } from 'app/theme' import FireZoneUnitSummary from 'features/fba/components/infoPanel/FireZoneUnitSummary' @@ -109,22 +109,24 @@ const FireZoneUnitTabs = ({ {sortedZoneNames.map((key, index) => { const isActive = tabNumber === index return ( - + + + ) })} From 7dad4f5a87f57ec04209e5502446c51c33e8bb77 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Tue, 27 Aug 2024 10:55:36 -0700 Subject: [PATCH 04/42] tooltip key --- web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx index 133402176..e6df9d22d 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -109,7 +109,7 @@ const FireZoneUnitTabs = ({ {sortedZoneNames.map((key, index) => { const isActive = tabNumber === index return ( - + Date: Tue, 27 Aug 2024 11:31:38 -0700 Subject: [PATCH 05/42] select first zone on load --- web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx index e6df9d22d..501bec996 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -67,7 +67,7 @@ const FireZoneUnitTabs = ({ setTabNumber(0) setSelectedFireShape(getTabFireShape(0)) } - }, [selectedFireZoneUnit, selectedFireCenter]) + }, [selectedFireZoneUnit, selectedFireCenter, provincialSummary]) const getTabFireShape = (tabNumber: number): FireShape | undefined => { if (sortedZoneNames.length > 0) { From 9ad5482a9595d247a045274dee7f9e475830c46a Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Tue, 27 Aug 2024 11:31:57 -0700 Subject: [PATCH 06/42] styled fuel headers --- .../fba/components/viz/FuelSummary.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/web/src/features/fba/components/viz/FuelSummary.tsx b/web/src/features/fba/components/viz/FuelSummary.tsx index 409d63ab1..ddc7785a2 100644 --- a/web/src/features/fba/components/viz/FuelSummary.tsx +++ b/web/src/features/fba/components/viz/FuelSummary.tsx @@ -4,8 +4,8 @@ import { Box, Tooltip, Typography } from '@mui/material' import { groupBy, isUndefined } from 'lodash' import { DateTime } from 'luxon' import FuelDistribution from 'features/fba/components/viz/FuelDistribution' -import { DataGridPro, GridColDef, GridRenderCellParams } from '@mui/x-data-grid-pro' -import { useTheme } from '@mui/material/styles' +import { DataGridPro, GridColDef, GridColumnHeaderParams, GridRenderCellParams } from '@mui/x-data-grid-pro' +import { styled, useTheme } from '@mui/material/styles' export interface FuelTypeInfoSummary { area: number @@ -23,14 +23,23 @@ interface FuelSummaryProps { selectedFireZoneUnit: FireShape | undefined } +const StyledHeader = styled('div')({ + whiteSpace: 'normal', + wordWrap: 'break-word', + textAlign: 'center', + fontSize: '0.75rem', + fontWeight: '700' +}) + // Column definitions for fire zone unit fuel summary table const columns: GridColDef[] = [ { field: 'code', headerClassName: 'fuel-summary-header', - headerName: 'Fuel Type', + headerName: 'Primary Fuels', sortable: false, - width: 75, + width: 120, + renderHeader: (params: GridColumnHeaderParams) => {params.colDef.headerName}, renderCell: (params: GridRenderCellParams) => ( {params.row[params.field]} @@ -41,9 +50,10 @@ const columns: GridColDef[] = [ field: 'area', flex: 3, headerClassName: 'fuel-summary-header', - headerName: 'Distribution > 4k kW/m', + headerName: 'Proportion of Advisory Area', minWidth: 200, sortable: false, + renderHeader: (params: GridColumnHeaderParams) => {params.colDef.headerName}, renderCell: (params: GridRenderCellParams) => { return } From af3ed515b4eab7cc9de5ca72b043561632f3089f Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Tue, 27 Aug 2024 11:55:35 -0700 Subject: [PATCH 07/42] fuel bar tooltip --- .../fba/components/viz/FuelDistribution.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/web/src/features/fba/components/viz/FuelDistribution.tsx b/web/src/features/fba/components/viz/FuelDistribution.tsx index 84b67e26b..1871d4acb 100644 --- a/web/src/features/fba/components/viz/FuelDistribution.tsx +++ b/web/src/features/fba/components/viz/FuelDistribution.tsx @@ -1,6 +1,7 @@ -import { Box } from '@mui/material' +import { Box, Tooltip } from '@mui/material' import React from 'react' import { getColorByFuelTypeCode } from 'features/fba/components/viz/color' +import { theme } from 'app/theme' interface FuelDistributionProps { code: string @@ -10,10 +11,12 @@ interface FuelDistributionProps { // Represents the percent contribution of the given fuel type to the overall high HFI area. const FuelDistribution = ({ code, percent }: FuelDistributionProps) => { return ( - + + + ) } From d3bd58d1ff47bb7e0ab3dcddf20c254eeadf8899 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Tue, 27 Aug 2024 12:24:34 -0700 Subject: [PATCH 08/42] thicker indicator --- .../fba/components/infoPanel/FireZoneUnitTabs.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx index 501bec996..2f6e37ca6 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -105,7 +105,15 @@ const FireZoneUnitTabs = ({ - + {sortedZoneNames.map((key, index) => { const isActive = tabNumber === index return ( @@ -118,7 +126,7 @@ const FireZoneUnitTabs = ({ borderTopLeftRadius: '4px', borderTopRightRadius: '4px', border: '1px solid grey', - marginRight: '4px', + marginRight: theme.spacing(0.5), marginTop: theme.spacing(2), fontWeight: 'bold', color: isActive ? 'black' : 'grey', From dcf920017c8668ddcc549d6ef57084d912dee982 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Fri, 30 Aug 2024 11:29:47 -0700 Subject: [PATCH 09/42] Remove elevation call --- web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx b/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx index e63daa58e..13d432301 100644 --- a/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx +++ b/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx @@ -26,7 +26,6 @@ import { fetchSFMSRunDates } from 'features/fba/slices/runDatesSlice' import { isNull, isUndefined } from 'lodash' import { fetchHighHFIFuels } from 'features/fba/slices/hfiFuelTypesSlice' import { fetchFireShapeAreas } from 'features/fba/slices/fireZoneAreasSlice' -import { fetchfireZoneElevationInfo } from 'features/fba/slices/fireZoneElevationInfoSlice' import { fetchfireZoneTPIStats } from 'features/fba/slices/fireZoneTPIStatsSlice' import { StyledFormControl } from 'components/StyledFormControl' import { getMostRecentProcessedSnowByDate } from 'api/snow' @@ -130,9 +129,6 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { !isUndefined(selectedFireShape) ) { dispatch(fetchHighHFIFuels(runType, doiISODate, mostRecentRunDate.toString(), selectedFireShape.fire_shape_id)) - dispatch( - fetchfireZoneElevationInfo(selectedFireShape.fire_shape_id, runType, doiISODate, mostRecentRunDate.toString()) - ) dispatch( fetchfireZoneTPIStats(selectedFireShape.fire_shape_id, runType, doiISODate, mostRecentRunDate.toString()) ) From b62cca36a74c0c14b26095e4d2432c11818f39f6 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Tue, 3 Sep 2024 13:52:20 -0700 Subject: [PATCH 10/42] radio button console warning --- web/src/features/fba/components/ActualForecastControl.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/features/fba/components/ActualForecastControl.tsx b/web/src/features/fba/components/ActualForecastControl.tsx index ce86cf78e..d59b99fe9 100644 --- a/web/src/features/fba/components/ActualForecastControl.tsx +++ b/web/src/features/fba/components/ActualForecastControl.tsx @@ -9,9 +9,9 @@ export interface ActualForecastControlProps { setRunType: React.Dispatch> } const ActualForecastControl = ({ runType, setRunType }: ActualForecastControlProps) => { - const changeHandler = (_: React.ChangeEvent<{}>, value: any | null) => { + const changeHandler = (_: React.ChangeEvent<{}>, value: string | null) => { if (!isNull(value)) { - setRunType(value) + setRunType(value as RunType) } } return ( @@ -38,7 +38,7 @@ const ActualForecastControl = ({ runType, setRunType }: ActualForecastControlPro > Time Frame - + Date: Tue, 3 Sep 2024 16:24:41 -0700 Subject: [PATCH 11/42] TPI centre endpoint & tab zooming --- api/app/db/crud/auto_spatial_advisory.py | 21 ++++++ api/app/routers/fba.py | 24 +++++++ web/src/api/fbaAPI.ts | 11 ++++ web/src/app/rootReducer.ts | 4 +- .../infoPanel/FireZoneUnitSummary.tsx | 4 +- .../components/infoPanel/FireZoneUnitTabs.tsx | 66 ++++++++++++------- .../features/fba/components/map/FBAMap.tsx | 20 ++++-- .../fba/pages/FireBehaviourAdvisoryPage.tsx | 55 ++++++++++------ .../fba/slices/fireCentreTPIStatsSlice.ts | 57 ++++++++++++++++ 9 files changed, 211 insertions(+), 51 deletions(-) create mode 100644 web/src/features/fba/slices/fireCentreTPIStatsSlice.ts diff --git a/api/app/db/crud/auto_spatial_advisory.py b/api/app/db/crud/auto_spatial_advisory.py index 0fe86e46b..30e09899a 100644 --- a/api/app/db/crud/auto_spatial_advisory.py +++ b/api/app/db/crud/auto_spatial_advisory.py @@ -366,6 +366,27 @@ async def get_zonal_tpi_stats(session: AsyncSession, fire_zone_id: int, run_type return result.first() +async def get_centre_tpi_stats(session: AsyncSession, fire_centre_name: str, run_type: RunType, run_datetime: datetime, for_date: date) -> AdvisoryTPIStats: + run_parameters_id = await get_run_parameters_id(session, run_type, run_datetime, for_date) + + stmt = ( + select( + AdvisoryTPIStats.advisory_shape_id, + Shape.source_identifier, + AdvisoryTPIStats.valley_bottom, + AdvisoryTPIStats.mid_slope, + AdvisoryTPIStats.upper_slope, + AdvisoryTPIStats.pixel_size_metres, + ) + .join(Shape, Shape.id == AdvisoryTPIStats.advisory_shape_id) + .join(FireCentre, FireCentre.id == Shape.fire_centre) + .where(FireCentre.name == fire_centre_name, AdvisoryTPIStats.run_parameters == run_parameters_id) + ) + + result = await session.execute(stmt) + return result.all() + + async def get_provincial_rollup(session: AsyncSession, run_type: RunTypeEnum, run_datetime: datetime, for_date: date) -> List[Row]: logger.info("gathering provincial rollup") run_parameter_id = await get_run_parameters_id(session, run_type, run_datetime, for_date) diff --git a/api/app/routers/fba.py b/api/app/routers/fba.py index 7af041a04..8f3b1ff4a 100644 --- a/api/app/routers/fba.py +++ b/api/app/routers/fba.py @@ -16,6 +16,7 @@ get_run_datetimes, get_zonal_elevation_stats, get_zonal_tpi_stats, + get_centre_tpi_stats, ) from app.db.models.auto_spatial_advisory import RunTypeEnum from app.schemas.fba import ( @@ -182,3 +183,26 @@ async def get_fire_zone_tpi_stats(fire_zone_id: int, run_type: RunType, run_date mid_slope=stats.mid_slope * square_metres, upper_slope=stats.upper_slope * square_metres, ) + + +@router.get("/fire-centre-tpi-stats/{run_type}/{for_date}/{run_datetime}/{fire_centre_name}", response_model=dict[str, List[FireZoneTPIStats]]) +async def get_fire_centre_tpi_stats(fire_centre_name: str, run_type: RunType, run_datetime: datetime, for_date: date, _=Depends(authentication_required)): + """Return the elevation TPI statistics for each advisory threshold for a fire centre""" + logger.info("/fba/fire-centre-tpi-stats/") + async with get_async_read_session_scope() as session: + tpi_stats_for_centre = await get_centre_tpi_stats(session, fire_centre_name, run_type, run_datetime, for_date) + + data = [] + for row in tpi_stats_for_centre: + square_metres = math.pow(row.pixel_size_metres, 2) + + data.append( + FireZoneTPIStats( + fire_zone_id=row.source_identifier, + valley_bottom=row.valley_bottom * square_metres, + mid_slope=row.mid_slope * square_metres, + upper_slope=row.upper_slope * square_metres, + ) + ) + + return {fire_centre_name: data} diff --git a/web/src/api/fbaAPI.ts b/web/src/api/fbaAPI.ts index 45dd54f33..72ba3bf31 100644 --- a/web/src/api/fbaAPI.ts +++ b/web/src/api/fbaAPI.ts @@ -169,6 +169,17 @@ export async function getFireZoneTPIStats( return data } +export async function getFireCentreTPIStats( + fire_centre_name: string, + run_type: RunType, + run_datetime: string, + for_date: string +): Promise> { + const url = `fba/fire-centre-tpi-stats/${run_type.toLowerCase()}/${run_datetime}/${for_date}/${fire_centre_name}` + const { data } = await axios.get(url) + return data +} + export async function getValueAtCoordinate( layer: string, latitude: number, diff --git a/web/src/app/rootReducer.ts b/web/src/app/rootReducer.ts index cc5fc8fdd..bc4a8cc3c 100644 --- a/web/src/app/rootReducer.ts +++ b/web/src/app/rootReducer.ts @@ -21,6 +21,7 @@ import selectedStationGroupsMembersSlice from 'commonSlices/selectedStationGroup import dataSlice from 'features/moreCast2/slices/dataSlice' import selectedStationsSlice from 'features/moreCast2/slices/selectedStationsSlice' import provincialSummarySlice from 'features/fba/slices/provincialSummarySlice' +import fireCentreTPIStatsSlice from 'features/fba/slices/fireCentreTPIStatsSlice' const rootReducer = combineReducers({ percentileStations: stationReducer, @@ -40,6 +41,7 @@ const rootReducer = combineReducers({ hfiFuelTypes: hfiFuelTypesSlice, fireZoneElevationInfo: fireZoneElevationInfoSlice, fireZoneTPIStats: fireZoneTPIStatsSlice, + fireCentreTPIStats: fireCentreTPIStatsSlice, stationGroups: stationGroupsSlice, stationGroupsMembers: selectedStationGroupsMembersSlice, weatherIndeterminates: dataSlice, @@ -70,7 +72,7 @@ export const selectValueAtCoordinate = (state: RootState) => state.valueAtCoordi export const selectHFIFuelTypes = (state: RootState) => state.hfiFuelTypes export const selectFireZoneElevationInfo = (state: RootState) => state.fireZoneElevationInfo export const selectFireZoneTPIStats = (state: RootState) => state.fireZoneTPIStats - +export const selectFireCentreTPIStats = (state: RootState) => state.fireCentreTPIStats export const selectHFIDailiesLoading = (state: RootState): boolean => state.hfiCalculatorDailies.fireCentresLoading export const selectHFICalculatorState = (state: RootState): HFICalculatorState => state.hfiCalculatorDailies export const selectHFIStationsLoading = (state: RootState): boolean => state.hfiStations.loading diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx index e8b7cb5e9..0ee761a3c 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx @@ -9,7 +9,7 @@ import FuelSummary from 'features/fba/components/viz/FuelSummary' interface FireZoneUnitSummaryProps { selectedFireZoneUnit: FireShape | undefined fuelTypeInfo: Record - fireZoneTPIStats: FireZoneTPIStats | null + fireZoneTPIStats: FireZoneTPIStats | undefined } const FireZoneUnitSummary = ({ fuelTypeInfo, fireZoneTPIStats, selectedFireZoneUnit }: FireZoneUnitSummaryProps) => { @@ -30,7 +30,7 @@ const FireZoneUnitSummary = ({ fuelTypeInfo, fireZoneTPIStats, selectedFireZoneU - {isNull(fireZoneTPIStats) || + {isUndefined(fireZoneTPIStats) || fireZoneTPIStats.valley_bottom + fireZoneTPIStats.mid_slope + fireZoneTPIStats.upper_slope === 0 ? ( No elevation information available. ) : ( diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx index 2f6e37ca6..fa4054c14 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -13,14 +13,15 @@ import { useSelector } from 'react-redux' interface FireZoneUnitTabs { selectedFireZoneUnit: FireShape | undefined fuelTypeInfo: Record - fireZoneTPIStats: FireZoneTPIStats | null + setZoomSource: React.Dispatch> + fireCentreTPIStats: Record | null selectedFireCenter: FireCenter | undefined advisoryThreshold: number setSelectedFireShape: React.Dispatch> } const calculateStatus = (details: FireShapeAreaDetail[], advisoryThreshold: number) => { - let status = 'white' + let status = '#DCDCDC' if (details.length === 0) { return status @@ -44,22 +45,37 @@ const calculateStatus = (details: FireShapeAreaDetail[], advisoryThreshold: numb const FireZoneUnitTabs = ({ fuelTypeInfo, - fireZoneTPIStats, selectedFireZoneUnit, + setZoomSource, selectedFireCenter, advisoryThreshold, + fireCentreTPIStats, setSelectedFireShape }: FireZoneUnitTabs) => { const provincialSummary = useSelector(selectProvincialSummary) const [tabNumber, setTabNumber] = useState(0) const fireCenterSummary = selectedFireCenter ? provincialSummary[selectedFireCenter.name] : [] - const groupedFireZoneUnitInfos = useMemo(() => groupBy(fireCenterSummary, 'fire_shape_name'), [fireCenterSummary]) - const sortedZoneNames = useMemo(() => Object.keys(groupedFireZoneUnitInfos).sort(), [groupedFireZoneUnitInfos]) + + const groupedFireZoneUnits = useMemo(() => groupBy(fireCenterSummary, 'fire_shape_id'), [fireCenterSummary]) + const sortedGroupedFireZoneUnits = useMemo( + () => + Object.values(groupedFireZoneUnits) + .map(group => ({ + fire_shape_id: group[0].fire_shape_id, + fire_shape_name: group[0].fire_shape_name, + fire_centre_name: group[0].fire_centre_name, + fireShapeDetails: group + })) + .sort((a, b) => a.fire_shape_name.localeCompare(b.fire_shape_name)), + [groupedFireZoneUnits] + ) useEffect(() => { if (selectedFireZoneUnit) { - const newIndex = sortedZoneNames.indexOf(selectedFireZoneUnit.mof_fire_zone_name) + const newIndex = sortedGroupedFireZoneUnits.findIndex( + zone => zone.fire_shape_id === selectedFireZoneUnit.fire_shape_id + ) if (newIndex !== -1) { setTabNumber(newIndex) } @@ -67,17 +83,16 @@ const FireZoneUnitTabs = ({ setTabNumber(0) setSelectedFireShape(getTabFireShape(0)) } - }, [selectedFireZoneUnit, selectedFireCenter, provincialSummary]) + }, [selectedFireZoneUnit, sortedGroupedFireZoneUnits]) const getTabFireShape = (tabNumber: number): FireShape | undefined => { - if (sortedZoneNames.length > 0) { - const selectedTabZone = sortedZoneNames[tabNumber] - const selectedFireShapeInfo = groupedFireZoneUnitInfos[selectedTabZone][0] + if (sortedGroupedFireZoneUnits.length > 0) { + const selectedTabZone = sortedGroupedFireZoneUnits[tabNumber] const fireShape: FireShape = { - fire_shape_id: selectedFireShapeInfo.fire_shape_id, - mof_fire_centre_name: selectedFireShapeInfo.fire_centre_name, - mof_fire_zone_name: selectedFireShapeInfo.fire_shape_name + fire_shape_id: selectedTabZone.fire_shape_id, + mof_fire_centre_name: selectedTabZone.fire_centre_name, + mof_fire_zone_name: selectedTabZone.fire_shape_name } return fireShape @@ -89,12 +104,15 @@ const FireZoneUnitTabs = ({ const fireShape = getTabFireShape(newValue) setSelectedFireShape(fireShape) + setZoomSource('fireShape') } if (isUndefined(selectedFireCenter) || isNull(selectedFireCenter)) { return
} + const tpiStatsArray = fireCentreTPIStats?.[selectedFireCenter.name] + return (
- {sortedZoneNames.map((key, index) => { + {sortedGroupedFireZoneUnits.map((zone, index) => { const isActive = tabNumber === index + const key = zone.fire_shape_id return ( - + ) })} - {sortedZoneNames.map((key, index) => ( - + {sortedGroupedFireZoneUnits.map((zone, index) => ( + stats.fire_zone_id == zone.fire_shape_id) : undefined + } selectedFireZoneUnit={selectedFireZoneUnit} /> diff --git a/web/src/features/fba/components/map/FBAMap.tsx b/web/src/features/fba/components/map/FBAMap.tsx index 7ab6c20ff..5c20e18e1 100644 --- a/web/src/features/fba/components/map/FBAMap.tsx +++ b/web/src/features/fba/components/map/FBAMap.tsx @@ -170,10 +170,7 @@ const FBAMap = (props: FBAMapProps) => { if (!feature) { return } - const zoneExtent = fireZoneExtentsMap.get(feature.getProperties().OBJECTID.toString()) - if (!isUndefined(zoneExtent)) { - map.getView().fit(zoneExtent, { duration: 400, padding: [100, 100, 100, 100], maxZoom: 8 }) - } + const fireZone: FireShape = { fire_shape_id: feature.getProperties().OBJECTID, mof_fire_zone_name: feature.getProperties().FIRE_ZONE, @@ -188,6 +185,7 @@ const FBAMap = (props: FBAMapProps) => { }, [map]) // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { + // zoom to fire center or whole province if (!map) return if (props.selectedFireCenter && props.zoomSource === 'fireCenter') { @@ -199,7 +197,19 @@ const FBAMap = (props: FBAMapProps) => { // reset map view to full province map.getView().fit(bcExtent, { duration: 600, padding: [50, 50, 50, 50] }) } - }, [props.selectedFireCenter]) // eslint-disable-line react-hooks/exhaustive-deps + }, [props.selectedFireCenter]) + + useEffect(() => { + // zoom to fire zone + if (!map) return + + if (props.selectedFireShape && props.zoomSource === 'fireShape') { + const zoneExtent = fireZoneExtentsMap.get(props.selectedFireShape.fire_shape_id.toString()) + if (!isUndefined(zoneExtent)) { + map.getView().fit(zoneExtent, { duration: 400, padding: [100, 100, 100, 100], maxZoom: 8 }) + } + } + }, [props.selectedFireShape]) useEffect(() => { if (!map) return diff --git a/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx b/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx index 13d432301..182548eaf 100644 --- a/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx +++ b/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx @@ -9,7 +9,8 @@ import { selectFireCenters, selectHFIFuelTypes, selectRunDates, - selectFireShapeAreas + selectFireShapeAreas, + selectFireCentreTPIStats } from 'app/rootReducer' import { useDispatch, useSelector } from 'react-redux' import { fetchFireCenters } from 'commonSlices/fireCentersSlice' @@ -33,6 +34,7 @@ import InfoPanel from 'features/fba/components/infoPanel/InfoPanel' import { fetchProvincialSummary } from 'features/fba/slices/provincialSummarySlice' import AdvisoryReport from 'features/fba/components/infoPanel/AdvisoryReport' import FireZoneUnitTabs from 'features/fba/components/infoPanel/FireZoneUnitTabs' +import { fetchFireCentreTPIStats } from 'features/fba/slices/fireCentreTPIStatsSlice' export enum RunType { FORECAST = 'FORECAST', @@ -49,6 +51,7 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { const { fireCenters } = useSelector(selectFireCenters) const { hfiThresholdsFuelTypes } = useSelector(selectHFIFuelTypes) const { fireZoneTPIStats } = useSelector(selectFireZoneTPIStats) + const { fireCentreTPIStats } = useSelector(selectFireCentreTPIStats) const [fireCenter, setFireCenter] = useState(undefined) @@ -65,6 +68,10 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { const { mostRecentRunDate } = useSelector(selectRunDates) const { fireShapeAreas } = useSelector(selectFireShapeAreas) const [selectedFireZoneTPIStats, setSelectedFireZoneTPIStats] = useState(null) + const [selectedFireCentreTPIStats, setSelectedFireCentreTPIStats] = useState | null>(null) // Query our API for the most recently processed snow coverage date <= the currently selected date. const fetchLastProcessedSnow = async (selectedDate: DateTime) => { @@ -90,6 +97,16 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { } }, [fireCenter]) + useEffect(() => { + if (selectedFireShape?.mof_fire_centre_name) { + const matchingFireCenter = fireCenters.find(center => center.name === selectedFireShape.mof_fire_centre_name) + + if (matchingFireCenter) { + setFireCenter(matchingFireCenter) + } + } + }, [selectedFireShape, fireCenters]) + const updateDate = (newDate: DateTime) => { if (newDate !== dateOfInterest) { setDateOfInterest(newDate) @@ -135,6 +152,19 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { } }, [mostRecentRunDate, selectedFireShape]) // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { + const doiISODate = dateOfInterest.toISODate() + if ( + !isNull(mostRecentRunDate) && + !isNull(doiISODate) && + !isUndefined(mostRecentRunDate) && + !isUndefined(fireCenter) && + !isNull(fireCenter) + ) { + dispatch(fetchFireCentreTPIStats(fireCenter.name, runType, doiISODate, mostRecentRunDate.toString())) + } + }, [fireCenter, mostRecentRunDate]) + useEffect(() => { const doiISODate = dateOfInterest.toISODate() if (!isNull(doiISODate)) { @@ -143,16 +173,6 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { } }, [mostRecentRunDate]) // eslint-disable-line react-hooks/exhaustive-deps - useEffect(() => { - if (selectedFireShape?.mof_fire_centre_name) { - const matchingFireCenter = fireCenters.find(center => center.name === selectedFireShape.mof_fire_centre_name) - - if (matchingFireCenter) { - setFireCenter(matchingFireCenter) - } - } - }, [selectedFireShape, fireCenters]) - useEffect(() => { const selectedFireShapeId = selectedFireShape?.fire_shape_id if (isNull(fireZoneTPIStats) || isUndefined(selectedFireShapeId)) { @@ -164,14 +184,10 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { }, [fireZoneTPIStats]) useEffect(() => { - if (selectedFireShape?.mof_fire_centre_name) { - const matchingFireCenter = fireCenters.find(center => center.name === selectedFireShape.mof_fire_centre_name) - - if (matchingFireCenter) { - setFireCenter(matchingFireCenter) - } + if (fireCentreTPIStats && Object.keys(fireCentreTPIStats)[0] === fireCenter?.name) { + setSelectedFireCentreTPIStats(fireCentreTPIStats) } - }, [selectedFireShape, fireCenters]) + }, [fireCentreTPIStats]) useEffect(() => { document.title = ASA_DOC_TITLE @@ -236,7 +252,8 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { | null +} + +const initialState: State = { + loading: false, + error: null, + fireCentreTPIStats: null +} + +const fireCentreTPIStatsSlice = createSlice({ + name: 'fireCentreTPIStats', + initialState, + reducers: { + getFireCentreTPIStatsStart(state: State) { + state.error = null + state.fireCentreTPIStats = null + state.loading = true + }, + getFireCentreTPIStatsFailed(state: State, action: PayloadAction) { + state.error = action.payload + state.loading = false + }, + getFireCentreTPIStatsSuccess(state: State, action: PayloadAction>) { + state.error = null + state.fireCentreTPIStats = action.payload + state.loading = false + } + } +}) + +export const { getFireCentreTPIStatsStart, getFireCentreTPIStatsFailed, getFireCentreTPIStatsSuccess } = + fireCentreTPIStatsSlice.actions + +export default fireCentreTPIStatsSlice.reducer + +export const fetchFireCentreTPIStats = + (fire_centre_name: string, runType: RunType, forDate: string, runDatetime: string): AppThunk => + async dispatch => { + try { + dispatch(getFireCentreTPIStatsStart()) + const fireCentreTPIStats = await getFireCentreTPIStats(fire_centre_name, runType, forDate, runDatetime) + dispatch(getFireCentreTPIStatsSuccess(fireCentreTPIStats)) + } catch (err) { + dispatch(getFireCentreTPIStatsFailed((err as Error).toString())) + logError(err) + } + } From 013343e54be0207c48f6ba13235ff76f9e43f562 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Tue, 3 Sep 2024 17:06:16 -0700 Subject: [PATCH 12/42] radio test fix --- web/src/features/fba/components/ActualForecastControl.tsx | 2 +- .../fba/components/actualForecastControl.test.tsx | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/web/src/features/fba/components/ActualForecastControl.tsx b/web/src/features/fba/components/ActualForecastControl.tsx index d59b99fe9..0f039dc17 100644 --- a/web/src/features/fba/components/ActualForecastControl.tsx +++ b/web/src/features/fba/components/ActualForecastControl.tsx @@ -9,7 +9,7 @@ export interface ActualForecastControlProps { setRunType: React.Dispatch> } const ActualForecastControl = ({ runType, setRunType }: ActualForecastControlProps) => { - const changeHandler = (_: React.ChangeEvent<{}>, value: string | null) => { + const changeHandler = (_: React.ChangeEvent<{}>, value: string) => { if (!isNull(value)) { setRunType(value as RunType) } diff --git a/web/src/features/fba/components/actualForecastControl.test.tsx b/web/src/features/fba/components/actualForecastControl.test.tsx index bb8c69947..3427bf396 100644 --- a/web/src/features/fba/components/actualForecastControl.test.tsx +++ b/web/src/features/fba/components/actualForecastControl.test.tsx @@ -13,10 +13,16 @@ describe('ActualForecastControl', () => { }) it('should call setRunType with the correct value when a radio button is selected', () => { - const { getByTestId } = render() + const { getByTestId, rerender } = render( + + ) fireEvent.click(getByTestId('actual-radio')) expect(mockSetRunType).toHaveBeenCalledWith(RunType.ACTUAL) + expect(mockSetRunType).toHaveBeenCalledTimes(1) + + rerender() fireEvent.click(getByTestId('forecast-radio')) + expect(mockSetRunType).toHaveBeenCalledTimes(2) expect(mockSetRunType).toHaveBeenCalledWith(RunType.FORECAST) }) }) From 4c220d3a5616634aeb81793d3983f8882981a4ed Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Wed, 4 Sep 2024 08:26:54 -0700 Subject: [PATCH 13/42] nullish op --- .../features/fba/components/infoPanel/FireZoneUnitSummary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx index 0ee761a3c..6fe44df3e 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx @@ -30,7 +30,7 @@ const FireZoneUnitSummary = ({ fuelTypeInfo, fireZoneTPIStats, selectedFireZoneU - {isUndefined(fireZoneTPIStats) || + {!fireZoneTPIStats || fireZoneTPIStats.valley_bottom + fireZoneTPIStats.mid_slope + fireZoneTPIStats.upper_slope === 0 ? ( No elevation information available. ) : ( From 236f714c8a679f1497048f1d2496513d69e7daf3 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Wed, 4 Sep 2024 08:46:42 -0700 Subject: [PATCH 14/42] merge fix --- web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx b/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx index d0b829871..fa777015d 100644 --- a/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx +++ b/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx @@ -244,7 +244,7 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { setZoomSource={setZoomSource} fireCentreTPIStats={selectedFireCentreTPIStats} selectedFireCenter={fireCenter} - advisoryThreshold={advisoryThreshold} + advisoryThreshold={ADVISORY_THRESHOLD} setSelectedFireShape={setSelectedFireShape} /> From 38196e93fb95bce1a6a922d1517bdcf2f1fe3521 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Wed, 4 Sep 2024 08:54:27 -0700 Subject: [PATCH 15/42] smells --- .../infoPanel/FireZoneUnitSummary.tsx | 2 +- .../components/infoPanel/FireZoneUnitTabs.tsx | 2 +- .../infoPanel/fireZoneUnitSummary.test.tsx | 28 ++++--------------- .../fba/components/viz/FuelDistribution.tsx | 1 - 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx index 6fe44df3e..21d037580 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx @@ -1,6 +1,6 @@ import React from 'react' import { Grid, Typography } from '@mui/material' -import { isNull, isUndefined } from 'lodash' +import { isUndefined } from 'lodash' import { FireShape, FireZoneTPIStats, FireZoneThresholdFuelTypeArea } from 'api/fbaAPI' import ElevationStatus from 'features/fba/components/viz/ElevationStatus' import { useTheme } from '@mui/material/styles' diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx index fa4054c14..2cdb763c9 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -1,6 +1,6 @@ import { Box, Grid, Tab, Tabs, Tooltip } from '@mui/material' import { FireCenter, FireShape, FireShapeAreaDetail, FireZoneThresholdFuelTypeArea, FireZoneTPIStats } from 'api/fbaAPI' -import { INFO_PANEL_CONTENT_BACKGROUND, theme, TRANSPARENT_COLOUR } from 'app/theme' +import { INFO_PANEL_CONTENT_BACKGROUND, theme } from 'app/theme' import FireZoneUnitSummary from 'features/fba/components/infoPanel/FireZoneUnitSummary' import InfoAccordion from 'features/fba/components/infoPanel/InfoAccordion' import TabPanel from 'features/fba/components/infoPanel/TabPanel' diff --git a/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx b/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx index 0bca2c74e..b23c5f395 100644 --- a/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx +++ b/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx @@ -4,7 +4,7 @@ import { FireShape } from 'api/fbaAPI' import { render } from '@testing-library/react' const fireZoneTPIStats = { - fire_zone_id: 0, + fire_zone_id: 0, valley_bottom: 0, mid_slope: 100, upper_slope: 0 @@ -25,11 +25,7 @@ describe('FireZoneUnitSummary', () => { window.ResizeObserver = ResizeObserver it('should not render empty div if selectedFireZoneUnit is undefined', () => { const { getByTestId } = render( - + ) const fireZoneUnitInfo = getByTestId('fire-zone-unit-summary-empty') expect(fireZoneUnitInfo).toBeInTheDocument() @@ -42,11 +38,7 @@ describe('FireZoneUnitSummary', () => { area_sqm: 10 } const { getByTestId } = render( - + ) const fireZoneUnitInfo = getByTestId('fire-zone-unit-summary') expect(fireZoneUnitInfo).toBeInTheDocument() @@ -59,11 +51,7 @@ describe('FireZoneUnitSummary', () => { area_sqm: 10 } const { queryByTestId } = render( - + ) const fireZoneUnitInfo = queryByTestId('elevation-status') expect(fireZoneUnitInfo).not.toBeInTheDocument() @@ -76,11 +64,7 @@ describe('FireZoneUnitSummary', () => { area_sqm: 10 } const { getByTestId } = render( - + ) const fireZoneUnitInfo = getByTestId('elevation-status') expect(fireZoneUnitInfo).toBeInTheDocument() @@ -97,7 +81,7 @@ describe('FireZoneUnitSummary', () => { Date: Thu, 5 Sep 2024 08:33:41 -0700 Subject: [PATCH 16/42] hfiFuels fire centre endpoint & frontend --- api/app/db/crud/auto_spatial_advisory.py | 11 ++++ api/app/routers/fba.py | 44 ++++++++++++++ web/src/api/fbaAPI.ts | 17 ++++++ web/src/app/rootReducer.ts | 3 + .../components/infoPanel/FireZoneUnitTabs.tsx | 9 +-- .../fba/pages/FireBehaviourAdvisoryPage.tsx | 54 +++--------------- .../fba/slices/fireCentreHfiFuelTypesSlice.ts | 57 +++++++++++++++++++ .../fba/slices/fireCentreTPIStatsSlice.ts | 4 +- .../features/fba/slices/hfiFuelTypesSlice.ts | 2 +- 9 files changed, 148 insertions(+), 53 deletions(-) create mode 100644 web/src/features/fba/slices/fireCentreHfiFuelTypesSlice.ts diff --git a/api/app/db/crud/auto_spatial_advisory.py b/api/app/db/crud/auto_spatial_advisory.py index 30e09899a..43f5e6f5e 100644 --- a/api/app/db/crud/auto_spatial_advisory.py +++ b/api/app/db/crud/auto_spatial_advisory.py @@ -130,6 +130,17 @@ async def get_all_sfms_fuel_types(session: AsyncSession) -> List[SFMSFuelType]: return fuel_types +async def get_zone_ids_in_centre(session: AsyncSession, fire_centre_name: str): + logger.info(f"retrieving fire zones within {fire_centre_name} from advisory_shapes table") + + stmt = select(Shape.source_identifier).join(FireCentre, FireCentre.id == Shape.fire_centre).where(FireCentre.name == fire_centre_name) + result = await session.execute(stmt) + + all_results = result.scalars().all() + + return all_results + + async def get_precomputed_high_hfi_fuel_type_areas_for_shape(session: AsyncSession, run_type: RunTypeEnum, run_datetime: datetime, for_date: date, advisory_shape_id: int) -> List[Row]: perf_start = perf_counter() stmt = ( diff --git a/api/app/routers/fba.py b/api/app/routers/fba.py index 8f3b1ff4a..fb232cb68 100644 --- a/api/app/routers/fba.py +++ b/api/app/routers/fba.py @@ -17,6 +17,7 @@ get_zonal_elevation_stats, get_zonal_tpi_stats, get_centre_tpi_stats, + get_zone_ids_in_centre, ) from app.db.models.auto_spatial_advisory import RunTypeEnum from app.schemas.fba import ( @@ -142,6 +143,49 @@ async def get_hfi_fuels_data_for_fire_zone(run_type: RunType, for_date: date, ru return {zone_id: data} +@router.get("/fire-centre-hfi-fuels/{run_type}/{for_date}/{run_datetime}/{fire_centre_name}", response_model=dict[str, dict[int, List[ClassifiedHfiThresholdFuelTypeArea]]]) +async def get_hfi_fuels_data_for_fire_centre(run_type: RunType, for_date: date, run_datetime: datetime, fire_centre_name: str): + """ + Fetch rollup of fuel type/HFI threshold/area data for a specified fire zone. + """ + logger.info("fire-centre-hfi-fuels/%s/%s/%s/%s", run_type.value, for_date, run_datetime, fire_centre_name) + + async with get_async_read_session_scope() as session: + # get thresholds data + thresholds = await get_all_hfi_thresholds(session) + # get fuel type ids data + fuel_types = await get_all_sfms_fuel_types(session) + # get fire zone id's within a fire centre + zone_ids = await get_zone_ids_in_centre(session, fire_centre_name) + + all_zone_data = {} + for zone_id in zone_ids: + # get HFI/fuels data for specific zone + hfi_fuel_type_ids_for_zone = await get_precomputed_high_hfi_fuel_type_areas_for_shape( + session, run_type=RunTypeEnum(run_type.value), for_date=for_date, run_datetime=run_datetime, advisory_shape_id=zone_id + ) + zone_data = [] + + for record in hfi_fuel_type_ids_for_zone: + fuel_type_id = record[1] + threshold_id = record[2] + # area is stored in square metres in DB. For user convenience, convert to hectares + # 1 ha = 10,000 sq.m. + area = record[3] / 10000 + fuel_type_obj = next((ft for ft in fuel_types if ft.fuel_type_id == fuel_type_id), None) + threshold_obj = next((th for th in thresholds if th.id == threshold_id), None) + zone_data.append( + ClassifiedHfiThresholdFuelTypeArea( + fuel_type=SFMSFuelType(fuel_type_id=fuel_type_obj.fuel_type_id, fuel_type_code=fuel_type_obj.fuel_type_code, description=fuel_type_obj.description), + threshold=HfiThreshold(id=threshold_obj.id, name=threshold_obj.name, description=threshold_obj.description), + area=area, + ) + ) + all_zone_data[zone_id] = zone_data + + return {fire_centre_name: all_zone_data} + + @router.get("/sfms-run-datetimes/{run_type}/{for_date}", response_model=List[datetime]) async def get_run_datetimes_for_date_and_runtype(run_type: RunType, for_date: date, _=Depends(authentication_required)): """Return list of datetimes for which SFMS has run, given a specific for_date and run_type. diff --git a/web/src/api/fbaAPI.ts b/web/src/api/fbaAPI.ts index 72ba3bf31..461f6137b 100644 --- a/web/src/api/fbaAPI.ts +++ b/web/src/api/fbaAPI.ts @@ -96,6 +96,12 @@ export interface FuelType { description: string } +export interface FireCentreHfiFuelsData { + [fire_centre_name: string]: { + [fire_zone_id: number]: FireZoneThresholdFuelTypeArea[] + } +} + export async function getFBAFireCenters(): Promise { const url = '/fba/fire-centers' @@ -147,6 +153,17 @@ export async function getHFIThresholdsFuelTypesForZone( return data } +export async function getHFIThresholdsFuelTypesForCentre( + run_type: RunType, + for_date: string, + run_datetime: string, + fire_centre: string +): Promise { + const url = `fba/fire-centre-hfi-fuels/${run_type.toLowerCase()}/${for_date}/${run_datetime}/${fire_centre}` + const { data } = await axios.get(url) + return data +} + export async function getFireZoneElevationInfo( fire_zone_id: number, run_type: RunType, diff --git a/web/src/app/rootReducer.ts b/web/src/app/rootReducer.ts index bc4a8cc3c..f2a58fe82 100644 --- a/web/src/app/rootReducer.ts +++ b/web/src/app/rootReducer.ts @@ -22,6 +22,7 @@ import dataSlice from 'features/moreCast2/slices/dataSlice' import selectedStationsSlice from 'features/moreCast2/slices/selectedStationsSlice' import provincialSummarySlice from 'features/fba/slices/provincialSummarySlice' import fireCentreTPIStatsSlice from 'features/fba/slices/fireCentreTPIStatsSlice' +import fireCentreHfiFuelTypesSlice from 'features/fba/slices/fireCentreHfiFuelTypesSlice' const rootReducer = combineReducers({ percentileStations: stationReducer, @@ -39,6 +40,7 @@ const rootReducer = combineReducers({ runDates: runDatesSlice, valueAtCoordinate: valueAtCoordinateSlice, hfiFuelTypes: hfiFuelTypesSlice, + fireCentreHfiFuelTypes: fireCentreHfiFuelTypesSlice, fireZoneElevationInfo: fireZoneElevationInfoSlice, fireZoneTPIStats: fireZoneTPIStatsSlice, fireCentreTPIStats: fireCentreTPIStatsSlice, @@ -70,6 +72,7 @@ export const selectFireShapeAreas = (state: RootState) => state.fireShapeAreas export const selectRunDates = (state: RootState) => state.runDates export const selectValueAtCoordinate = (state: RootState) => state.valueAtCoordinate export const selectHFIFuelTypes = (state: RootState) => state.hfiFuelTypes +export const selectFireCentreHFIFuelTypes = (state: RootState) => state.fireCentreHfiFuelTypes export const selectFireZoneElevationInfo = (state: RootState) => state.fireZoneElevationInfo export const selectFireZoneTPIStats = (state: RootState) => state.fireZoneTPIStats export const selectFireCentreTPIStats = (state: RootState) => state.fireCentreTPIStats diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx index 2cdb763c9..c0f2dba5d 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -1,5 +1,5 @@ import { Box, Grid, Tab, Tabs, Tooltip } from '@mui/material' -import { FireCenter, FireShape, FireShapeAreaDetail, FireZoneThresholdFuelTypeArea, FireZoneTPIStats } from 'api/fbaAPI' +import { FireCenter, FireCentreHfiFuelsData, FireShape, FireShapeAreaDetail, FireZoneTPIStats } from 'api/fbaAPI' import { INFO_PANEL_CONTENT_BACKGROUND, theme } from 'app/theme' import FireZoneUnitSummary from 'features/fba/components/infoPanel/FireZoneUnitSummary' import InfoAccordion from 'features/fba/components/infoPanel/InfoAccordion' @@ -12,9 +12,9 @@ import { useSelector } from 'react-redux' interface FireZoneUnitTabs { selectedFireZoneUnit: FireShape | undefined - fuelTypeInfo: Record setZoomSource: React.Dispatch> fireCentreTPIStats: Record | null + fireCentreHfiFuelTypes: FireCentreHfiFuelsData selectedFireCenter: FireCenter | undefined advisoryThreshold: number setSelectedFireShape: React.Dispatch> @@ -44,12 +44,12 @@ const calculateStatus = (details: FireShapeAreaDetail[], advisoryThreshold: numb } const FireZoneUnitTabs = ({ - fuelTypeInfo, selectedFireZoneUnit, setZoomSource, selectedFireCenter, advisoryThreshold, fireCentreTPIStats, + fireCentreHfiFuelTypes, setSelectedFireShape }: FireZoneUnitTabs) => { const provincialSummary = useSelector(selectProvincialSummary) @@ -112,6 +112,7 @@ const FireZoneUnitTabs = ({ } const tpiStatsArray = fireCentreTPIStats?.[selectedFireCenter.name] + const hfiFuelStats = fireCentreHfiFuelTypes?.[selectedFireCenter.name] return (
@@ -158,7 +159,7 @@ const FireZoneUnitTabs = ({ {sortedGroupedFireZoneUnits.map((zone, index) => ( stats.fire_zone_id == zone.fire_shape_id) : undefined } diff --git a/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx b/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx index fa777015d..699e847e7 100644 --- a/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx +++ b/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx @@ -5,28 +5,25 @@ import FBAMap from 'features/fba/components/map/FBAMap' import FireCenterDropdown from 'components/FireCenterDropdown' import { DateTime } from 'luxon' import { - selectFireZoneTPIStats, selectFireCenters, - selectHFIFuelTypes, selectRunDates, selectFireShapeAreas, - selectFireCentreTPIStats + selectFireCentreTPIStats, + selectFireCentreHFIFuelTypes } from 'app/rootReducer' import { useDispatch, useSelector } from 'react-redux' import { fetchFireCenters } from 'commonSlices/fireCentersSlice' import { theme } from 'app/theme' import { fetchWxStations } from 'features/stations/slices/stationsSlice' import { getStations, StationSource } from 'api/stationAPI' -import { FireCenter, FireShape, FireZoneTPIStats } from 'api/fbaAPI' +import { FireCenter, FireShape } from 'api/fbaAPI' import { ASA_DOC_TITLE, FIRE_BEHAVIOUR_ADVISORY_NAME, PST_UTC_OFFSET } from 'utils/constants' import WPSDatePicker from 'components/WPSDatePicker' import { AppDispatch } from 'app/store' import ActualForecastControl from 'features/fba/components/ActualForecastControl' import { fetchSFMSRunDates } from 'features/fba/slices/runDatesSlice' import { isNull, isUndefined } from 'lodash' -import { fetchHighHFIFuels } from 'features/fba/slices/hfiFuelTypesSlice' import { fetchFireShapeAreas } from 'features/fba/slices/fireZoneAreasSlice' -import { fetchfireZoneTPIStats } from 'features/fba/slices/fireZoneTPIStatsSlice' import { StyledFormControl } from 'components/StyledFormControl' import { getMostRecentProcessedSnowByDate } from 'api/snow' import InfoPanel from 'features/fba/components/infoPanel/InfoPanel' @@ -35,6 +32,7 @@ import AdvisoryReport from 'features/fba/components/infoPanel/AdvisoryReport' import FireZoneUnitTabs from 'features/fba/components/infoPanel/FireZoneUnitTabs' import { fetchFireCentreTPIStats } from 'features/fba/slices/fireCentreTPIStatsSlice' import AboutDataPopover from 'features/fba/components/AboutDataPopover' +import { fetchFireCentreHfiFuelTypes } from 'features/fba/slices/fireCentreHfiFuelTypesSlice' const ADVISORY_THRESHOLD = 20 @@ -51,9 +49,8 @@ export const FireCentreFormControl = styled(FormControl)({ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { const dispatch: AppDispatch = useDispatch() const { fireCenters } = useSelector(selectFireCenters) - const { hfiThresholdsFuelTypes } = useSelector(selectHFIFuelTypes) - const { fireZoneTPIStats } = useSelector(selectFireZoneTPIStats) const { fireCentreTPIStats } = useSelector(selectFireCentreTPIStats) + const { fireCentreHfiFuelTypes } = useSelector(selectFireCentreHFIFuelTypes) const [fireCenter, setFireCenter] = useState(undefined) @@ -68,11 +65,6 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { const [snowDate, setSnowDate] = useState(null) const { mostRecentRunDate } = useSelector(selectRunDates) const { fireShapeAreas } = useSelector(selectFireShapeAreas) - const [selectedFireZoneTPIStats, setSelectedFireZoneTPIStats] = useState(null) - const [selectedFireCentreTPIStats, setSelectedFireCentreTPIStats] = useState | null>(null) // Query our API for the most recently processed snow coverage date <= the currently selected date. const fetchLastProcessedSnow = async (selectedDate: DateTime) => { @@ -138,21 +130,6 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { fetchLastProcessedSnow(dateOfInterest) }, [dateOfInterest]) // eslint-disable-line react-hooks/exhaustive-deps - useEffect(() => { - const doiISODate = dateOfInterest.toISODate() - if ( - !isNull(mostRecentRunDate) && - !isNull(doiISODate) && - !isUndefined(mostRecentRunDate) && - !isUndefined(selectedFireShape) - ) { - dispatch(fetchHighHFIFuels(runType, doiISODate, mostRecentRunDate.toString(), selectedFireShape.fire_shape_id)) - dispatch( - fetchfireZoneTPIStats(selectedFireShape.fire_shape_id, runType, doiISODate, mostRecentRunDate.toString()) - ) - } - }, [mostRecentRunDate, selectedFireShape]) // eslint-disable-line react-hooks/exhaustive-deps - useEffect(() => { const doiISODate = dateOfInterest.toISODate() if ( @@ -163,6 +140,7 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { !isNull(fireCenter) ) { dispatch(fetchFireCentreTPIStats(fireCenter.name, runType, doiISODate, mostRecentRunDate.toString())) + dispatch(fetchFireCentreHfiFuelTypes(fireCenter.name, runType, doiISODate, mostRecentRunDate.toString())) } }, [fireCenter, mostRecentRunDate]) @@ -174,22 +152,6 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { } }, [mostRecentRunDate]) // eslint-disable-line react-hooks/exhaustive-deps - useEffect(() => { - const selectedFireShapeId = selectedFireShape?.fire_shape_id - if (isNull(fireZoneTPIStats) || isUndefined(selectedFireShapeId)) { - setSelectedFireZoneTPIStats(null) - } - if (fireZoneTPIStats?.fire_zone_id === selectedFireShapeId) { - setSelectedFireZoneTPIStats(fireZoneTPIStats) - } - }, [fireZoneTPIStats]) - - useEffect(() => { - if (fireCentreTPIStats && Object.keys(fireCentreTPIStats)[0] === fireCenter?.name) { - setSelectedFireCentreTPIStats(fireCentreTPIStats) - } - }, [fireCentreTPIStats]) - useEffect(() => { document.title = ASA_DOC_TITLE }, []) @@ -239,10 +201,10 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { selectedFireCenter={fireCenter} /> ) { + state.error = action.payload + state.loading = false + }, + getFireCentreHfiFuelTypesSuccess(state: State, action: PayloadAction) { + state.error = null + state.fireCentreHfiFuelTypes = action.payload + state.loading = false + } + } +}) + +export const { getFireCentreHfiFuelTypesStart, getFireCentreHfiFuelTypesFailed, getFireCentreHfiFuelTypesSuccess } = + fireCentreHfiFuelTypesSlice.actions + +export default fireCentreHfiFuelTypesSlice.reducer + +export const fetchFireCentreHfiFuelTypes = + (fireCentre: string, runType: RunType, forDate: string, runDatetime: string): AppThunk => + async dispatch => { + try { + dispatch(getFireCentreHfiFuelTypesStart()) + const data = await getHFIThresholdsFuelTypesForCentre(runType, forDate, runDatetime, fireCentre) + dispatch(getFireCentreHfiFuelTypesSuccess(data)) + } catch (err) { + dispatch(getFireCentreHfiFuelTypesFailed((err as Error).toString())) + logError(err) + } + } diff --git a/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts b/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts index 99b004515..5f3059896 100644 --- a/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts +++ b/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts @@ -44,11 +44,11 @@ export const { getFireCentreTPIStatsStart, getFireCentreTPIStatsFailed, getFireC export default fireCentreTPIStatsSlice.reducer export const fetchFireCentreTPIStats = - (fire_centre_name: string, runType: RunType, forDate: string, runDatetime: string): AppThunk => + (fireCentre: string, runType: RunType, forDate: string, runDatetime: string): AppThunk => async dispatch => { try { dispatch(getFireCentreTPIStatsStart()) - const fireCentreTPIStats = await getFireCentreTPIStats(fire_centre_name, runType, forDate, runDatetime) + const fireCentreTPIStats = await getFireCentreTPIStats(fireCentre, runType, forDate, runDatetime) dispatch(getFireCentreTPIStatsSuccess(fireCentreTPIStats)) } catch (err) { dispatch(getFireCentreTPIStatsFailed((err as Error).toString())) diff --git a/web/src/features/fba/slices/hfiFuelTypesSlice.ts b/web/src/features/fba/slices/hfiFuelTypesSlice.ts index ab801ab5c..a19266635 100644 --- a/web/src/features/fba/slices/hfiFuelTypesSlice.ts +++ b/web/src/features/fba/slices/hfiFuelTypesSlice.ts @@ -18,7 +18,7 @@ const initialState: State = { } const hfiFuelTypesSlice = createSlice({ - name: 'runDates', + name: 'hfiFuelTypes', initialState, reducers: { getHFIFuelsStart(state: State) { From 49205d21fa8a6c1d16b33af54b4b4f219e8ffdd2 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Thu, 5 Sep 2024 08:46:09 -0700 Subject: [PATCH 17/42] custom hook? --- .../components/infoPanel/FireZoneUnitTabs.tsx | 24 +++----------- .../fba/hooks/useFireZoneUnitDetails.ts | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 web/src/features/fba/hooks/useFireZoneUnitDetails.ts diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx index c0f2dba5d..4d88072e8 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -5,10 +5,9 @@ import FireZoneUnitSummary from 'features/fba/components/infoPanel/FireZoneUnitS import InfoAccordion from 'features/fba/components/infoPanel/InfoAccordion' import TabPanel from 'features/fba/components/infoPanel/TabPanel' import { ADVISORY_ORANGE_FILL, ADVISORY_RED_FILL } from 'features/fba/components/map/featureStylers' -import { selectProvincialSummary } from 'features/fba/slices/provincialSummarySlice' -import { groupBy, isNull, isUndefined } from 'lodash' -import React, { useEffect, useMemo, useState } from 'react' -import { useSelector } from 'react-redux' +import { useFireZoneUnitDetails } from 'features/fba/hooks/useFireZoneUnitDetails' +import { isNull, isUndefined } from 'lodash' +import React, { useEffect, useState } from 'react' interface FireZoneUnitTabs { selectedFireZoneUnit: FireShape | undefined @@ -52,24 +51,9 @@ const FireZoneUnitTabs = ({ fireCentreHfiFuelTypes, setSelectedFireShape }: FireZoneUnitTabs) => { - const provincialSummary = useSelector(selectProvincialSummary) const [tabNumber, setTabNumber] = useState(0) - const fireCenterSummary = selectedFireCenter ? provincialSummary[selectedFireCenter.name] : [] - - const groupedFireZoneUnits = useMemo(() => groupBy(fireCenterSummary, 'fire_shape_id'), [fireCenterSummary]) - const sortedGroupedFireZoneUnits = useMemo( - () => - Object.values(groupedFireZoneUnits) - .map(group => ({ - fire_shape_id: group[0].fire_shape_id, - fire_shape_name: group[0].fire_shape_name, - fire_centre_name: group[0].fire_centre_name, - fireShapeDetails: group - })) - .sort((a, b) => a.fire_shape_name.localeCompare(b.fire_shape_name)), - [groupedFireZoneUnits] - ) + const sortedGroupedFireZoneUnits = useFireZoneUnitDetails(selectedFireCenter) useEffect(() => { if (selectedFireZoneUnit) { diff --git a/web/src/features/fba/hooks/useFireZoneUnitDetails.ts b/web/src/features/fba/hooks/useFireZoneUnitDetails.ts new file mode 100644 index 000000000..d031f516a --- /dev/null +++ b/web/src/features/fba/hooks/useFireZoneUnitDetails.ts @@ -0,0 +1,32 @@ +import { useMemo } from 'react' +import { useSelector } from 'react-redux' +import { groupBy } from 'lodash' +import { FireCenter, FireShapeAreaDetail } from 'api/fbaAPI' +import { selectProvincialSummary } from 'features/fba/slices/provincialSummarySlice' + +export interface GroupedFireZoneUnitDetails { + fire_shape_id: number + fire_shape_name: string + fire_centre_name: string + fireShapeDetails: FireShapeAreaDetail[] +} + +export const useFireZoneUnitDetails = (selectedFireCenter: FireCenter | undefined): GroupedFireZoneUnitDetails[] => { + const provincialSummary = useSelector(selectProvincialSummary) + + return useMemo(() => { + if (!selectedFireCenter) return [] + + const fireCenterSummary = provincialSummary[selectedFireCenter.name] || [] + const groupedFireZoneUnits = groupBy(fireCenterSummary, 'fire_shape_id') + + return Object.values(groupedFireZoneUnits) + .map(group => ({ + fire_shape_id: group[0].fire_shape_id, + fire_shape_name: group[0].fire_shape_name, + fire_centre_name: group[0].fire_centre_name, + fireShapeDetails: group + })) + .sort((a, b) => a.fire_shape_name.localeCompare(b.fire_shape_name)) + }, [selectedFireCenter, provincialSummary]) +} From 1ae8828df66c09ba0d293c096b081a7e6097db91 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Thu, 5 Sep 2024 08:58:36 -0700 Subject: [PATCH 18/42] commented code remove --- web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx index 4d88072e8..027192f1d 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -127,7 +127,6 @@ const FireZoneUnitTabs = ({ sx={{ backgroundColor: calculateStatus(zone.fireShapeDetails, advisoryThreshold), minWidth: 'auto', - // border: '0.25px solid grey', marginTop: theme.spacing(2), fontWeight: 'bold', color: isActive ? 'black' : 'grey', From d01873826b60f8996e90786331b2b4638565623e Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Thu, 5 Sep 2024 12:37:15 -0700 Subject: [PATCH 19/42] hook name change --- .../features/fba/components/infoPanel/FireZoneUnitTabs.tsx | 4 ++-- web/src/features/fba/components/viz/FuelSummary.tsx | 2 +- .../{useFireZoneUnitDetails.ts => useFireCentreDetails.ts} | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename web/src/features/fba/hooks/{useFireZoneUnitDetails.ts => useFireCentreDetails.ts} (90%) diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx index 027192f1d..34a16db16 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -5,7 +5,7 @@ import FireZoneUnitSummary from 'features/fba/components/infoPanel/FireZoneUnitS import InfoAccordion from 'features/fba/components/infoPanel/InfoAccordion' import TabPanel from 'features/fba/components/infoPanel/TabPanel' import { ADVISORY_ORANGE_FILL, ADVISORY_RED_FILL } from 'features/fba/components/map/featureStylers' -import { useFireZoneUnitDetails } from 'features/fba/hooks/useFireZoneUnitDetails' +import { useFireCentreDetails } from 'features/fba/hooks/useFireCentreDetails' import { isNull, isUndefined } from 'lodash' import React, { useEffect, useState } from 'react' @@ -53,7 +53,7 @@ const FireZoneUnitTabs = ({ }: FireZoneUnitTabs) => { const [tabNumber, setTabNumber] = useState(0) - const sortedGroupedFireZoneUnits = useFireZoneUnitDetails(selectedFireCenter) + const sortedGroupedFireZoneUnits = useFireCentreDetails(selectedFireCenter) useEffect(() => { if (selectedFireZoneUnit) { diff --git a/web/src/features/fba/components/viz/FuelSummary.tsx b/web/src/features/fba/components/viz/FuelSummary.tsx index ddc7785a2..c28c327ee 100644 --- a/web/src/features/fba/components/viz/FuelSummary.tsx +++ b/web/src/features/fba/components/viz/FuelSummary.tsx @@ -50,7 +50,7 @@ const columns: GridColDef[] = [ field: 'area', flex: 3, headerClassName: 'fuel-summary-header', - headerName: 'Proportion of Advisory Area', + headerName: 'Distribution > 4k kW/m', minWidth: 200, sortable: false, renderHeader: (params: GridColumnHeaderParams) => {params.colDef.headerName}, diff --git a/web/src/features/fba/hooks/useFireZoneUnitDetails.ts b/web/src/features/fba/hooks/useFireCentreDetails.ts similarity index 90% rename from web/src/features/fba/hooks/useFireZoneUnitDetails.ts rename to web/src/features/fba/hooks/useFireCentreDetails.ts index d031f516a..7b27395c9 100644 --- a/web/src/features/fba/hooks/useFireZoneUnitDetails.ts +++ b/web/src/features/fba/hooks/useFireCentreDetails.ts @@ -11,7 +11,7 @@ export interface GroupedFireZoneUnitDetails { fireShapeDetails: FireShapeAreaDetail[] } -export const useFireZoneUnitDetails = (selectedFireCenter: FireCenter | undefined): GroupedFireZoneUnitDetails[] => { +export const useFireCentreDetails = (selectedFireCenter: FireCenter | undefined): GroupedFireZoneUnitDetails[] => { const provincialSummary = useSelector(selectProvincialSummary) return useMemo(() => { From 9d963d05d3ce0bfe2e77b0ed38a1ca541a380020 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Mon, 9 Sep 2024 13:05:35 -0700 Subject: [PATCH 20/42] memoize variables & data-testid's --- .../components/infoPanel/FireZoneUnitTabs.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx index 34a16db16..443e016d4 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -7,7 +7,7 @@ import TabPanel from 'features/fba/components/infoPanel/TabPanel' import { ADVISORY_ORANGE_FILL, ADVISORY_RED_FILL } from 'features/fba/components/map/featureStylers' import { useFireCentreDetails } from 'features/fba/hooks/useFireCentreDetails' import { isNull, isUndefined } from 'lodash' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' interface FireZoneUnitTabs { selectedFireZoneUnit: FireShape | undefined @@ -65,7 +65,7 @@ const FireZoneUnitTabs = ({ } } else { setTabNumber(0) - setSelectedFireShape(getTabFireShape(0)) + setSelectedFireShape(getTabFireShape(0)) // if no selected FireShape, select the first one in the sorted tabs } }, [selectedFireZoneUnit, sortedGroupedFireZoneUnits]) @@ -91,13 +91,22 @@ const FireZoneUnitTabs = ({ setZoomSource('fireShape') } + const tpiStatsArray = useMemo(() => { + if (selectedFireCenter) { + return fireCentreTPIStats?.[selectedFireCenter.name] + } + }, [fireCentreTPIStats, selectedFireCenter]) + + const hfiFuelStats = useMemo(() => { + if (selectedFireCenter) { + return fireCentreHfiFuelTypes?.[selectedFireCenter?.name] + } + }, [fireCentreHfiFuelTypes, selectedFireCenter]) + if (isUndefined(selectedFireCenter) || isNull(selectedFireCenter)) { return
} - const tpiStatsArray = fireCentreTPIStats?.[selectedFireCenter.name] - const hfiFuelStats = fireCentreHfiFuelTypes?.[selectedFireCenter.name] - return (
Date: Mon, 9 Sep 2024 13:29:58 -0700 Subject: [PATCH 21/42] add tests --- .../components/infoPanel/FireZoneUnitTabs.tsx | 2 +- .../infoPanel/fireZoneUnitTabs.test.tsx | 125 ++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx index 443e016d4..b38dad15d 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -104,7 +104,7 @@ const FireZoneUnitTabs = ({ }, [fireCentreHfiFuelTypes, selectedFireCenter]) if (isUndefined(selectedFireCenter) || isNull(selectedFireCenter)) { - return
+ return
} return ( diff --git a/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx b/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx new file mode 100644 index 000000000..19b30a6cc --- /dev/null +++ b/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx @@ -0,0 +1,125 @@ +import React from 'react' +import { render, screen, fireEvent } from '@testing-library/react' +import FireZoneUnitTabs from './FireZoneUnitTabs' +import { FireCenter, FireCentreHfiFuelsData, FireShape, FireZoneTPIStats } from 'api/fbaAPI' +import { vi } from 'vitest' + +const fireCentre1 = 'Centre 1' + +// Mock props +const mockSelectedFireZoneUnitA: FireShape = { + fire_shape_id: 1, + mof_fire_centre_name: fireCentre1, + mof_fire_zone_name: 'A Zone' +} + +const mockSelectedFireCenter: FireCenter = { + id: 1, + name: fireCentre1, + stations: [] +} + +const mockFireCentreTPIStats: Record = { + [fireCentre1]: [{ fire_zone_id: 1, valley_bottom: 10, mid_slope: 90, upper_slope: 10 }] +} + +const mockFireCentreHfiFuelTypes: FireCentreHfiFuelsData = { + [fireCentre1]: { + '1': [ + { + fuel_type: { fuel_type_id: 1, fuel_type_code: 'C', description: 'fuel type' }, + area: 10, + threshold: { id: 1, name: 'threshold', description: 'description' } + } + ] + } +} + +const mockSortedGroupedFireZoneUnits = [ + { + fire_shape_id: 1, + fire_shape_name: 'A Zone', + fire_centre_name: fireCentre1, + fireShapeDetails: [] + }, + { + fire_shape_id: 2, + fire_shape_name: 'B Zone', + fire_centre_name: fireCentre1, + fireShapeDetails: [] + } +] + +vi.mock('features/fba/hooks/useFireCentreDetails', () => ({ + useFireCentreDetails: () => mockSortedGroupedFireZoneUnits +})) + +const setSelectedFireShapeMock = vi.fn() +const setZoomSourceMock = vi.fn() + +const renderComponent = () => + render( + + ) + +describe('FireZoneUnitTabs', () => { + it('should render', () => { + const { getByTestId } = renderComponent() + + const summaryTabs = getByTestId('firezone-summary-tabs') + expect(summaryTabs).toBeInTheDocument() + }) + it('should render tabs for each zone in a centre', () => { + const { getByTestId } = renderComponent() + + const tab1 = getByTestId('zone-1-tab') + expect(tab1).toBeInTheDocument() + const tab2 = getByTestId('zone-2-tab') + expect(tab2).toBeInTheDocument() + const tabs = screen.getAllByRole('tab') + expect(tabs.length).toBe(2) + }) + it('should select the first zone tab of a fire centre alphabetically if no zone is selected, but not zoom to it', () => { + renderComponent() + + expect(setSelectedFireShapeMock).toHaveBeenCalledWith(mockSelectedFireZoneUnitA) + expect(setZoomSourceMock).not.toHaveBeenCalled() + }) + it('should switch to a different tab when clicked and set the map zoom source', () => { + renderComponent() + + const tab2 = screen.getByTestId('zone-2-tab') + fireEvent.click(tab2) + + expect(setSelectedFireShapeMock).toHaveBeenCalledWith({ + fire_shape_id: 2, + mof_fire_centre_name: fireCentre1, + mof_fire_zone_name: 'B Zone' + }) + expect(setZoomSourceMock).toHaveBeenCalledWith('fireShape') + }) + it('should render empty if there is no selected Fire Centre', () => { + const { getByTestId } = render( + + ) + + const emptyTabs = getByTestId('fire-zone-unit-tabs-empty') + expect(emptyTabs).toBeInTheDocument() + }) +}) From 71f0ca6613ee1c80bf50cc6d51717a45a05be078 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Mon, 9 Sep 2024 13:30:38 -0700 Subject: [PATCH 22/42] comments --- .../features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx b/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx index 19b30a6cc..56c5e736d 100644 --- a/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx +++ b/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx @@ -6,7 +6,6 @@ import { vi } from 'vitest' const fireCentre1 = 'Centre 1' -// Mock props const mockSelectedFireZoneUnitA: FireShape = { fire_shape_id: 1, mof_fire_centre_name: fireCentre1, From 1e35698f0c2ba9420694ec86921e3e3b617bfbf8 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Mon, 9 Sep 2024 13:34:15 -0700 Subject: [PATCH 23/42] js doc for custom hook --- web/src/features/fba/hooks/useFireCentreDetails.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/src/features/fba/hooks/useFireCentreDetails.ts b/web/src/features/fba/hooks/useFireCentreDetails.ts index 7b27395c9..5c85ccb0a 100644 --- a/web/src/features/fba/hooks/useFireCentreDetails.ts +++ b/web/src/features/fba/hooks/useFireCentreDetails.ts @@ -11,6 +11,13 @@ export interface GroupedFireZoneUnitDetails { fireShapeDetails: FireShapeAreaDetail[] } +/** + * Hook for grabbing a fire centre from the provincial summary, grouping by unique 'fire_shape_id' and + * providing easy access to the shape name, centre, and FireShapeAreaDetails for calculating zone status + * + * @param selectedFireCenter + * @returns + */ export const useFireCentreDetails = (selectedFireCenter: FireCenter | undefined): GroupedFireZoneUnitDetails[] => { const provincialSummary = useSelector(selectProvincialSummary) From 1945443d87d3ba0d241fee787cca131d24ca872b Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Mon, 9 Sep 2024 14:27:30 -0700 Subject: [PATCH 24/42] tab colour tests --- .../infoPanel/fireZoneUnitTabs.test.tsx | 53 ++++++++++++++++--- web/vite.config.ts | 5 +- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx b/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx index 56c5e736d..a95eb8902 100644 --- a/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx +++ b/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx @@ -1,15 +1,18 @@ import React from 'react' import { render, screen, fireEvent } from '@testing-library/react' import FireZoneUnitTabs from './FireZoneUnitTabs' -import { FireCenter, FireCentreHfiFuelsData, FireShape, FireZoneTPIStats } from 'api/fbaAPI' +import { FireCenter, FireCentreHfiFuelsData, FireShape, FireShapeAreaDetail, FireZoneTPIStats } from 'api/fbaAPI' import { vi } from 'vitest' +import { ADVISORY_ORANGE_FILL, ADVISORY_RED_FILL } from '@/features/fba/components/map/featureStylers' const fireCentre1 = 'Centre 1' +const zoneA = 'A Zone' +const zoneB = 'B Zone' const mockSelectedFireZoneUnitA: FireShape = { fire_shape_id: 1, mof_fire_centre_name: fireCentre1, - mof_fire_zone_name: 'A Zone' + mof_fire_zone_name: zoneA } const mockSelectedFireCenter: FireCenter = { @@ -22,6 +25,34 @@ const mockFireCentreTPIStats: Record = { [fireCentre1]: [{ fire_zone_id: 1, valley_bottom: 10, mid_slope: 90, upper_slope: 10 }] } +const getAdvisoryDetails = ( + fireZoneName: string, + fireShapeId: number, + advisoryPercent: number, + warningPercent: number +): FireShapeAreaDetail[] => { + return [ + { + fire_shape_id: fireShapeId, + threshold: 1, + combustible_area: 1, + elevated_hfi_area: 2, + elevated_hfi_percentage: advisoryPercent, + fire_shape_name: fireZoneName, + fire_centre_name: fireCentre1 + }, + { + fire_shape_id: fireShapeId, + threshold: 2, + combustible_area: 1, + elevated_hfi_area: 2, + elevated_hfi_percentage: warningPercent, + fire_shape_name: fireZoneName, + fire_centre_name: fireCentre1 + } + ] +} + const mockFireCentreHfiFuelTypes: FireCentreHfiFuelsData = { [fireCentre1]: { '1': [ @@ -37,15 +68,15 @@ const mockFireCentreHfiFuelTypes: FireCentreHfiFuelsData = { const mockSortedGroupedFireZoneUnits = [ { fire_shape_id: 1, - fire_shape_name: 'A Zone', + fire_shape_name: zoneA, fire_centre_name: fireCentre1, - fireShapeDetails: [] + fireShapeDetails: getAdvisoryDetails(zoneA, 1, 30, 10) }, { fire_shape_id: 2, - fire_shape_name: 'B Zone', + fire_shape_name: zoneB, fire_centre_name: fireCentre1, - fireShapeDetails: [] + fireShapeDetails: getAdvisoryDetails(zoneB, 2, 30, 30) } ] @@ -101,7 +132,7 @@ describe('FireZoneUnitTabs', () => { expect(setSelectedFireShapeMock).toHaveBeenCalledWith({ fire_shape_id: 2, mof_fire_centre_name: fireCentre1, - mof_fire_zone_name: 'B Zone' + mof_fire_zone_name: zoneB }) expect(setZoomSourceMock).toHaveBeenCalledWith('fireShape') }) @@ -121,4 +152,12 @@ describe('FireZoneUnitTabs', () => { const emptyTabs = getByTestId('fire-zone-unit-tabs-empty') expect(emptyTabs).toBeInTheDocument() }) + it('should render tabs with the correct advisory colour', () => { + const { getByTestId } = renderComponent() + + const tab1 = getByTestId('zone-1-tab') + expect(tab1).toHaveStyle(`backgroundColor: ${ADVISORY_ORANGE_FILL}`) + const tab2 = getByTestId('zone-2-tab') + expect(tab2).toHaveStyle(`backgroundColor: ${ADVISORY_RED_FILL}`) + }) }) diff --git a/web/vite.config.ts b/web/vite.config.ts index a39003fcd..8acf9505c 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react' import { resolve } from 'path' import svgr from 'vite-plugin-svgr' import istanbul from 'vite-plugin-istanbul' -import { sentryVitePlugin } from "@sentry/vite-plugin"; +import { sentryVitePlugin } from '@sentry/vite-plugin' export default defineConfig({ build: { @@ -43,7 +43,7 @@ export default defineConfig({ project: 'frontend', authToken: process.env.SENTRY_AUTH_TOKEN, sourcemaps: { - filesToDeleteAfterUpload: "build/assets/**.map" + filesToDeleteAfterUpload: 'build/assets/**.map' } }), istanbul({ @@ -65,6 +65,7 @@ export default defineConfig({ }, resolve: { alias: { + '@': path.resolve(__dirname, './src'), app: resolve(__dirname, 'src', 'app'), features: resolve(__dirname, 'src', 'features'), utils: resolve(__dirname, 'src', 'utils'), From 9a00d956a7f3c04b4b7a6598da30b474c1ae5091 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Mon, 9 Sep 2024 14:28:14 -0700 Subject: [PATCH 25/42] new lines --- .../fba/components/infoPanel/fireZoneUnitTabs.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx b/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx index a95eb8902..427cbbbb3 100644 --- a/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx +++ b/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx @@ -107,6 +107,7 @@ describe('FireZoneUnitTabs', () => { const summaryTabs = getByTestId('firezone-summary-tabs') expect(summaryTabs).toBeInTheDocument() }) + it('should render tabs for each zone in a centre', () => { const { getByTestId } = renderComponent() @@ -117,12 +118,14 @@ describe('FireZoneUnitTabs', () => { const tabs = screen.getAllByRole('tab') expect(tabs.length).toBe(2) }) + it('should select the first zone tab of a fire centre alphabetically if no zone is selected, but not zoom to it', () => { renderComponent() expect(setSelectedFireShapeMock).toHaveBeenCalledWith(mockSelectedFireZoneUnitA) expect(setZoomSourceMock).not.toHaveBeenCalled() }) + it('should switch to a different tab when clicked and set the map zoom source', () => { renderComponent() @@ -136,6 +139,7 @@ describe('FireZoneUnitTabs', () => { }) expect(setZoomSourceMock).toHaveBeenCalledWith('fireShape') }) + it('should render empty if there is no selected Fire Centre', () => { const { getByTestId } = render( { const emptyTabs = getByTestId('fire-zone-unit-tabs-empty') expect(emptyTabs).toBeInTheDocument() }) + it('should render tabs with the correct advisory colour', () => { const { getByTestId } = renderComponent() From 7bda8aabada40f92deed22cad628356e4704f273 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Mon, 9 Sep 2024 14:29:46 -0700 Subject: [PATCH 26/42] reorder details func --- .../infoPanel/fireZoneUnitTabs.test.tsx | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx b/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx index 427cbbbb3..6b6067dea 100644 --- a/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx +++ b/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx @@ -5,26 +5,6 @@ import { FireCenter, FireCentreHfiFuelsData, FireShape, FireShapeAreaDetail, Fir import { vi } from 'vitest' import { ADVISORY_ORANGE_FILL, ADVISORY_RED_FILL } from '@/features/fba/components/map/featureStylers' -const fireCentre1 = 'Centre 1' -const zoneA = 'A Zone' -const zoneB = 'B Zone' - -const mockSelectedFireZoneUnitA: FireShape = { - fire_shape_id: 1, - mof_fire_centre_name: fireCentre1, - mof_fire_zone_name: zoneA -} - -const mockSelectedFireCenter: FireCenter = { - id: 1, - name: fireCentre1, - stations: [] -} - -const mockFireCentreTPIStats: Record = { - [fireCentre1]: [{ fire_zone_id: 1, valley_bottom: 10, mid_slope: 90, upper_slope: 10 }] -} - const getAdvisoryDetails = ( fireZoneName: string, fireShapeId: number, @@ -53,6 +33,26 @@ const getAdvisoryDetails = ( ] } +const fireCentre1 = 'Centre 1' +const zoneA = 'A Zone' +const zoneB = 'B Zone' + +const mockSelectedFireZoneUnitA: FireShape = { + fire_shape_id: 1, + mof_fire_centre_name: fireCentre1, + mof_fire_zone_name: zoneA +} + +const mockSelectedFireCenter: FireCenter = { + id: 1, + name: fireCentre1, + stations: [] +} + +const mockFireCentreTPIStats: Record = { + [fireCentre1]: [{ fire_zone_id: 1, valley_bottom: 10, mid_slope: 90, upper_slope: 10 }] +} + const mockFireCentreHfiFuelTypes: FireCentreHfiFuelsData = { [fireCentre1]: { '1': [ From 78badf10d87f4a42f43c1ac7d7c8b73497977fcf Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Mon, 9 Sep 2024 15:50:01 -0700 Subject: [PATCH 27/42] consolidate zone status calculators --- web/src/features/fba/calculateZoneStatus.ts | 48 +++++++++++++++++++ .../fba/components/infoPanel/AdvisoryText.tsx | 18 +------ .../components/infoPanel/FireZoneUnitInfo.tsx | 29 +---------- .../components/infoPanel/FireZoneUnitTabs.tsx | 29 ++--------- 4 files changed, 55 insertions(+), 69 deletions(-) create mode 100644 web/src/features/fba/calculateZoneStatus.ts diff --git a/web/src/features/fba/calculateZoneStatus.ts b/web/src/features/fba/calculateZoneStatus.ts new file mode 100644 index 000000000..abb8a26fd --- /dev/null +++ b/web/src/features/fba/calculateZoneStatus.ts @@ -0,0 +1,48 @@ +import { FireShapeAreaDetail } from '@/api/fbaAPI' +import { ADVISORY_ORANGE_FILL, ADVISORY_RED_FILL } from '@/features/fba/components/map/featureStylers' +import { AdvisoryStatus } from '@/utils/constants' + +export const calculateStatusColour = ( + details: FireShapeAreaDetail[], + advisoryThreshold: number, + defaultColour: string +) => { + let status = defaultColour + + if (details.length === 0) { + return status + } + + 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 (advisoryPercentage + warningPercentage > advisoryThreshold) { + status = ADVISORY_ORANGE_FILL + } + + if (warningPercentage > advisoryThreshold) { + status = ADVISORY_RED_FILL + } + + return status +} + +export const calculateStatusText = ( + details: FireShapeAreaDetail[], + advisoryThreshold: number +): 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 + } +} diff --git a/web/src/features/fba/components/infoPanel/AdvisoryText.tsx b/web/src/features/fba/components/infoPanel/AdvisoryText.tsx index 1368db89e..520a6469b 100644 --- a/web/src/features/fba/components/infoPanel/AdvisoryText.tsx +++ b/web/src/features/fba/components/infoPanel/AdvisoryText.tsx @@ -6,6 +6,7 @@ import { useSelector } from 'react-redux' import { selectProvincialSummary } from 'features/fba/slices/provincialSummarySlice' import { AdvisoryStatus } from 'utils/constants' import { groupBy } from 'lodash' +import { calculateStatusText } from '@/features/fba/calculateZoneStatus' interface AdvisoryTextProps { issueDate: DateTime | null @@ -17,21 +18,6 @@ interface AdvisoryTextProps { 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) => { const zoneStatusMap: Record = { [AdvisoryStatus.ADVISORY]: [], @@ -40,7 +26,7 @@ const AdvisoryText = ({ issueDate, forDate, advisoryThreshold, selectedFireCente for (const zoneUnit in fireZoneUnitDetails) { const fireShapeAreaDetails: FireShapeAreaDetail[] = fireZoneUnitDetails[zoneUnit] - const status = calculateStatus(fireShapeAreaDetails) + const status = calculateStatusText(fireShapeAreaDetails, advisoryThreshold) if (status) { zoneStatusMap[status].push(zoneUnit) diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitInfo.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitInfo.tsx index ab35f9c5b..ee2063787 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitInfo.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitInfo.tsx @@ -1,9 +1,9 @@ import React from 'react' import { Box, ListItem, ListItemIcon, Typography } from '@mui/material' -import { ADVISORY_ORANGE_FILL, ADVISORY_RED_FILL } from 'features/fba/components/map/featureStylers' import { FireShapeAreaDetail } from 'api/fbaAPI' import { useTheme } from '@mui/material/styles' import { TRANSPARENT_COLOUR } from 'app/theme' +import { calculateStatusColour } from '@/features/fba/calculateZoneStatus' interface FireZoneUnitInfoProps { advisoryThreshold: number @@ -13,31 +13,6 @@ interface FireZoneUnitInfoProps { const FireZoneUnitInfo = ({ advisoryThreshold, fireZoneUnitName, fireZoneUnitDetails }: FireZoneUnitInfoProps) => { const theme = useTheme() - const calculateStatus = (details: FireShapeAreaDetail[]) => { - // Default is transparent - let status = TRANSPARENT_COLOUR - - if (details.length === 0) { - return status - } - - 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 (advisoryPercentage + warningPercentage > advisoryThreshold) { - // advisory color orange - status = ADVISORY_ORANGE_FILL - } - - if (warningPercentage > advisoryThreshold) { - // advisory color red - status = ADVISORY_RED_FILL - } - - return status - } return ( @@ -45,7 +20,7 @@ const FireZoneUnitInfo = ({ advisoryThreshold, fireZoneUnitName, fireZoneUnitDet > } -const calculateStatus = (details: FireShapeAreaDetail[], advisoryThreshold: number) => { - let status = '#DCDCDC' - - if (details.length === 0) { - return status - } - - 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 (advisoryPercentage + warningPercentage > advisoryThreshold) { - status = ADVISORY_ORANGE_FILL - } - - if (warningPercentage > advisoryThreshold) { - status = ADVISORY_RED_FILL - } - - return status -} - const FireZoneUnitTabs = ({ selectedFireZoneUnit, setZoomSource, @@ -135,7 +112,7 @@ const FireZoneUnitTabs = ({ key={key} data-testid={`zone-${key}-tab`} sx={{ - backgroundColor: calculateStatus(zone.fireShapeDetails, advisoryThreshold), + backgroundColor: calculateStatusColour(zone.fireShapeDetails, advisoryThreshold, '#DCDCDC'), minWidth: 'auto', marginTop: theme.spacing(2), fontWeight: 'bold', From 2033a7523e6e5dc84fa6571c649817bb57fe3094 Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Mon, 9 Sep 2024 16:13:54 -0700 Subject: [PATCH 28/42] Implement e2e --- .vscode/settings.json | 2 ++ api/app/db/crud/auto_spatial_advisory.py | 23 +++++++++++++------ api/app/routers/fba.py | 14 +++++------ api/app/schemas/fba.py | 8 +++++++ web/src/api/fbaAPI.ts | 7 ++++++ .../fba/components/viz/CriticalHours.tsx | 20 ++++++++++++++++ .../fba/components/viz/FuelSummary.tsx | 21 +++++++++++++++-- 7 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 web/src/features/fba/components/viz/CriticalHours.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 6ee2bd253..9a1c2c7a4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -107,6 +107,7 @@ "PROJCS", "pydantic", "RDPS", + "reduxjs", "reproject", "rocketchat", "rollup", @@ -115,6 +116,7 @@ "sfms", "sqlalchemy", "starlette", + "testid", "tobytes", "upsampled", "uvicorn", diff --git a/api/app/db/crud/auto_spatial_advisory.py b/api/app/db/crud/auto_spatial_advisory.py index 371d1404d..4694ff06b 100644 --- a/api/app/db/crud/auto_spatial_advisory.py +++ b/api/app/db/crud/auto_spatial_advisory.py @@ -141,7 +141,7 @@ async def get_zone_ids_in_centre(session: AsyncSession, fire_centre_name: str): return all_results - + async def get_all_sfms_fuel_type_records(session: AsyncSession) -> List[SFMSFuelType]: """ Retrieve all records from the sfms_fuel_types table. @@ -154,21 +154,30 @@ async def get_all_sfms_fuel_type_records(session: AsyncSession) -> List[SFMSFuel return result.all() -async def get_precomputed_high_hfi_fuel_type_areas_for_shape(session: AsyncSession, run_type: RunTypeEnum, run_datetime: datetime, for_date: date, advisory_shape_id: int) -> List[Row]: +async def get_precomputed_stats_for_shape(session: AsyncSession, run_type: RunTypeEnum, run_datetime: datetime, for_date: date, advisory_shape_id: int) -> List[Row]: perf_start = perf_counter() stmt = ( - select(AdvisoryFuelStats.advisory_shape_id, AdvisoryFuelStats.fuel_type, AdvisoryFuelStats.threshold, AdvisoryFuelStats.area, AdvisoryFuelStats.run_parameters) - .join_from(AdvisoryFuelStats, RunParameters, AdvisoryFuelStats.run_parameters == RunParameters.id) - .join_from(AdvisoryFuelStats, Shape, AdvisoryFuelStats.advisory_shape_id == Shape.id) + select( + AdvisoryFuelStats.advisory_shape_id, + CriticalHours.start_hour, + CriticalHours.end_hour, + AdvisoryFuelStats.fuel_type, + AdvisoryFuelStats.threshold, + AdvisoryFuelStats.area, + AdvisoryFuelStats.run_parameters, + ) + .distinct(AdvisoryFuelStats.fuel_type, AdvisoryFuelStats.run_parameters) # Keep unique records by fuel_type and run_parameters + .join(RunParameters, AdvisoryFuelStats.run_parameters == RunParameters.id) # Join RunParameters once + .join(CriticalHours, CriticalHours.run_parameters == RunParameters.id) # Join CriticalHours via RunParameters + .join(Shape, AdvisoryFuelStats.advisory_shape_id == Shape.id) # Join Shape .where( Shape.source_identifier == str(advisory_shape_id), RunParameters.run_type == run_type.value, RunParameters.run_datetime == run_datetime, RunParameters.for_date == for_date, ) - .order_by(AdvisoryFuelStats.fuel_type) - .order_by(AdvisoryFuelStats.threshold) ) + result = await session.execute(stmt) all_results = result.all() perf_end = perf_counter() diff --git a/api/app/routers/fba.py b/api/app/routers/fba.py index fb232cb68..9e83daf94 100644 --- a/api/app/routers/fba.py +++ b/api/app/routers/fba.py @@ -11,7 +11,7 @@ get_all_sfms_fuel_types, get_all_hfi_thresholds, get_hfi_area, - get_precomputed_high_hfi_fuel_type_areas_for_shape, + get_precomputed_stats_for_shape, get_provincial_rollup, get_run_datetimes, get_zonal_elevation_stats, @@ -21,6 +21,7 @@ ) from app.db.models.auto_spatial_advisory import RunTypeEnum from app.schemas.fba import ( + AdvisoryCriticalHours, ClassifiedHfiThresholdFuelTypeArea, FireCenterListResponse, FireShapeAreaListResponse, @@ -119,7 +120,7 @@ async def get_hfi_fuels_data_for_fire_zone(run_type: RunType, for_date: date, ru fuel_types = await get_all_sfms_fuel_types(session) # get HFI/fuels data for specific zone - hfi_fuel_type_ids_for_zone = await get_precomputed_high_hfi_fuel_type_areas_for_shape( + hfi_fuel_type_ids_for_zone = await get_precomputed_stats_for_shape( session, run_type=RunTypeEnum(run_type.value), for_date=for_date, run_datetime=run_datetime, advisory_shape_id=zone_id ) data = [] @@ -161,23 +162,22 @@ async def get_hfi_fuels_data_for_fire_centre(run_type: RunType, for_date: date, all_zone_data = {} for zone_id in zone_ids: # get HFI/fuels data for specific zone - hfi_fuel_type_ids_for_zone = await get_precomputed_high_hfi_fuel_type_areas_for_shape( + hfi_fuel_type_ids_for_zone = await get_precomputed_stats_for_shape( session, run_type=RunTypeEnum(run_type.value), for_date=for_date, run_datetime=run_datetime, advisory_shape_id=zone_id ) zone_data = [] - for record in hfi_fuel_type_ids_for_zone: - fuel_type_id = record[1] - threshold_id = record[2] + for shape_id, critical_hour_start, critical_hour_end, fuel_type_id, threshold_id, area, run_parameters in hfi_fuel_type_ids_for_zone: # area is stored in square metres in DB. For user convenience, convert to hectares # 1 ha = 10,000 sq.m. - area = record[3] / 10000 + area = area / 10000 fuel_type_obj = next((ft for ft in fuel_types if ft.fuel_type_id == fuel_type_id), None) threshold_obj = next((th for th in thresholds if th.id == threshold_id), None) zone_data.append( ClassifiedHfiThresholdFuelTypeArea( fuel_type=SFMSFuelType(fuel_type_id=fuel_type_obj.fuel_type_id, fuel_type_code=fuel_type_obj.fuel_type_code, description=fuel_type_obj.description), threshold=HfiThreshold(id=threshold_obj.id, name=threshold_obj.name, description=threshold_obj.description), + critical_hours=AdvisoryCriticalHours(start_time=critical_hour_start, end_time=critical_hour_end), area=area, ) ) diff --git a/api/app/schemas/fba.py b/api/app/schemas/fba.py index 4a3c60a82..19c864dc1 100644 --- a/api/app/schemas/fba.py +++ b/api/app/schemas/fba.py @@ -92,6 +92,13 @@ class SFMSFuelType(BaseModel): description: str +class AdvisoryCriticalHours(BaseModel): + """Critical Hours for an advisory.""" + + start_time: float + end_time: float + + class ClassifiedHfiThresholdFuelTypeArea(BaseModel): """Collection of data objects recording the area within an advisory shape that meets a particular HfiThreshold for a specific SFMSFuelType @@ -99,6 +106,7 @@ class ClassifiedHfiThresholdFuelTypeArea(BaseModel): fuel_type: SFMSFuelType threshold: HfiThreshold + critical_hours: AdvisoryCriticalHours area: float diff --git a/web/src/api/fbaAPI.ts b/web/src/api/fbaAPI.ts index 461f6137b..c004440b5 100644 --- a/web/src/api/fbaAPI.ts +++ b/web/src/api/fbaAPI.ts @@ -25,9 +25,16 @@ export interface FBAResponse { fire_centers: FireCenter[] } +export interface AdvisoryCriticalHours { + start_time: number + end_time: number +} + + export interface FireZoneThresholdFuelTypeArea { fuel_type: FuelType threshold: HfiThreshold + critical_hours: AdvisoryCriticalHours area: number } diff --git a/web/src/features/fba/components/viz/CriticalHours.tsx b/web/src/features/fba/components/viz/CriticalHours.tsx new file mode 100644 index 000000000..60df81b23 --- /dev/null +++ b/web/src/features/fba/components/viz/CriticalHours.tsx @@ -0,0 +1,20 @@ +import { Tooltip, Typography } from '@mui/material' +import React from 'react' +import { isUndefined } from 'lodash' + +interface CriticalHoursProps { + start?: number + end?: number +} + +const CriticalHours = ({ start, end }: CriticalHoursProps) => { + return ( + + + {isUndefined(start) || isUndefined(end) ? "-" : `${start} - ${end}`} + + + ) +} + +export default React.memo(CriticalHours) diff --git a/web/src/features/fba/components/viz/FuelSummary.tsx b/web/src/features/fba/components/viz/FuelSummary.tsx index c28c327ee..d6391c27d 100644 --- a/web/src/features/fba/components/viz/FuelSummary.tsx +++ b/web/src/features/fba/components/viz/FuelSummary.tsx @@ -6,11 +6,12 @@ import { DateTime } from 'luxon' import FuelDistribution from 'features/fba/components/viz/FuelDistribution' import { DataGridPro, GridColDef, GridColumnHeaderParams, GridRenderCellParams } from '@mui/x-data-grid-pro' import { styled, useTheme } from '@mui/material/styles' +import CriticalHours from 'features/fba/components/viz/CriticalHours' export interface FuelTypeInfoSummary { area: number - criticalHoursStart?: DateTime - criticalHoursEnd?: DateTime + criticalHoursStart?: number + criticalHoursEnd?: number id: number code: string description: string @@ -57,6 +58,18 @@ const columns: GridColDef[] = [ renderCell: (params: GridRenderCellParams) => { return } + }, + { + field: 'criticalHours', + flex: 3, + headerClassName: 'fuel-summary-header', + headerName: 'Critical Hours', + minWidth: 120, + sortable: false, + renderHeader: (params: GridColumnHeaderParams) => {params.colDef.headerName}, + renderCell: (params: GridRenderCellParams) => { + return + } } ] @@ -87,10 +100,14 @@ const FuelSummary = ({ fuelTypeInfo, selectedFireZoneUnit }: FuelSummaryProps) = if (groupedFuelDetail.length) { const area = groupedFuelDetail.reduce((acc, { area }) => acc + area, 0) const fuelType = groupedFuelDetail[0].fuel_type + const startTime = groupedFuelDetail[0].critical_hours.start_time + const endTime = groupedFuelDetail[0].critical_hours.end_time const fuelInfo: FuelTypeInfoSummary = { area, code: fuelType.fuel_type_code, description: fuelType.description, + criticalHoursStart: startTime, + criticalHoursEnd: endTime, id: fuelType.fuel_type_id, percent: totalHFIArea4K ? (area / totalHFIArea4K) * 100 : 0, selected: false From ac1491155f27164b1f96e98ed94747c3b44e73a3 Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Mon, 9 Sep 2024 16:18:43 -0700 Subject: [PATCH 29/42] Cleanup dead frontend code --- web/src/api/fbaAPI.ts | 11 ---- web/src/app/rootReducer.ts | 2 - .../features/fba/slices/hfiFuelTypesSlice.ts | 56 ------------------- 3 files changed, 69 deletions(-) delete mode 100644 web/src/features/fba/slices/hfiFuelTypesSlice.ts diff --git a/web/src/api/fbaAPI.ts b/web/src/api/fbaAPI.ts index c004440b5..05e8a87f0 100644 --- a/web/src/api/fbaAPI.ts +++ b/web/src/api/fbaAPI.ts @@ -149,17 +149,6 @@ export async function getAllRunDates(run_type: RunType, for_date: string): Promi return data } -export async function getHFIThresholdsFuelTypesForZone( - run_type: RunType, - for_date: string, - run_datetime: string, - zone_id: number -): Promise> { - const url = `fba/hfi-fuels/${run_type.toLowerCase()}/${for_date}/${run_datetime}/${zone_id}` - const { data } = await axios.get(url) - return data -} - export async function getHFIThresholdsFuelTypesForCentre( run_type: RunType, for_date: string, diff --git a/web/src/app/rootReducer.ts b/web/src/app/rootReducer.ts index f2a58fe82..86a82e0b7 100644 --- a/web/src/app/rootReducer.ts +++ b/web/src/app/rootReducer.ts @@ -13,7 +13,6 @@ import fireCentersSlice from 'commonSlices/fireCentersSlice' import fireShapeAreasSlice from 'features/fba/slices/fireZoneAreasSlice' import valueAtCoordinateSlice from 'features/fba/slices/valueAtCoordinateSlice' import runDatesSlice from 'features/fba/slices/runDatesSlice' -import hfiFuelTypesSlice from 'features/fba/slices/hfiFuelTypesSlice' import fireZoneElevationInfoSlice from 'features/fba/slices/fireZoneElevationInfoSlice' import fireZoneTPIStatsSlice from 'features/fba/slices/fireZoneTPIStatsSlice' import stationGroupsSlice from 'commonSlices/stationGroupsSlice' @@ -39,7 +38,6 @@ const rootReducer = combineReducers({ fireShapeAreas: fireShapeAreasSlice, runDates: runDatesSlice, valueAtCoordinate: valueAtCoordinateSlice, - hfiFuelTypes: hfiFuelTypesSlice, fireCentreHfiFuelTypes: fireCentreHfiFuelTypesSlice, fireZoneElevationInfo: fireZoneElevationInfoSlice, fireZoneTPIStats: fireZoneTPIStatsSlice, diff --git a/web/src/features/fba/slices/hfiFuelTypesSlice.ts b/web/src/features/fba/slices/hfiFuelTypesSlice.ts deleted file mode 100644 index a19266635..000000000 --- a/web/src/features/fba/slices/hfiFuelTypesSlice.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -import { AppThunk } from 'app/store' -import { logError } from 'utils/error' -import { FireZoneThresholdFuelTypeArea, getHFIThresholdsFuelTypesForZone } from 'api/fbaAPI' -import { RunType } from 'features/fba/pages/FireBehaviourAdvisoryPage' - -interface State { - loading: boolean - error: string | null - hfiThresholdsFuelTypes: Record -} - -const initialState: State = { - loading: false, - error: null, - hfiThresholdsFuelTypes: {} -} - -const hfiFuelTypesSlice = createSlice({ - name: 'hfiFuelTypes', - initialState, - reducers: { - getHFIFuelsStart(state: State) { - state.error = null - state.loading = true - state.hfiThresholdsFuelTypes = {} - }, - getHFIFuelsFailed(state: State, action: PayloadAction) { - state.error = action.payload - state.loading = false - }, - getHFIFuelsStartSuccess(state: State, action: PayloadAction>) { - state.error = null - state.hfiThresholdsFuelTypes = action.payload - state.loading = false - } - } -}) - -export const { getHFIFuelsStart, getHFIFuelsFailed, getHFIFuelsStartSuccess } = hfiFuelTypesSlice.actions - -export default hfiFuelTypesSlice.reducer - -export const fetchHighHFIFuels = - (runType: RunType, forDate: string, runDatetime: string, zoneID: number): AppThunk => - async dispatch => { - try { - dispatch(getHFIFuelsStart()) - const data = await getHFIThresholdsFuelTypesForZone(runType, forDate, runDatetime, zoneID) - dispatch(getHFIFuelsStartSuccess(data)) - } catch (err) { - dispatch(getHFIFuelsFailed((err as Error).toString())) - logError(err) - } - } From 7265891983679e31b96adb1ad8d362ea70baeaed Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Mon, 9 Sep 2024 16:42:09 -0700 Subject: [PATCH 30/42] make typescript happy --- web/src/app/rootReducer.ts | 4 -- web/src/commonSlices/fireCentersSlice.ts | 10 ++-- .../selectedStationGroupMembers.ts | 10 ++-- web/src/commonSlices/stationGroupsSlice.ts | 10 ++-- .../cHaines/slices/cHainesModelRunsSlice.tsx | 22 +++---- .../slices/cHainesPredictionsSlice.tsx | 10 ++-- .../fba/slices/fireCentreHfiFuelTypesSlice.ts | 10 ++-- .../fba/slices/fireCentreTPIStatsSlice.ts | 13 +++-- .../features/fba/slices/fireZoneAreasSlice.ts | 10 ++-- .../fba/slices/fireZoneElevationInfoSlice.ts | 13 +++-- .../fba/slices/fireZoneTPIStatsSlice.ts | 57 ------------------- .../features/fba/slices/hfiFuelTypesSlice.ts | 13 +++-- web/src/features/fba/slices/runDatesSlice.ts | 13 +++-- .../fba/slices/valueAtCoordinateSlice.ts | 10 ++-- .../slices/fbaCalculatorSlice.ts | 10 ++-- .../hfiCalculator/slices/stationsSlice.ts | 10 ++-- .../features/moreCast2/slices/dataSlice.ts | 10 ++-- .../moreCast2/slices/selectedStationsSlice.ts | 6 +- .../slices/percentilesSlice.ts | 2 +- .../features/stations/slices/stationsSlice.ts | 16 +++--- 20 files changed, 105 insertions(+), 154 deletions(-) delete mode 100644 web/src/features/fba/slices/fireZoneTPIStatsSlice.ts diff --git a/web/src/app/rootReducer.ts b/web/src/app/rootReducer.ts index f2a58fe82..5731ed03c 100644 --- a/web/src/app/rootReducer.ts +++ b/web/src/app/rootReducer.ts @@ -15,7 +15,6 @@ import valueAtCoordinateSlice from 'features/fba/slices/valueAtCoordinateSlice' import runDatesSlice from 'features/fba/slices/runDatesSlice' import hfiFuelTypesSlice from 'features/fba/slices/hfiFuelTypesSlice' import fireZoneElevationInfoSlice from 'features/fba/slices/fireZoneElevationInfoSlice' -import fireZoneTPIStatsSlice from 'features/fba/slices/fireZoneTPIStatsSlice' import stationGroupsSlice from 'commonSlices/stationGroupsSlice' import selectedStationGroupsMembersSlice from 'commonSlices/selectedStationGroupMembers' import dataSlice from 'features/moreCast2/slices/dataSlice' @@ -42,7 +41,6 @@ const rootReducer = combineReducers({ hfiFuelTypes: hfiFuelTypesSlice, fireCentreHfiFuelTypes: fireCentreHfiFuelTypesSlice, fireZoneElevationInfo: fireZoneElevationInfoSlice, - fireZoneTPIStats: fireZoneTPIStatsSlice, fireCentreTPIStats: fireCentreTPIStatsSlice, stationGroups: stationGroupsSlice, stationGroupsMembers: selectedStationGroupsMembersSlice, @@ -56,7 +54,6 @@ export type RootState = ReturnType export default rootReducer -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ export const selectPercentileStations = (state: RootState) => state.percentileStations export const selectHFIDailies = (state: RootState) => state.hfiCalculatorDailies export const selectFireWeatherStations = (state: RootState) => state.fireWeatherStations @@ -74,7 +71,6 @@ export const selectValueAtCoordinate = (state: RootState) => state.valueAtCoordi export const selectHFIFuelTypes = (state: RootState) => state.hfiFuelTypes export const selectFireCentreHFIFuelTypes = (state: RootState) => state.fireCentreHfiFuelTypes export const selectFireZoneElevationInfo = (state: RootState) => state.fireZoneElevationInfo -export const selectFireZoneTPIStats = (state: RootState) => state.fireZoneTPIStats export const selectFireCentreTPIStats = (state: RootState) => state.fireCentreTPIStats export const selectHFIDailiesLoading = (state: RootState): boolean => state.hfiCalculatorDailies.fireCentresLoading export const selectHFICalculatorState = (state: RootState): HFICalculatorState => state.hfiCalculatorDailies diff --git a/web/src/commonSlices/fireCentersSlice.ts b/web/src/commonSlices/fireCentersSlice.ts index cc71028e6..2e4f6ed00 100644 --- a/web/src/commonSlices/fireCentersSlice.ts +++ b/web/src/commonSlices/fireCentersSlice.ts @@ -4,13 +4,13 @@ import { AppThunk } from 'app/store' import { logError } from 'utils/error' import { FBAResponse, FireCenter, getFBAFireCenters } from 'api/fbaAPI' -interface State { +export interface FireCentresState { loading: boolean error: string | null fireCenters: FireCenter[] } -const initialState: State = { +const initialState: FireCentresState = { loading: false, error: null, fireCenters: [] @@ -20,16 +20,16 @@ const fireCentersSlice = createSlice({ name: 'fireCenters', initialState, reducers: { - getFireCentersStart(state: State) { + getFireCentersStart(state: FireCentresState) { state.error = null state.loading = true state.fireCenters = [] }, - getFireCentersFailed(state: State, action: PayloadAction) { + getFireCentersFailed(state: FireCentresState, action: PayloadAction) { state.error = action.payload state.loading = false }, - getFireCentersSuccess(state: State, action: PayloadAction) { + getFireCentersSuccess(state: FireCentresState, action: PayloadAction) { state.error = null state.fireCenters = action.payload.fire_centers state.loading = false diff --git a/web/src/commonSlices/selectedStationGroupMembers.ts b/web/src/commonSlices/selectedStationGroupMembers.ts index e7b09190c..a7c439b8a 100644 --- a/web/src/commonSlices/selectedStationGroupMembers.ts +++ b/web/src/commonSlices/selectedStationGroupMembers.ts @@ -4,13 +4,13 @@ import { AppThunk } from 'app/store' import { logError } from 'utils/error' import { getStationGroupsMembers, StationGroupMember } from 'api/stationAPI' -interface State { +export interface SelectedStationGroupState { loading: boolean error: string | null members: StationGroupMember[] } -export const initialState: State = { +export const initialState: SelectedStationGroupState = { loading: false, error: null, members: [] @@ -20,15 +20,15 @@ const selectedStationGroupsMembersSlice = createSlice({ name: 'selectedStationGroupsMembers', initialState, reducers: { - getStationGroupsMembersStart(state: State) { + getStationGroupsMembersStart(state: SelectedStationGroupState) { state.error = null state.loading = true }, - getStationGroupsMembersFailed(state: State, action: PayloadAction) { + getStationGroupsMembersFailed(state: SelectedStationGroupState, action: PayloadAction) { state.error = action.payload state.loading = false }, - getStationGroupsMembersSuccess(state: State, action: PayloadAction) { + getStationGroupsMembersSuccess(state: SelectedStationGroupState, action: PayloadAction) { state.error = null state.members = action.payload state.loading = false diff --git a/web/src/commonSlices/stationGroupsSlice.ts b/web/src/commonSlices/stationGroupsSlice.ts index 7e811f4e2..4c8159daa 100644 --- a/web/src/commonSlices/stationGroupsSlice.ts +++ b/web/src/commonSlices/stationGroupsSlice.ts @@ -4,13 +4,13 @@ import { AppThunk } from 'app/store' import { logError } from 'utils/error' import { getStationGroups, StationGroup } from 'api/stationAPI' -interface State { +export interface StationGroupsState { loading: boolean error: string | null groups: StationGroup[] } -const initialState: State = { +const initialState: StationGroupsState = { loading: false, error: null, groups: [] @@ -20,15 +20,15 @@ const stationGroupsSlice = createSlice({ name: 'stationGroups', initialState, reducers: { - getStationGroupsStart(state: State) { + getStationGroupsStart(state: StationGroupsState) { state.error = null state.loading = true }, - getStationGroupsFailed(state: State, action: PayloadAction) { + getStationGroupsFailed(state: StationGroupsState, action: PayloadAction) { state.error = action.payload state.loading = false }, - getStationGroupsSuccess(state: State, action: PayloadAction) { + getStationGroupsSuccess(state: StationGroupsState, action: PayloadAction) { state.error = null state.groups = action.payload state.loading = false diff --git a/web/src/features/cHaines/slices/cHainesModelRunsSlice.tsx b/web/src/features/cHaines/slices/cHainesModelRunsSlice.tsx index 558bfcfd0..840184724 100644 --- a/web/src/features/cHaines/slices/cHainesModelRunsSlice.tsx +++ b/web/src/features/cHaines/slices/cHainesModelRunsSlice.tsx @@ -4,7 +4,7 @@ import { AppThunk } from 'app/store' import { logError } from 'utils/error' import { FeatureCollection } from 'geojson' -interface State { +export interface CHainesModelState { loading: boolean error: string | null model_runs: ModelRun[] @@ -22,7 +22,7 @@ interface GeoJSONContext { result: FeatureCollection } -const initialState: State = { +const initialState: CHainesModelState = { loading: false, error: null, model_runs: [], @@ -37,11 +37,11 @@ const cHainesModelRunsSlice = createSlice({ name: 'c-haines-model-runs', initialState: initialState, reducers: { - getModelRunsStart(state: State) { + getModelRunsStart(state: CHainesModelState) { state.loading = true state.selected_prediction_timestamp = '' }, - getModelRunsSuccess(state: State, action: PayloadAction) { + getModelRunsSuccess(state: CHainesModelState, action: PayloadAction) { state.model_runs = action.payload.model_runs if (state.model_runs.length > 0) { state.selected_model_abbreviation = state.model_runs[0].model.abbrev @@ -56,11 +56,11 @@ const cHainesModelRunsSlice = createSlice({ state.loading = false state.error = null }, - getModelRunsFailed(state: State, action: PayloadAction) { + getModelRunsFailed(state: CHainesModelState, action: PayloadAction) { state.loading = false state.error = action.payload }, - setSelectedModel(state: State, action: PayloadAction) { + setSelectedModel(state: CHainesModelState, action: PayloadAction) { state.selected_model_abbreviation = action.payload const model_run = state.model_runs.find(instance => instance.model.abbrev === action.payload) if (model_run) { @@ -72,7 +72,7 @@ const cHainesModelRunsSlice = createSlice({ state.selected_model_run_timestamp = '' } }, - setSelectedModelRun(state: State, action: PayloadAction) { + setSelectedModelRun(state: CHainesModelState, action: PayloadAction) { state.selected_model_run_timestamp = action.payload const model_run = state.model_runs.find( instance => @@ -84,13 +84,13 @@ const cHainesModelRunsSlice = createSlice({ state.selected_prediction_timestamp = '' } }, - setSelectedPrediction(state: State, action: PayloadAction) { + setSelectedPrediction(state: CHainesModelState, action: PayloadAction) { state.selected_prediction_timestamp = action.payload }, - getPredictionStart(state: State) { + getPredictionStart(state: CHainesModelState) { state.loading = true }, - getPredictionSuccess(state: State, action: PayloadAction) { + getPredictionSuccess(state: CHainesModelState, action: PayloadAction) { if (!(action.payload.model in state.model_run_predictions)) { state.model_run_predictions[action.payload.model] = {} } @@ -101,7 +101,7 @@ const cHainesModelRunsSlice = createSlice({ action.payload.prediction_timestamp ] = action.payload.result }, - getPredictionFailed(state: State, action: PayloadAction) { + getPredictionFailed(state: CHainesModelState, action: PayloadAction) { state.loading = false state.error = action.payload } diff --git a/web/src/features/cHaines/slices/cHainesPredictionsSlice.tsx b/web/src/features/cHaines/slices/cHainesPredictionsSlice.tsx index 3b5cff60d..c8c1bd1bd 100644 --- a/web/src/features/cHaines/slices/cHainesPredictionsSlice.tsx +++ b/web/src/features/cHaines/slices/cHainesPredictionsSlice.tsx @@ -4,7 +4,7 @@ import { AppThunk } from 'app/store' import { logError } from 'utils/error' import { FeatureCollection } from 'geojson' -interface State { +export interface CHainesPredictionState { loading: boolean error: string | null model_runs: Record> @@ -16,7 +16,7 @@ interface GeoJSONContext { result: FeatureCollection } -const initialState: State = { +const initialState: CHainesPredictionState = { loading: false, error: null, model_runs: {} @@ -26,13 +26,13 @@ const cHainesPredictionsSlice = createSlice({ name: 'c-haines-predictions', initialState: initialState, reducers: { - getPredictionStart(state: State) { + getPredictionStart(state: CHainesPredictionState) { state.loading = true }, - getPredictionSuccess(state: State, action: PayloadAction) { + getPredictionSuccess(state: CHainesPredictionState, action: PayloadAction) { state.model_runs[action.payload.model_run_timestamp][action.payload.prediction_timestamp] = action.payload.result }, - getPredictionFailed(state: State, action: PayloadAction) { + getPredictionFailed(state: CHainesPredictionState, action: PayloadAction) { state.loading = false state.error = action.payload } diff --git a/web/src/features/fba/slices/fireCentreHfiFuelTypesSlice.ts b/web/src/features/fba/slices/fireCentreHfiFuelTypesSlice.ts index c0dc74897..4468b4a90 100644 --- a/web/src/features/fba/slices/fireCentreHfiFuelTypesSlice.ts +++ b/web/src/features/fba/slices/fireCentreHfiFuelTypesSlice.ts @@ -5,13 +5,13 @@ import { logError } from 'utils/error' import { FireCentreHfiFuelsData, getHFIThresholdsFuelTypesForCentre } from 'api/fbaAPI' import { RunType } from 'features/fba/pages/FireBehaviourAdvisoryPage' -interface State { +export interface CentreHFIFuelTypeState { loading: boolean error: string | null fireCentreHfiFuelTypes: FireCentreHfiFuelsData } -const initialState: State = { +const initialState: CentreHFIFuelTypeState = { loading: false, error: null, fireCentreHfiFuelTypes: {} @@ -21,16 +21,16 @@ const fireCentreHfiFuelTypesSlice = createSlice({ name: 'fireCentreHfiFuelTypes', initialState, reducers: { - getFireCentreHfiFuelTypesStart(state: State) { + getFireCentreHfiFuelTypesStart(state: CentreHFIFuelTypeState) { state.error = null state.fireCentreHfiFuelTypes = {} state.loading = true }, - getFireCentreHfiFuelTypesFailed(state: State, action: PayloadAction) { + getFireCentreHfiFuelTypesFailed(state: CentreHFIFuelTypeState, action: PayloadAction) { state.error = action.payload state.loading = false }, - getFireCentreHfiFuelTypesSuccess(state: State, action: PayloadAction) { + getFireCentreHfiFuelTypesSuccess(state: CentreHFIFuelTypeState, action: PayloadAction) { state.error = null state.fireCentreHfiFuelTypes = action.payload state.loading = false diff --git a/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts b/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts index 5f3059896..ab0c179e4 100644 --- a/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts +++ b/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts @@ -5,13 +5,13 @@ import { logError } from 'utils/error' import { FireZoneTPIStats, getFireCentreTPIStats } from 'api/fbaAPI' import { RunType } from 'features/fba/pages/FireBehaviourAdvisoryPage' -interface State { +export interface CentreTPIStatsState { loading: boolean error: string | null fireCentreTPIStats: Record | null } -const initialState: State = { +const initialState: CentreTPIStatsState = { loading: false, error: null, fireCentreTPIStats: null @@ -21,16 +21,19 @@ const fireCentreTPIStatsSlice = createSlice({ name: 'fireCentreTPIStats', initialState, reducers: { - getFireCentreTPIStatsStart(state: State) { + getFireCentreTPIStatsStart(state: CentreTPIStatsState) { state.error = null state.fireCentreTPIStats = null state.loading = true }, - getFireCentreTPIStatsFailed(state: State, action: PayloadAction) { + getFireCentreTPIStatsFailed(state: CentreTPIStatsState, action: PayloadAction) { state.error = action.payload state.loading = false }, - getFireCentreTPIStatsSuccess(state: State, action: PayloadAction>) { + getFireCentreTPIStatsSuccess( + state: CentreTPIStatsState, + action: PayloadAction> + ) { state.error = null state.fireCentreTPIStats = action.payload state.loading = false diff --git a/web/src/features/fba/slices/fireZoneAreasSlice.ts b/web/src/features/fba/slices/fireZoneAreasSlice.ts index 4d0f89b73..84e9d7ce7 100644 --- a/web/src/features/fba/slices/fireZoneAreasSlice.ts +++ b/web/src/features/fba/slices/fireZoneAreasSlice.ts @@ -6,13 +6,13 @@ import { FireShapeArea, FireShapeAreaListResponse, getFireShapeAreas } from 'api import { RunType } from 'features/fba/pages/FireBehaviourAdvisoryPage' import { isNull, isUndefined } from 'lodash' -interface State { +export interface FireZoneAreasState { loading: boolean error: string | null fireShapeAreas: FireShapeArea[] } -const initialState: State = { +const initialState: FireZoneAreasState = { loading: false, error: null, fireShapeAreas: [] @@ -22,16 +22,16 @@ const fireShapeAreasSlice = createSlice({ name: 'fireShapeAreas', initialState, reducers: { - getFireShapeAreasStart(state: State) { + getFireShapeAreasStart(state: FireZoneAreasState) { state.error = null state.loading = true state.fireShapeAreas = [] }, - getFireShapeAreasFailed(state: State, action: PayloadAction) { + getFireShapeAreasFailed(state: FireZoneAreasState, action: PayloadAction) { state.error = action.payload state.loading = false }, - getFireShapeAreasSuccess(state: State, action: PayloadAction) { + getFireShapeAreasSuccess(state: FireZoneAreasState, action: PayloadAction) { state.error = null state.fireShapeAreas = action.payload.shapes state.loading = false diff --git a/web/src/features/fba/slices/fireZoneElevationInfoSlice.ts b/web/src/features/fba/slices/fireZoneElevationInfoSlice.ts index 37be24350..bbde6e077 100644 --- a/web/src/features/fba/slices/fireZoneElevationInfoSlice.ts +++ b/web/src/features/fba/slices/fireZoneElevationInfoSlice.ts @@ -5,13 +5,13 @@ import { logError } from 'utils/error' import { ElevationInfoByThreshold, FireZoneElevationInfoResponse, getFireZoneElevationInfo } from 'api/fbaAPI' import { RunType } from 'features/fba/pages/FireBehaviourAdvisoryPage' -interface State { +export interface ZoneElevationInfoState { loading: boolean error: string | null fireZoneElevationInfo: ElevationInfoByThreshold[] } -const initialState: State = { +const initialState: ZoneElevationInfoState = { loading: false, error: null, fireZoneElevationInfo: [] @@ -21,16 +21,19 @@ const fireZoneElevationInfoSlice = createSlice({ name: 'fireZoneElevationInfo', initialState, reducers: { - getFireZoneElevationInfoStart(state: State) { + getFireZoneElevationInfoStart(state: ZoneElevationInfoState) { state.error = null state.fireZoneElevationInfo = [] state.loading = true }, - getFireZoneElevationInfoFailed(state: State, action: PayloadAction) { + getFireZoneElevationInfoFailed(state: ZoneElevationInfoState, action: PayloadAction) { state.error = action.payload state.loading = false }, - getFireZoneElevationInfoStartSuccess(state: State, action: PayloadAction) { + getFireZoneElevationInfoStartSuccess( + state: ZoneElevationInfoState, + action: PayloadAction + ) { state.error = null state.fireZoneElevationInfo = action.payload.hfi_elevation_info state.loading = false diff --git a/web/src/features/fba/slices/fireZoneTPIStatsSlice.ts b/web/src/features/fba/slices/fireZoneTPIStatsSlice.ts deleted file mode 100644 index abc1378f8..000000000 --- a/web/src/features/fba/slices/fireZoneTPIStatsSlice.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -import { AppThunk } from 'app/store' -import { logError } from 'utils/error' -import { FireZoneTPIStats, getFireZoneTPIStats } from 'api/fbaAPI' -import { RunType } from 'features/fba/pages/FireBehaviourAdvisoryPage' - -interface State { - loading: boolean - error: string | null - fireZoneTPIStats: FireZoneTPIStats | null -} - -const initialState: State = { - loading: false, - error: null, - fireZoneTPIStats: null -} - -const fireZoneTPIStatsSlice = createSlice({ - name: 'fireZoneTPIStats', - initialState, - reducers: { - getFireZoneTPIStatsStart(state: State) { - state.error = null - state.fireZoneTPIStats = null - state.loading = true - }, - getFireZoneTPIStatsFailed(state: State, action: PayloadAction) { - state.error = action.payload - state.loading = false - }, - getFireZoneTPIStatsSuccess(state: State, action: PayloadAction) { - state.error = null - state.fireZoneTPIStats = action.payload - state.loading = false - } - } -}) - -export const { getFireZoneTPIStatsStart, getFireZoneTPIStatsFailed, getFireZoneTPIStatsSuccess } = -fireZoneTPIStatsSlice.actions - -export default fireZoneTPIStatsSlice.reducer - -export const fetchfireZoneTPIStats = - (fire_zone_id: number, runType: RunType, forDate: string, runDatetime: string): AppThunk => - async dispatch => { - try { - dispatch(getFireZoneTPIStatsStart()) - const fireZoneTPIStats = await getFireZoneTPIStats(fire_zone_id, runType, forDate, runDatetime) - dispatch(getFireZoneTPIStatsSuccess(fireZoneTPIStats)) - } catch (err) { - dispatch(getFireZoneTPIStatsFailed((err as Error).toString())) - logError(err) - } - } diff --git a/web/src/features/fba/slices/hfiFuelTypesSlice.ts b/web/src/features/fba/slices/hfiFuelTypesSlice.ts index a19266635..35477d7ff 100644 --- a/web/src/features/fba/slices/hfiFuelTypesSlice.ts +++ b/web/src/features/fba/slices/hfiFuelTypesSlice.ts @@ -5,13 +5,13 @@ import { logError } from 'utils/error' import { FireZoneThresholdFuelTypeArea, getHFIThresholdsFuelTypesForZone } from 'api/fbaAPI' import { RunType } from 'features/fba/pages/FireBehaviourAdvisoryPage' -interface State { +export interface HFIFuelTypeState { loading: boolean error: string | null hfiThresholdsFuelTypes: Record } -const initialState: State = { +const initialState: HFIFuelTypeState = { loading: false, error: null, hfiThresholdsFuelTypes: {} @@ -21,16 +21,19 @@ const hfiFuelTypesSlice = createSlice({ name: 'hfiFuelTypes', initialState, reducers: { - getHFIFuelsStart(state: State) { + getHFIFuelsStart(state: HFIFuelTypeState) { state.error = null state.loading = true state.hfiThresholdsFuelTypes = {} }, - getHFIFuelsFailed(state: State, action: PayloadAction) { + getHFIFuelsFailed(state: HFIFuelTypeState, action: PayloadAction) { state.error = action.payload state.loading = false }, - getHFIFuelsStartSuccess(state: State, action: PayloadAction>) { + getHFIFuelsStartSuccess( + state: HFIFuelTypeState, + action: PayloadAction> + ) { state.error = null state.hfiThresholdsFuelTypes = action.payload state.loading = false diff --git a/web/src/features/fba/slices/runDatesSlice.ts b/web/src/features/fba/slices/runDatesSlice.ts index adb179037..6f462048b 100644 --- a/web/src/features/fba/slices/runDatesSlice.ts +++ b/web/src/features/fba/slices/runDatesSlice.ts @@ -6,14 +6,14 @@ import { getAllRunDates, getMostRecentRunDate } from 'api/fbaAPI' import { DateTime } from 'luxon' import { RunType } from 'features/fba/pages/FireBehaviourAdvisoryPage' -interface State { +export interface RunDateState { loading: boolean error: string | null runDates: DateTime[] mostRecentRunDate: string | null } -const initialState: State = { +const initialState: RunDateState = { loading: false, error: null, runDates: [], @@ -24,17 +24,20 @@ const runDatesSlice = createSlice({ name: 'runDates', initialState, reducers: { - getRunDatesStart(state: State) { + getRunDatesStart(state: RunDateState) { state.error = null state.loading = true state.runDates = [] state.mostRecentRunDate = null }, - getRunDatesFailed(state: State, action: PayloadAction) { + getRunDatesFailed(state: RunDateState, action: PayloadAction) { state.error = action.payload state.loading = false }, - getRunDatesSuccess(state: State, action: PayloadAction<{ runDates: DateTime[]; mostRecentRunDate: string }>) { + getRunDatesSuccess( + state: RunDateState, + action: PayloadAction<{ runDates: DateTime[]; mostRecentRunDate: string }> + ) { state.error = null state.runDates = action.payload.runDates state.mostRecentRunDate = action.payload.mostRecentRunDate diff --git a/web/src/features/fba/slices/valueAtCoordinateSlice.ts b/web/src/features/fba/slices/valueAtCoordinateSlice.ts index adc69f063..e04affccf 100644 --- a/web/src/features/fba/slices/valueAtCoordinateSlice.ts +++ b/web/src/features/fba/slices/valueAtCoordinateSlice.ts @@ -8,13 +8,13 @@ export interface IValueAtCoordinate { value: string | undefined description: string } -interface State { +export interface ValueAtCoordState { loading: boolean error: string | null values: IValueAtCoordinate[] } -const initialState: State = { +const initialState: ValueAtCoordState = { loading: false, error: null, values: [] @@ -24,16 +24,16 @@ const valueAtCoordinateSlice = createSlice({ name: 'valueAtCoordinate', initialState, reducers: { - getValueAtCoordinateStart(state: State) { + getValueAtCoordinateStart(state: ValueAtCoordState) { state.error = null state.loading = true state.values = [] }, - getValueAtCoordinateFailed(state: State, action: PayloadAction) { + getValueAtCoordinateFailed(state: ValueAtCoordState, action: PayloadAction) { state.error = action.payload state.loading = false }, - getValueAtCoordinateSuccess(state: State, action: PayloadAction) { + getValueAtCoordinateSuccess(state: ValueAtCoordState, action: PayloadAction) { state.error = null state.values = action.payload state.loading = false diff --git a/web/src/features/fbaCalculator/slices/fbaCalculatorSlice.ts b/web/src/features/fbaCalculator/slices/fbaCalculatorSlice.ts index 0a2f9115d..bfa19f65d 100644 --- a/web/src/features/fbaCalculator/slices/fbaCalculatorSlice.ts +++ b/web/src/features/fbaCalculator/slices/fbaCalculatorSlice.ts @@ -10,14 +10,14 @@ import { DateTime } from 'luxon' import { PST_UTC_OFFSET } from 'utils/constants' import { pstFormatter } from 'utils/date' -interface State { +export interface FBACalcState { loading: boolean error: string | null fireBehaviourResultStations: FBAStation[] date: string | null } -const initialState: State = { +const initialState: FBACalcState = { loading: false, error: null, fireBehaviourResultStations: [], @@ -28,17 +28,17 @@ const fireBehaviourStationsSlice = createSlice({ name: 'fireBehaviourStations', initialState, reducers: { - getFireBehaviourStationsStart(state: State) { + getFireBehaviourStationsStart(state: FBACalcState) { state.error = null state.loading = true state.fireBehaviourResultStations = [] state.date = null }, - getFireBehaviourStationsFailed(state: State, action: PayloadAction) { + getFireBehaviourStationsFailed(state: FBACalcState, action: PayloadAction) { state.error = action.payload state.loading = false }, - getFireBehaviourStationsSuccess(state: State, action: PayloadAction) { + getFireBehaviourStationsSuccess(state: FBACalcState, action: PayloadAction) { state.error = null state.fireBehaviourResultStations = action.payload.stations state.date = DateTime.fromFormat(action.payload.date, 'yyyy/MM/dd') diff --git a/web/src/features/hfiCalculator/slices/stationsSlice.ts b/web/src/features/hfiCalculator/slices/stationsSlice.ts index f8c65d710..f1ad5a874 100644 --- a/web/src/features/hfiCalculator/slices/stationsSlice.ts +++ b/web/src/features/hfiCalculator/slices/stationsSlice.ts @@ -4,13 +4,13 @@ import { FireCentre, getHFIStations, HFIWeatherStationsResponse } from 'api/hfiC import { AppThunk } from 'app/store' import { logError } from 'utils/error' -interface State { +export interface StationsState { loading: boolean error: string | null fireCentres: FireCentre[] } -const initialState: State = { +const initialState: StationsState = { loading: false, error: null, fireCentres: [] @@ -20,16 +20,16 @@ const stationsSlice = createSlice({ name: 'hfiStations', initialState, reducers: { - getHFIStationsStart(state: State) { + getHFIStationsStart(state: StationsState) { state.error = null state.loading = true state.fireCentres = [] }, - getHFIStationsFailed(state: State, action: PayloadAction) { + getHFIStationsFailed(state: StationsState, action: PayloadAction) { state.error = action.payload state.loading = false }, - getHFIStationsSuccess(state: State, action: PayloadAction) { + getHFIStationsSuccess(state: StationsState, action: PayloadAction) { state.error = null state.fireCentres = action.payload.fire_centres state.loading = false diff --git a/web/src/features/moreCast2/slices/dataSlice.ts b/web/src/features/moreCast2/slices/dataSlice.ts index 15aa369ea..7e0258b42 100644 --- a/web/src/features/moreCast2/slices/dataSlice.ts +++ b/web/src/features/moreCast2/slices/dataSlice.ts @@ -25,7 +25,7 @@ import { StationGroupMember } from 'api/stationAPI' import { MorecastDraftForecast } from 'features/moreCast2/forecastDraft' const morecastDraftForecast = new MorecastDraftForecast(localStorage) -interface State { +export interface DataState { loading: boolean error: string | null actuals: WeatherIndeterminate[] @@ -34,7 +34,7 @@ interface State { predictions: WeatherIndeterminate[] } -export const initialState: State = { +export const initialState: DataState = { loading: false, error: null, actuals: [], @@ -47,7 +47,7 @@ const dataSlice = createSlice({ name: 'DataSlice', initialState, reducers: { - getWeatherIndeterminatesStart(state: State) { + getWeatherIndeterminatesStart(state: DataState) { state.error = null state.actuals = [] state.forecasts = [] @@ -55,11 +55,11 @@ const dataSlice = createSlice({ state.predictions = [] state.loading = true }, - getWeatherIndeterminatesFailed(state: State, action: PayloadAction) { + getWeatherIndeterminatesFailed(state: DataState, action: PayloadAction) { state.error = action.payload state.loading = false }, - getWeatherIndeterminatesSuccess(state: State, action: PayloadAction) { + getWeatherIndeterminatesSuccess(state: DataState, action: PayloadAction) { state.error = null state.actuals = action.payload.actuals state.forecasts = action.payload.forecasts diff --git a/web/src/features/moreCast2/slices/selectedStationsSlice.ts b/web/src/features/moreCast2/slices/selectedStationsSlice.ts index 3ff149370..45c32e1b4 100644 --- a/web/src/features/moreCast2/slices/selectedStationsSlice.ts +++ b/web/src/features/moreCast2/slices/selectedStationsSlice.ts @@ -2,11 +2,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { StationGroupMember } from 'api/stationAPI' import { RootState } from 'app/rootReducer' -interface State { +export interface SelectedStationState { selectedStations: StationGroupMember[] } -export const initialState: State = { +export const initialState: SelectedStationState = { selectedStations: [] } @@ -14,7 +14,7 @@ const selectedStationsSlice = createSlice({ name: 'selectedStationsSlice', initialState, reducers: { - selectedStationsChanged(state: State, action: PayloadAction) { + selectedStationsChanged(state: SelectedStationState, action: PayloadAction) { state.selectedStations = action.payload } } diff --git a/web/src/features/percentileCalculator/slices/percentilesSlice.ts b/web/src/features/percentileCalculator/slices/percentilesSlice.ts index d88361e9a..05bda1ad5 100644 --- a/web/src/features/percentileCalculator/slices/percentilesSlice.ts +++ b/web/src/features/percentileCalculator/slices/percentilesSlice.ts @@ -4,7 +4,7 @@ import { getPercentiles, PercentilesResponse, YearRange } from 'api/percentileAP import { AppThunk } from 'app/store' import { logError } from 'utils/error' -interface PercentilesState { +export interface PercentilesState { loading: boolean error: string | null result: PercentilesResponse | null diff --git a/web/src/features/stations/slices/stationsSlice.ts b/web/src/features/stations/slices/stationsSlice.ts index 5cbf14b38..e4cff9a0a 100644 --- a/web/src/features/stations/slices/stationsSlice.ts +++ b/web/src/features/stations/slices/stationsSlice.ts @@ -3,7 +3,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { StationSource, DetailedGeoJsonStation, GeoJsonStation } from 'api/stationAPI' import { AppThunk } from 'app/store' import { logError } from 'utils/error' -interface State { +export interface StationsState { loading: boolean error: string | null stations: GeoJsonStation[] | DetailedGeoJsonStation[] @@ -12,7 +12,7 @@ interface State { codesOfRetrievedStationData: number[] } -const initialState: State = { +const initialState: StationsState = { loading: false, error: null, stations: [], @@ -25,30 +25,30 @@ const stationsSlice = createSlice({ name: 'stations', initialState, reducers: { - getStationsStart(state: State) { + getStationsStart(state: StationsState) { state.loading = true }, - getStationsFailed(state: State, action: PayloadAction) { + getStationsFailed(state: StationsState, action: PayloadAction) { state.loading = false state.error = action.payload }, - getStationsSuccess(state: State, action: PayloadAction) { + getStationsSuccess(state: StationsState, action: PayloadAction) { state.error = null state.stations = action.payload - const stationsByCode: State['stationsByCode'] = {} + const stationsByCode: StationsState['stationsByCode'] = {} action.payload.forEach(station => { stationsByCode[station.properties.code] = station }) state.stationsByCode = stationsByCode state.loading = false }, - selectStation(state: State, action: PayloadAction) { + selectStation(state: StationsState, action: PayloadAction) { const selectedStationsList = state.selectedStationsByCode selectedStationsList.push(action.payload) const selectedStationsSet = new Set(selectedStationsList) state.selectedStationsByCode = Array.from(selectedStationsSet.values()) }, - selectStations(state: State, action: PayloadAction) { + selectStations(state: StationsState, action: PayloadAction) { state.selectedStationsByCode = [] state.selectedStationsByCode = action.payload } From ba6275a3d2e0803cae1a1a380202f7e1401221bf Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Mon, 9 Sep 2024 16:52:49 -0700 Subject: [PATCH 31/42] Endpoint tests --- .vscode/settings.json | 1 + api/app/db/crud/auto_spatial_advisory.py | 2 - api/app/routers/fba.py | 40 +------------ api/app/tests/fba/test_fba_endpoint.py | 45 +++++++++------ web/src/app/rootReducer.ts | 2 - .../fba/slices/fireZoneElevationInfoSlice.ts | 57 ------------------- 6 files changed, 31 insertions(+), 116 deletions(-) delete mode 100644 web/src/features/fba/slices/fireZoneElevationInfoSlice.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a1c2c7a4..b9b3fc4c2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -87,6 +87,7 @@ "HRDPS", "idir", "Indeterminates", + "Kamloops", "luxon", "maxx", "maxy", diff --git a/api/app/db/crud/auto_spatial_advisory.py b/api/app/db/crud/auto_spatial_advisory.py index 4694ff06b..eca443cdd 100644 --- a/api/app/db/crud/auto_spatial_advisory.py +++ b/api/app/db/crud/auto_spatial_advisory.py @@ -158,13 +158,11 @@ async def get_precomputed_stats_for_shape(session: AsyncSession, run_type: RunTy perf_start = perf_counter() stmt = ( select( - AdvisoryFuelStats.advisory_shape_id, CriticalHours.start_hour, CriticalHours.end_hour, AdvisoryFuelStats.fuel_type, AdvisoryFuelStats.threshold, AdvisoryFuelStats.area, - AdvisoryFuelStats.run_parameters, ) .distinct(AdvisoryFuelStats.fuel_type, AdvisoryFuelStats.run_parameters) # Keep unique records by fuel_type and run_parameters .join(RunParameters, AdvisoryFuelStats.run_parameters == RunParameters.id) # Join RunParameters once diff --git a/api/app/routers/fba.py b/api/app/routers/fba.py index 9e83daf94..b3ae4df2c 100644 --- a/api/app/routers/fba.py +++ b/api/app/routers/fba.py @@ -106,44 +106,6 @@ async def get_provincial_summary(run_type: RunType, run_datetime: datetime, for_ return ProvincialSummaryResponse(provincial_summary=fire_shape_area_details) -@router.get("/hfi-fuels/{run_type}/{for_date}/{run_datetime}/{zone_id}", response_model=dict[int, List[ClassifiedHfiThresholdFuelTypeArea]]) -async def get_hfi_fuels_data_for_fire_zone(run_type: RunType, for_date: date, run_datetime: datetime, zone_id: int): - """ - Fetch rollup of fuel type/HFI threshold/area data for a specified fire zone. - """ - logger.info("hfi-fuels/%s/%s/%s/%s", run_type.value, for_date, run_datetime, zone_id) - - async with get_async_read_session_scope() as session: - # get thresholds data - thresholds = await get_all_hfi_thresholds(session) - # get fuel type ids data - fuel_types = await get_all_sfms_fuel_types(session) - - # get HFI/fuels data for specific zone - hfi_fuel_type_ids_for_zone = await get_precomputed_stats_for_shape( - session, run_type=RunTypeEnum(run_type.value), for_date=for_date, run_datetime=run_datetime, advisory_shape_id=zone_id - ) - data = [] - - for record in hfi_fuel_type_ids_for_zone: - fuel_type_id = record[1] - threshold_id = record[2] - # area is stored in square metres in DB. For user convenience, convert to hectares - # 1 ha = 10,000 sq.m. - area = record[3] / 10000 - fuel_type_obj = next((ft for ft in fuel_types if ft.fuel_type_id == fuel_type_id), None) - threshold_obj = next((th for th in thresholds if th.id == threshold_id), None) - data.append( - ClassifiedHfiThresholdFuelTypeArea( - fuel_type=SFMSFuelType(fuel_type_id=fuel_type_obj.fuel_type_id, fuel_type_code=fuel_type_obj.fuel_type_code, description=fuel_type_obj.description), - threshold=HfiThreshold(id=threshold_obj.id, name=threshold_obj.name, description=threshold_obj.description), - area=area, - ) - ) - - return {zone_id: data} - - @router.get("/fire-centre-hfi-fuels/{run_type}/{for_date}/{run_datetime}/{fire_centre_name}", response_model=dict[str, dict[int, List[ClassifiedHfiThresholdFuelTypeArea]]]) async def get_hfi_fuels_data_for_fire_centre(run_type: RunType, for_date: date, run_datetime: datetime, fire_centre_name: str): """ @@ -167,7 +129,7 @@ async def get_hfi_fuels_data_for_fire_centre(run_type: RunType, for_date: date, ) zone_data = [] - for shape_id, critical_hour_start, critical_hour_end, fuel_type_id, threshold_id, area, run_parameters in hfi_fuel_type_ids_for_zone: + for critical_hour_start, critical_hour_end, fuel_type_id, threshold_id, area in hfi_fuel_type_ids_for_zone: # area is stored in square metres in DB. For user convenience, convert to hectares # 1 ha = 10,000 sq.m. area = area / 10000 diff --git a/api/app/tests/fba/test_fba_endpoint.py b/api/app/tests/fba/test_fba_endpoint.py index 191d7d984..5d0f8c0c5 100644 --- a/api/app/tests/fba/test_fba_endpoint.py +++ b/api/app/tests/fba/test_fba_endpoint.py @@ -3,18 +3,18 @@ import pytest from fastapi.testclient import TestClient from datetime import date, datetime, timezone -from app.db.models.auto_spatial_advisory import AdvisoryElevationStats, AdvisoryTPIStats, RunParameters +from app.db.models.auto_spatial_advisory import AdvisoryTPIStats, HfiClassificationThreshold, RunParameters, SFMSFuelType get_fire_centres_url = "/api/fba/fire-centers" get_fire_zone_areas_url = "/api/fba/fire-shape-areas/forecast/2022-09-27/2022-09-27" get_fire_zone_tpi_stats_url = "/api/fba/fire-zone-tpi-stats/forecast/2022-09-27/2022-09-27/1" -get_fire_zone_elevation_info_url = "/api/fba/fire-zone-elevation-info/forecast/2022-09-27/2022-09-27/1" +get_fire_centre_info_url = "/api/fba/fire-centre-hfi-fuels/forecast/2022-09-27/2022-09-27/Kamloops%20Fire%20Centre" get_sfms_run_datetimes_url = "/api/fba/sfms-run-datetimes/forecast/2022-09-27" decode_fn = "jwt.decode" mock_tpi_stats = AdvisoryTPIStats(id=1, advisory_shape_id=1, valley_bottom=1, mid_slope=2, upper_slope=3, pixel_size_metres=50) -mock_elevation_info = [AdvisoryElevationStats(id=1, advisory_shape_id=1, threshold=1, minimum=1.0, quartile_25=2.0, median=3.0, quartile_75=4.0, maximum=5.0)] +mock_fire_centre_info = [(9.0, 11.0, 1, 1, 50)] mock_sfms_run_datetimes = [ RunParameters(id=1, run_type="forecast", run_datetime=datetime(year=2024, month=1, day=1, hour=1, tzinfo=timezone.utc), for_date=date(year=2024, month=1, day=2)) ] @@ -36,8 +36,8 @@ async def mock_get_tpi_stats(*_, **__): return mock_tpi_stats -async def mock_get_elevation_info(*_, **__): - return mock_elevation_info +async def mock_get_fire_centre_info(*_, **__): + return mock_fire_centre_info async def mock_get_sfms_run_datetimes(*_, **__): @@ -54,7 +54,7 @@ def client(): @pytest.mark.parametrize( "endpoint", - [get_fire_centres_url, get_fire_zone_areas_url, get_fire_zone_tpi_stats_url, get_fire_zone_elevation_info_url, get_sfms_run_datetimes_url], + [get_fire_centres_url, get_fire_zone_areas_url, get_fire_zone_tpi_stats_url, get_fire_centre_info_url, get_sfms_run_datetimes_url], ) def test_get_endpoints_unauthorized(client: TestClient, endpoint: str): """Forbidden to get fire zone areas when unauthorized""" @@ -72,19 +72,32 @@ def test_get_fire_centres_authorized(client: TestClient): assert response.status_code == 200 +async def mock_hfi_thresholds(*_, **__): + return [HfiClassificationThreshold(id=1, description="4000 < hfi < 10000", name="advisory")] + + +async def mock_sfms_fuel_types(*_, **__): + return [SFMSFuelType(id=1, fuel_type_id=1, fuel_type_code="C2", description="test fuel type c2")] + + +async def mock_zone_ids_in_centre(*_, **__): + return [1] + + @patch("app.routers.fba.get_auth_header", mock_get_auth_header) -@patch("app.routers.fba.get_zonal_elevation_stats", mock_get_elevation_info) +@patch("app.routers.fba.get_precomputed_stats_for_shape", mock_get_fire_centre_info) +@patch("app.routers.fba.get_all_hfi_thresholds", mock_hfi_thresholds) +@patch("app.routers.fba.get_all_sfms_fuel_types", mock_sfms_fuel_types) +@patch("app.routers.fba.get_zone_ids_in_centre", mock_zone_ids_in_centre) @pytest.mark.usefixtures("mock_jwt_decode") -def test_get_fire_zone_elevation_info_authorized(client: TestClient): - """Allowed to get fire zone elevation info when authorized""" - response = client.get(get_fire_zone_elevation_info_url) +def test_get_fire_center_info_authorized(client: TestClient): + """Allowed to get fire centre info when authorized""" + response = client.get(get_fire_centre_info_url) assert response.status_code == 200 - assert response.json()["hfi_elevation_info"][0]["threshold"] == mock_elevation_info[0].threshold - assert response.json()["hfi_elevation_info"][0]["elevation_info"]["minimum"] == mock_elevation_info[0].minimum - assert response.json()["hfi_elevation_info"][0]["elevation_info"]["quartile_25"] == mock_elevation_info[0].quartile_25 - assert response.json()["hfi_elevation_info"][0]["elevation_info"]["median"] == mock_elevation_info[0].median - assert response.json()["hfi_elevation_info"][0]["elevation_info"]["quartile_75"] == mock_elevation_info[0].quartile_75 - assert response.json()["hfi_elevation_info"][0]["elevation_info"]["maximum"] == mock_elevation_info[0].maximum + assert response.json()["Kamloops Fire Centre"]["1"][0]["fuel_type"]["fuel_type_id"] == 1 + assert response.json()["Kamloops Fire Centre"]["1"][0]["threshold"]["id"] == 1 + assert response.json()["Kamloops Fire Centre"]["1"][0]["critical_hours"]["start_time"] == 9.0 + assert response.json()["Kamloops Fire Centre"]["1"][0]["critical_hours"]["end_time"] == 11.0 @patch("app.routers.fba.get_auth_header", mock_get_auth_header) diff --git a/web/src/app/rootReducer.ts b/web/src/app/rootReducer.ts index 86a82e0b7..c2066f37c 100644 --- a/web/src/app/rootReducer.ts +++ b/web/src/app/rootReducer.ts @@ -13,7 +13,6 @@ import fireCentersSlice from 'commonSlices/fireCentersSlice' import fireShapeAreasSlice from 'features/fba/slices/fireZoneAreasSlice' import valueAtCoordinateSlice from 'features/fba/slices/valueAtCoordinateSlice' import runDatesSlice from 'features/fba/slices/runDatesSlice' -import fireZoneElevationInfoSlice from 'features/fba/slices/fireZoneElevationInfoSlice' import fireZoneTPIStatsSlice from 'features/fba/slices/fireZoneTPIStatsSlice' import stationGroupsSlice from 'commonSlices/stationGroupsSlice' import selectedStationGroupsMembersSlice from 'commonSlices/selectedStationGroupMembers' @@ -39,7 +38,6 @@ const rootReducer = combineReducers({ runDates: runDatesSlice, valueAtCoordinate: valueAtCoordinateSlice, fireCentreHfiFuelTypes: fireCentreHfiFuelTypesSlice, - fireZoneElevationInfo: fireZoneElevationInfoSlice, fireZoneTPIStats: fireZoneTPIStatsSlice, fireCentreTPIStats: fireCentreTPIStatsSlice, stationGroups: stationGroupsSlice, diff --git a/web/src/features/fba/slices/fireZoneElevationInfoSlice.ts b/web/src/features/fba/slices/fireZoneElevationInfoSlice.ts deleted file mode 100644 index 37be24350..000000000 --- a/web/src/features/fba/slices/fireZoneElevationInfoSlice.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -import { AppThunk } from 'app/store' -import { logError } from 'utils/error' -import { ElevationInfoByThreshold, FireZoneElevationInfoResponse, getFireZoneElevationInfo } from 'api/fbaAPI' -import { RunType } from 'features/fba/pages/FireBehaviourAdvisoryPage' - -interface State { - loading: boolean - error: string | null - fireZoneElevationInfo: ElevationInfoByThreshold[] -} - -const initialState: State = { - loading: false, - error: null, - fireZoneElevationInfo: [] -} - -const fireZoneElevationInfoSlice = createSlice({ - name: 'fireZoneElevationInfo', - initialState, - reducers: { - getFireZoneElevationInfoStart(state: State) { - state.error = null - state.fireZoneElevationInfo = [] - state.loading = true - }, - getFireZoneElevationInfoFailed(state: State, action: PayloadAction) { - state.error = action.payload - state.loading = false - }, - getFireZoneElevationInfoStartSuccess(state: State, action: PayloadAction) { - state.error = null - state.fireZoneElevationInfo = action.payload.hfi_elevation_info - state.loading = false - } - } -}) - -export const { getFireZoneElevationInfoStart, getFireZoneElevationInfoFailed, getFireZoneElevationInfoStartSuccess } = - fireZoneElevationInfoSlice.actions - -export default fireZoneElevationInfoSlice.reducer - -export const fetchfireZoneElevationInfo = - (fire_zone_id: number, runType: RunType, forDate: string, runDatetime: string): AppThunk => - async dispatch => { - try { - dispatch(getFireZoneElevationInfoStart()) - const fireZoneElevationInfo = await getFireZoneElevationInfo(fire_zone_id, runType, forDate, runDatetime) - dispatch(getFireZoneElevationInfoStartSuccess(fireZoneElevationInfo)) - } catch (err) { - dispatch(getFireZoneElevationInfoFailed((err as Error).toString())) - logError(err) - } - } From a99d112ed6794d5942a32aed8c2d7b26c2e19b57 Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Mon, 9 Sep 2024 17:02:56 -0700 Subject: [PATCH 32/42] Remove unused selectors --- web/src/app/rootReducer.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/src/app/rootReducer.ts b/web/src/app/rootReducer.ts index 630371950..9982d1fcf 100644 --- a/web/src/app/rootReducer.ts +++ b/web/src/app/rootReducer.ts @@ -64,9 +64,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 selectHFIFuelTypes = (state: RootState) => state.hfiFuelTypes export const selectFireCentreHFIFuelTypes = (state: RootState) => state.fireCentreHfiFuelTypes -export const selectFireZoneElevationInfo = (state: RootState) => state.fireZoneElevationInfo export const selectFireCentreTPIStats = (state: RootState) => state.fireCentreTPIStats export const selectHFIDailiesLoading = (state: RootState): boolean => state.hfiCalculatorDailies.fireCentresLoading export const selectHFICalculatorState = (state: RootState): HFICalculatorState => state.hfiCalculatorDailies From 96acbf71af6783dbc57cc86e3b4cccb8f760c968 Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Mon, 9 Sep 2024 17:08:41 -0700 Subject: [PATCH 33/42] Fix test mock --- .../fba/components/infoPanel/fireZoneUnitTabs.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx b/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx index 6b6067dea..224ce64e2 100644 --- a/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx +++ b/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx @@ -59,7 +59,8 @@ const mockFireCentreHfiFuelTypes: FireCentreHfiFuelsData = { { fuel_type: { fuel_type_id: 1, fuel_type_code: 'C', description: 'fuel type' }, area: 10, - threshold: { id: 1, name: 'threshold', description: 'description' } + threshold: { id: 1, name: 'threshold', description: 'description' }, + critical_hours: { start_time: 9, end_time: 11} } ] } From 816504fc8d4ad07866b8b0da8c6f5bf4602ee54b Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Mon, 9 Sep 2024 17:10:35 -0700 Subject: [PATCH 34/42] Cleanup logging and comments --- api/app/db/crud/auto_spatial_advisory.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api/app/db/crud/auto_spatial_advisory.py b/api/app/db/crud/auto_spatial_advisory.py index eca443cdd..a54750a85 100644 --- a/api/app/db/crud/auto_spatial_advisory.py +++ b/api/app/db/crud/auto_spatial_advisory.py @@ -164,10 +164,10 @@ async def get_precomputed_stats_for_shape(session: AsyncSession, run_type: RunTy AdvisoryFuelStats.threshold, AdvisoryFuelStats.area, ) - .distinct(AdvisoryFuelStats.fuel_type, AdvisoryFuelStats.run_parameters) # Keep unique records by fuel_type and run_parameters - .join(RunParameters, AdvisoryFuelStats.run_parameters == RunParameters.id) # Join RunParameters once - .join(CriticalHours, CriticalHours.run_parameters == RunParameters.id) # Join CriticalHours via RunParameters - .join(Shape, AdvisoryFuelStats.advisory_shape_id == Shape.id) # Join Shape + .distinct(AdvisoryFuelStats.fuel_type, AdvisoryFuelStats.run_parameters) + .join(RunParameters, AdvisoryFuelStats.run_parameters == RunParameters.id) + .join(CriticalHours, CriticalHours.run_parameters == RunParameters.id) + .join(Shape, AdvisoryFuelStats.advisory_shape_id == Shape.id) .where( Shape.source_identifier == str(advisory_shape_id), RunParameters.run_type == run_type.value, @@ -180,7 +180,7 @@ async def get_precomputed_stats_for_shape(session: AsyncSession, run_type: RunTy all_results = result.all() perf_end = perf_counter() delta = perf_end - perf_start - logger.info("%f delta count before and after fuel types/high hfi/zone query", delta) + logger.info("%f delta count before and after advisory stats query", delta) return all_results From dec76bf0be724a2f2b8852b930709c78b6844ac5 Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Mon, 9 Sep 2024 17:29:33 -0700 Subject: [PATCH 35/42] Delete unused endpoint --- api/app/routers/fba.py | 13 ------------- web/src/api/fbaAPI.ts | 11 ----------- 2 files changed, 24 deletions(-) diff --git a/api/app/routers/fba.py b/api/app/routers/fba.py index b3ae4df2c..52edc54b0 100644 --- a/api/app/routers/fba.py +++ b/api/app/routers/fba.py @@ -163,19 +163,6 @@ async def get_run_datetimes_for_date_and_runtype(run_type: RunType, for_date: da return datetimes -@router.get("/fire-zone-elevation-info/{run_type}/{for_date}/{run_datetime}/{fire_zone_id}", response_model=FireZoneElevationStatsListResponse) -async def get_fire_zone_elevation_stats(fire_zone_id: int, run_type: RunType, run_datetime: datetime, for_date: date, _=Depends(authentication_required)): - """Return the elevation statistics for each advisory threshold""" - async with get_async_read_session_scope() as session: - data = [] - rows = await get_zonal_elevation_stats(session, fire_zone_id, run_type, run_datetime, for_date) - for row in rows: - stats = FireZoneElevationStats(minimum=row.minimum, quartile_25=row.quartile_25, median=row.median, quartile_75=row.quartile_75, maximum=row.maximum) - stats_by_threshold = FireZoneElevationStatsByThreshold(threshold=row.threshold, elevation_info=stats) - data.append(stats_by_threshold) - return FireZoneElevationStatsListResponse(hfi_elevation_info=data) - - @router.get("/fire-zone-tpi-stats/{run_type}/{for_date}/{run_datetime}/{fire_zone_id}", response_model=FireZoneTPIStats) async def get_fire_zone_tpi_stats(fire_zone_id: int, run_type: RunType, run_datetime: datetime, for_date: date, _=Depends(authentication_required)): """Return the elevation TPI statistics for each advisory threshold""" diff --git a/web/src/api/fbaAPI.ts b/web/src/api/fbaAPI.ts index 05e8a87f0..dce338376 100644 --- a/web/src/api/fbaAPI.ts +++ b/web/src/api/fbaAPI.ts @@ -160,17 +160,6 @@ export async function getHFIThresholdsFuelTypesForCentre( return data } -export async function getFireZoneElevationInfo( - fire_zone_id: number, - run_type: RunType, - run_datetime: string, - for_date: string -): Promise { - const url = `fba/fire-zone-elevation-info/${run_type.toLowerCase()}/${run_datetime}/${for_date}/${fire_zone_id}` - const { data } = await axios.get(url) - return data -} - export async function getFireZoneTPIStats( fire_zone_id: number, run_type: RunType, From c8effa0545532130fa779360caa7a0981b9df8eb Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Tue, 10 Sep 2024 13:38:19 -0700 Subject: [PATCH 36/42] Add component test --- .../features/fba/components/viz/CriticalHours.tsx | 2 +- .../fba/components/viz/criticalHours.test.tsx | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 web/src/features/fba/components/viz/criticalHours.test.tsx diff --git a/web/src/features/fba/components/viz/CriticalHours.tsx b/web/src/features/fba/components/viz/CriticalHours.tsx index 60df81b23..e258523b9 100644 --- a/web/src/features/fba/components/viz/CriticalHours.tsx +++ b/web/src/features/fba/components/viz/CriticalHours.tsx @@ -11,7 +11,7 @@ const CriticalHours = ({ start, end }: CriticalHoursProps) => { return ( - {isUndefined(start) || isUndefined(end) ? "-" : `${start} - ${end}`} + {isUndefined(start) || isUndefined(end) ? "-" : `${start}:00 - ${end}:00`} ) diff --git a/web/src/features/fba/components/viz/criticalHours.test.tsx b/web/src/features/fba/components/viz/criticalHours.test.tsx new file mode 100644 index 000000000..22f528832 --- /dev/null +++ b/web/src/features/fba/components/viz/criticalHours.test.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import { render } from '@testing-library/react' +import CriticalHours from '@/features/fba/components/viz/CriticalHours' + +describe('CriticalHours', () => { + it('should render hours in 24 hour format', () => { + const { getByTestId } = render( + + ) + + const element = getByTestId('critical-hours') + expect(element).toBeInTheDocument() + expect(element).toHaveTextContent("8:00 - 11:00") + }) +}) From b7f4ddd1849df29b6d1daa36378db9d542a53cd8 Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Tue, 10 Sep 2024 13:56:43 -0700 Subject: [PATCH 37/42] Renaming --- api/app/routers/fba.py | 6 +-- api/app/tests/fba/test_fba_endpoint.py | 2 +- web/src/api/fbaAPI.ts | 12 ++--- web/src/app/rootReducer.ts | 9 ++-- .../infoPanel/FireZoneUnitSummary.tsx | 8 +-- .../components/infoPanel/FireZoneUnitTabs.tsx | 8 +-- .../infoPanel/fireZoneUnitSummary.test.tsx | 10 ++-- .../infoPanel/fireZoneUnitTabs.test.tsx | 18 +++---- .../fba/components/viz/FuelSummary.tsx | 13 +++-- .../fba/pages/FireBehaviourAdvisoryPage.tsx | 4 +- .../fba/slices/fireCentreHFIFuelStatsSlice.ts | 52 +++++++++++++++++++ .../fba/slices/fireCentreHfiFuelTypesSlice.ts | 52 ------------------- 12 files changed, 95 insertions(+), 99 deletions(-) create mode 100644 web/src/features/fba/slices/fireCentreHFIFuelStatsSlice.ts delete mode 100644 web/src/features/fba/slices/fireCentreHfiFuelTypesSlice.ts diff --git a/api/app/routers/fba.py b/api/app/routers/fba.py index 927257643..6717fb30f 100644 --- a/api/app/routers/fba.py +++ b/api/app/routers/fba.py @@ -102,12 +102,12 @@ async def get_provincial_summary(run_type: RunType, run_datetime: datetime, for_ return ProvincialSummaryResponse(provincial_summary=fire_shape_area_details) -@router.get("/fire-centre-hfi-fuels/{run_type}/{for_date}/{run_datetime}/{fire_centre_name}", response_model=dict[str, dict[int, List[ClassifiedHfiThresholdFuelTypeArea]]]) +@router.get("/fire-centre-hfi-stats/{run_type}/{for_date}/{run_datetime}/{fire_centre_name}", response_model=dict[str, dict[int, List[ClassifiedHfiThresholdFuelTypeArea]]]) async def get_hfi_fuels_data_for_fire_centre(run_type: RunType, for_date: date, run_datetime: datetime, fire_centre_name: str): """ - Fetch rollup of fuel type/HFI threshold/area data for a specified fire zone. + Fetch fuel type and critical hours data for all fire zones in a fire centre for a given date """ - logger.info("fire-centre-hfi-fuels/%s/%s/%s/%s", run_type.value, for_date, run_datetime, fire_centre_name) + logger.info("fire-centre-hfi-stats/%s/%s/%s/%s", run_type.value, for_date, run_datetime, fire_centre_name) async with get_async_read_session_scope() as session: # get thresholds data diff --git a/api/app/tests/fba/test_fba_endpoint.py b/api/app/tests/fba/test_fba_endpoint.py index 3f68a85d3..8768b85b3 100644 --- a/api/app/tests/fba/test_fba_endpoint.py +++ b/api/app/tests/fba/test_fba_endpoint.py @@ -11,7 +11,7 @@ get_fire_centres_url = "/api/fba/fire-centers" get_fire_zone_areas_url = "/api/fba/fire-shape-areas/forecast/2022-09-27/2022-09-27" get_fire_zone_tpi_stats_url = "/api/fba/fire-zone-tpi-stats/forecast/2022-09-27/2022-09-27/1" -get_fire_centre_info_url = "/api/fba/fire-centre-hfi-fuels/forecast/2022-09-27/2022-09-27/Kamloops%20Fire%20Centre" +get_fire_centre_info_url = "/api/fba/fire-centre-hfi-stats/forecast/2022-09-27/2022-09-27/Kamloops%20Fire%20Centre" get_fire_zone_elevation_info_url = "/api/fba/fire-zone-elevation-info/forecast/2022-09-27/2022-09-27/1" get_fire_centre_tpi_stats_url = f"/api/fba/fire-centre-tpi-stats/forecast/2024-08-10/2024-08-10/{mock_fire_centre_name}" get_sfms_run_datetimes_url = "/api/fba/sfms-run-datetimes/forecast/2022-09-27" diff --git a/web/src/api/fbaAPI.ts b/web/src/api/fbaAPI.ts index 50cbc89f3..e214f8be4 100644 --- a/web/src/api/fbaAPI.ts +++ b/web/src/api/fbaAPI.ts @@ -31,7 +31,7 @@ export interface AdvisoryCriticalHours { } -export interface FireZoneThresholdFuelTypeArea { +export interface FireZoneFuelStats { fuel_type: FuelType threshold: HfiThreshold critical_hours: AdvisoryCriticalHours @@ -103,9 +103,9 @@ export interface FuelType { description: string } -export interface FireCentreHfiFuelsData { +export interface FireCentreHFIStats { [fire_centre_name: string]: { - [fire_zone_id: number]: FireZoneThresholdFuelTypeArea[] + [fire_zone_id: number]: FireZoneFuelStats[] } } @@ -150,13 +150,13 @@ export async function getAllRunDates(run_type: RunType, for_date: string): Promi } -export async function getHFIThresholdsFuelTypesForCentre( +export async function getFireCentreHFIStats( run_type: RunType, for_date: string, run_datetime: string, fire_centre: string -): Promise { - const url = `fba/fire-centre-hfi-fuels/${run_type.toLowerCase()}/${for_date}/${run_datetime}/${fire_centre}` +): Promise { + const url = `fba/fire-centre-hfi-stats/${run_type.toLowerCase()}/${for_date}/${run_datetime}/${fire_centre}` const { data } = await axios.get(url) return data } diff --git a/web/src/app/rootReducer.ts b/web/src/app/rootReducer.ts index ccd50f677..1153a6667 100644 --- a/web/src/app/rootReducer.ts +++ b/web/src/app/rootReducer.ts @@ -13,7 +13,6 @@ import fireCentersSlice from 'commonSlices/fireCentersSlice' import fireShapeAreasSlice from 'features/fba/slices/fireZoneAreasSlice' import valueAtCoordinateSlice from 'features/fba/slices/valueAtCoordinateSlice' import runDatesSlice from 'features/fba/slices/runDatesSlice' -import hfiFuelTypesSlice from 'features/fba/slices/fireCentreHfiFuelTypesSlice' import fireZoneElevationInfoSlice from 'features/fba/slices/fireZoneElevationInfoSlice' import stationGroupsSlice from 'commonSlices/stationGroupsSlice' import selectedStationGroupsMembersSlice from 'commonSlices/selectedStationGroupMembers' @@ -21,7 +20,7 @@ import dataSlice from 'features/moreCast2/slices/dataSlice' import selectedStationsSlice from 'features/moreCast2/slices/selectedStationsSlice' import provincialSummarySlice from 'features/fba/slices/provincialSummarySlice' import fireCentreTPIStatsSlice from 'features/fba/slices/fireCentreTPIStatsSlice' -import fireCentreHfiFuelTypesSlice from 'features/fba/slices/fireCentreHfiFuelTypesSlice' +import fireCentreHFIFuelStatsSlice from 'features/fba/slices/fireCentreHFIFuelStatsSlice' const rootReducer = combineReducers({ percentileStations: stationReducer, @@ -38,8 +37,7 @@ const rootReducer = combineReducers({ fireShapeAreas: fireShapeAreasSlice, runDates: runDatesSlice, valueAtCoordinate: valueAtCoordinateSlice, - hfiFuelTypes: hfiFuelTypesSlice, - fireCentreHfiFuelTypes: fireCentreHfiFuelTypesSlice, + fireCentreHFIFuelStats: fireCentreHFIFuelStatsSlice, fireZoneElevationInfo: fireZoneElevationInfoSlice, fireCentreTPIStats: fireCentreTPIStatsSlice, stationGroups: stationGroupsSlice, @@ -68,8 +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 selectHFIFuelTypes = (state: RootState) => state.hfiFuelTypes -export const selectFireCentreHFIFuelTypes = (state: RootState) => state.fireCentreHfiFuelTypes +export const selectFireCentreHFIFuelTypes = (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 diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx index 21d037580..58b337939 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx @@ -1,18 +1,18 @@ import React from 'react' import { Grid, Typography } from '@mui/material' import { isUndefined } from 'lodash' -import { FireShape, FireZoneTPIStats, FireZoneThresholdFuelTypeArea } from 'api/fbaAPI' +import { FireShape, FireZoneTPIStats, FireZoneFuelStats } from 'api/fbaAPI' import ElevationStatus from 'features/fba/components/viz/ElevationStatus' import { useTheme } from '@mui/material/styles' import FuelSummary from 'features/fba/components/viz/FuelSummary' interface FireZoneUnitSummaryProps { selectedFireZoneUnit: FireShape | undefined - fuelTypeInfo: Record + fireZoneFuelStats: Record fireZoneTPIStats: FireZoneTPIStats | undefined } -const FireZoneUnitSummary = ({ fuelTypeInfo, fireZoneTPIStats, selectedFireZoneUnit }: FireZoneUnitSummaryProps) => { +const FireZoneUnitSummary = ({ fireZoneFuelStats, fireZoneTPIStats, selectedFireZoneUnit }: FireZoneUnitSummaryProps) => { const theme = useTheme() if (isUndefined(selectedFireZoneUnit)) { @@ -27,7 +27,7 @@ const FireZoneUnitSummary = ({ fuelTypeInfo, fireZoneTPIStats, selectedFireZoneU sx={{ paddingBottom: theme.spacing(2), paddingTop: theme.spacing(2) }} > - + {!fireZoneTPIStats || diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx index ada611a1e..0a9404c7e 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -27,7 +27,7 @@ const FireZoneUnitTabs = ({ setSelectedFireShape }: FireZoneUnitTabs) => { const { fireCentreTPIStats } = useSelector(selectFireCentreTPIStats) - const { fireCentreHfiFuelTypes } = useSelector(selectFireCentreHFIFuelTypes) + const { fireCentreHFIFuelStats } = useSelector(selectFireCentreHFIFuelTypes) const [tabNumber, setTabNumber] = useState(0) const sortedGroupedFireZoneUnits = useFireCentreDetails(selectedFireCenter) @@ -76,9 +76,9 @@ const FireZoneUnitTabs = ({ const hfiFuelStats = useMemo(() => { if (selectedFireCenter) { - return fireCentreHfiFuelTypes?.[selectedFireCenter?.name] + return fireCentreHFIFuelStats?.[selectedFireCenter?.name] } - }, [fireCentreHfiFuelTypes, selectedFireCenter]) + }, [fireCentreHFIFuelStats, selectedFireCenter]) if (isUndefined(selectedFireCenter) || isNull(selectedFireCenter)) { return
@@ -129,7 +129,7 @@ const FireZoneUnitTabs = ({ {sortedGroupedFireZoneUnits.map((zone, index) => ( stats.fire_zone_id == zone.fire_shape_id) : undefined } diff --git a/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx b/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx index 52059e929..a1c586914 100644 --- a/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx +++ b/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx @@ -24,7 +24,7 @@ describe('FireZoneUnitSummary', () => { window.ResizeObserver = ResizeObserver it('should not render empty div if selectedFireZoneUnit is undefined', () => { const { getByTestId } = render( - + ) const fireZoneUnitInfo = getByTestId('fire-zone-unit-summary-empty') expect(fireZoneUnitInfo).toBeInTheDocument() @@ -37,7 +37,7 @@ describe('FireZoneUnitSummary', () => { area_sqm: 10 } const { getByTestId } = render( - + ) const fireZoneUnitInfo = getByTestId('fire-zone-unit-summary') expect(fireZoneUnitInfo).toBeInTheDocument() @@ -50,7 +50,7 @@ describe('FireZoneUnitSummary', () => { area_sqm: 10 } const { queryByTestId } = render( - + ) const fireZoneUnitInfo = queryByTestId('elevation-status') expect(fireZoneUnitInfo).not.toBeInTheDocument() @@ -63,7 +63,7 @@ describe('FireZoneUnitSummary', () => { area_sqm: 10 } const { getByTestId } = render( - + ) const fireZoneUnitInfo = getByTestId('elevation-status') expect(fireZoneUnitInfo).toBeInTheDocument() @@ -78,7 +78,7 @@ describe('FireZoneUnitSummary', () => { } const { queryByTestId } = render( { +const buildTestStore = (hfiInitialState: FireCentreHFIFuelStatsState, tpiInitialState: CentreTPIStatsState) => { const rootReducer = combineReducers({ - fireCentreHfiFuelTypes: fireCentreHfiFuelTypesSlice, + fireCentreHFIFuelStats: fireCentreHFIFuelStatsSlice, fireCentreTPIStats: fireCentreTPIStatsSlice }) const testStore = configureStore({ reducer: rootReducer, preloadedState: { - fireCentreHfiFuelTypes: hfiInitialState, + fireCentreHFIFuelStats: hfiInitialState, fireCentreTPIStats: tpiInitialState } }) @@ -78,7 +78,7 @@ const mockFireCentreTPIStats: Record = { [fireCentre1]: [{ fire_zone_id: 1, valley_bottom: 10, mid_slope: 90, upper_slope: 10 }] } -const mockFireCentreHfiFuelTypes: FireCentreHfiFuelsData = { +const mockFireCentreHFIFuelStats: FireCentreHFIStats = { 'Centre 1': { 1: [ { @@ -128,7 +128,7 @@ const renderComponent = (testStore: any) => describe('FireZoneUnitTabs', () => { const testStore = buildTestStore( - { ...hfiInitialState, fireCentreHfiFuelTypes: mockFireCentreHfiFuelTypes }, + { ...hfiInitialState, fireCentreHFIFuelStats: mockFireCentreHFIFuelStats }, { ...tpiInitialState, fireCentreTPIStats: mockFireCentreTPIStats } ) it('should render', () => { diff --git a/web/src/features/fba/components/viz/FuelSummary.tsx b/web/src/features/fba/components/viz/FuelSummary.tsx index d6391c27d..948b89ced 100644 --- a/web/src/features/fba/components/viz/FuelSummary.tsx +++ b/web/src/features/fba/components/viz/FuelSummary.tsx @@ -1,8 +1,7 @@ import React, { useEffect, useState } from 'react' -import { FireShape, FireZoneThresholdFuelTypeArea } from 'api/fbaAPI' +import { FireShape, FireZoneFuelStats } from 'api/fbaAPI' import { Box, Tooltip, Typography } from '@mui/material' import { groupBy, isUndefined } from 'lodash' -import { DateTime } from 'luxon' import FuelDistribution from 'features/fba/components/viz/FuelDistribution' import { DataGridPro, GridColDef, GridColumnHeaderParams, GridRenderCellParams } from '@mui/x-data-grid-pro' import { styled, useTheme } from '@mui/material/styles' @@ -20,7 +19,7 @@ export interface FuelTypeInfoSummary { } interface FuelSummaryProps { - fuelTypeInfo: Record + fireZoneFuelStats: Record selectedFireZoneUnit: FireShape | undefined } @@ -73,17 +72,17 @@ const columns: GridColDef[] = [ } ] -const FuelSummary = ({ fuelTypeInfo, selectedFireZoneUnit }: FuelSummaryProps) => { +const FuelSummary = ({ fireZoneFuelStats, selectedFireZoneUnit }: FuelSummaryProps) => { const theme = useTheme() const [fuelTypeInfoRollup, setFuelTypeInfoRollup] = useState([]) useEffect(() => { - if (isUndefined(fuelTypeInfo) || isUndefined(selectedFireZoneUnit)) { + if (isUndefined(fireZoneFuelStats) || isUndefined(selectedFireZoneUnit)) { setFuelTypeInfoRollup([]) return } const shapeId = selectedFireZoneUnit.fire_shape_id - const fuelDetails = fuelTypeInfo[shapeId] + const fuelDetails = fireZoneFuelStats[shapeId] if (isUndefined(fuelDetails)) { setFuelTypeInfoRollup([]) return @@ -116,7 +115,7 @@ const FuelSummary = ({ fuelTypeInfo, selectedFireZoneUnit }: FuelSummaryProps) = } } setFuelTypeInfoRollup(rollUp) - }, [fuelTypeInfo]) // eslint-disable-line react-hooks/exhaustive-deps + }, [fireZoneFuelStats]) // eslint-disable-line react-hooks/exhaustive-deps return ( diff --git a/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx b/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx index a653ab24c..0c213856e 100644 --- a/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx +++ b/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx @@ -25,7 +25,7 @@ import AdvisoryReport from 'features/fba/components/infoPanel/AdvisoryReport' import FireZoneUnitTabs from 'features/fba/components/infoPanel/FireZoneUnitTabs' import { fetchFireCentreTPIStats } from 'features/fba/slices/fireCentreTPIStatsSlice' import AboutDataPopover from 'features/fba/components/AboutDataPopover' -import { fetchFireCentreHfiFuelTypes } from 'features/fba/slices/fireCentreHfiFuelTypesSlice' +import { fetchFireCentreHFIFuelStats } from 'features/fba/slices/fireCentreHFIFuelStatsSlice' const ADVISORY_THRESHOLD = 20 @@ -118,7 +118,7 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { !isNull(fireCenter) ) { dispatch(fetchFireCentreTPIStats(fireCenter.name, runType, doiISODate, mostRecentRunDate.toString())) - dispatch(fetchFireCentreHfiFuelTypes(fireCenter.name, runType, doiISODate, mostRecentRunDate.toString())) + dispatch(fetchFireCentreHFIFuelStats(fireCenter.name, runType, doiISODate, mostRecentRunDate.toString())) } }, [fireCenter, mostRecentRunDate]) diff --git a/web/src/features/fba/slices/fireCentreHFIFuelStatsSlice.ts b/web/src/features/fba/slices/fireCentreHFIFuelStatsSlice.ts new file mode 100644 index 000000000..51274f750 --- /dev/null +++ b/web/src/features/fba/slices/fireCentreHFIFuelStatsSlice.ts @@ -0,0 +1,52 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +import { AppThunk } from 'app/store' +import { logError } from 'utils/error' +import { FireCentreHFIStats, getFireCentreHFIStats } from 'api/fbaAPI' +import { RunType } from 'features/fba/pages/FireBehaviourAdvisoryPage' + +export interface FireCentreHFIFuelStatsState { + error: string | null + fireCentreHFIFuelStats: FireCentreHFIStats +} + +export const initialState: FireCentreHFIFuelStatsState = { + error: null, + fireCentreHFIFuelStats: {} +} + +const fireCentreHFIFuelStatsSlice = createSlice({ + name: 'fireCentreHfiFuelStats', + initialState, + reducers: { + getFireCentreHFIFuelStatsStart(state: FireCentreHFIFuelStatsState) { + state.error = null + state.fireCentreHFIFuelStats = {} + }, + getFireCentreHFIFuelStatsFailed(state: FireCentreHFIFuelStatsState, action: PayloadAction) { + state.error = action.payload + }, + getFireCentreHFIFuelStatsSuccess(state: FireCentreHFIFuelStatsState, action: PayloadAction) { + state.error = null + state.fireCentreHFIFuelStats = action.payload + } + } +}) + +export const { getFireCentreHFIFuelStatsStart, getFireCentreHFIFuelStatsFailed, getFireCentreHFIFuelStatsSuccess } = + fireCentreHFIFuelStatsSlice.actions + +export default fireCentreHFIFuelStatsSlice.reducer + +export const fetchFireCentreHFIFuelStats = + (fireCentre: string, runType: RunType, forDate: string, runDatetime: string): AppThunk => + async dispatch => { + try { + dispatch(getFireCentreHFIFuelStatsStart()) + const data = await getFireCentreHFIStats(runType, forDate, runDatetime, fireCentre) + dispatch(getFireCentreHFIFuelStatsSuccess(data)) + } catch (err) { + dispatch(getFireCentreHFIFuelStatsFailed((err as Error).toString())) + logError(err) + } + } diff --git a/web/src/features/fba/slices/fireCentreHfiFuelTypesSlice.ts b/web/src/features/fba/slices/fireCentreHfiFuelTypesSlice.ts deleted file mode 100644 index 62a2d489e..000000000 --- a/web/src/features/fba/slices/fireCentreHfiFuelTypesSlice.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -import { AppThunk } from 'app/store' -import { logError } from 'utils/error' -import { FireCentreHfiFuelsData, getHFIThresholdsFuelTypesForCentre } from 'api/fbaAPI' -import { RunType } from 'features/fba/pages/FireBehaviourAdvisoryPage' - -export interface CentreHFIFuelTypeState { - error: string | null - fireCentreHfiFuelTypes: FireCentreHfiFuelsData -} - -export const initialState: CentreHFIFuelTypeState = { - error: null, - fireCentreHfiFuelTypes: {} -} - -const fireCentreHfiFuelTypesSlice = createSlice({ - name: 'fireCentreHfiFuelTypes', - initialState, - reducers: { - getFireCentreHfiFuelTypesStart(state: CentreHFIFuelTypeState) { - state.error = null - state.fireCentreHfiFuelTypes = {} - }, - getFireCentreHfiFuelTypesFailed(state: CentreHFIFuelTypeState, action: PayloadAction) { - state.error = action.payload - }, - getFireCentreHfiFuelTypesSuccess(state: CentreHFIFuelTypeState, action: PayloadAction) { - state.error = null - state.fireCentreHfiFuelTypes = action.payload - } - } -}) - -export const { getFireCentreHfiFuelTypesStart, getFireCentreHfiFuelTypesFailed, getFireCentreHfiFuelTypesSuccess } = - fireCentreHfiFuelTypesSlice.actions - -export default fireCentreHfiFuelTypesSlice.reducer - -export const fetchFireCentreHfiFuelTypes = - (fireCentre: string, runType: RunType, forDate: string, runDatetime: string): AppThunk => - async dispatch => { - try { - dispatch(getFireCentreHfiFuelTypesStart()) - const data = await getHFIThresholdsFuelTypesForCentre(runType, forDate, runDatetime, fireCentre) - dispatch(getFireCentreHfiFuelTypesSuccess(data)) - } catch (err) { - dispatch(getFireCentreHfiFuelTypesFailed((err as Error).toString())) - logError(err) - } - } From e9f1716b3f7e34ebfa48c74aaa0d2848287a7402 Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Tue, 10 Sep 2024 13:58:12 -0700 Subject: [PATCH 38/42] Remove tooltip --- web/src/features/fba/components/viz/CriticalHours.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/src/features/fba/components/viz/CriticalHours.tsx b/web/src/features/fba/components/viz/CriticalHours.tsx index e258523b9..e3177ba1c 100644 --- a/web/src/features/fba/components/viz/CriticalHours.tsx +++ b/web/src/features/fba/components/viz/CriticalHours.tsx @@ -1,4 +1,4 @@ -import { Tooltip, Typography } from '@mui/material' +import { Typography } from '@mui/material' import React from 'react' import { isUndefined } from 'lodash' @@ -9,11 +9,9 @@ interface CriticalHoursProps { const CriticalHours = ({ start, end }: CriticalHoursProps) => { return ( - {isUndefined(start) || isUndefined(end) ? "-" : `${start}:00 - ${end}:00`} - ) } From fb0294e04b42f1d14fb203eb533f1a934d94b895 Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Tue, 10 Sep 2024 14:29:36 -0700 Subject: [PATCH 39/42] Fix merge regression --- web/src/features/fba/components/viz/FuelSummary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/features/fba/components/viz/FuelSummary.tsx b/web/src/features/fba/components/viz/FuelSummary.tsx index 948b89ced..83d38a46c 100644 --- a/web/src/features/fba/components/viz/FuelSummary.tsx +++ b/web/src/features/fba/components/viz/FuelSummary.tsx @@ -36,7 +36,7 @@ const columns: GridColDef[] = [ { field: 'code', headerClassName: 'fuel-summary-header', - headerName: 'Primary Fuels', + headerName: 'Fuel Type', sortable: false, width: 120, renderHeader: (params: GridColumnHeaderParams) => {params.colDef.headerName}, From 8484b9bb60ffeb985b4f1deec8cf79d035d10b36 Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Tue, 10 Sep 2024 14:55:30 -0700 Subject: [PATCH 40/42] Handle no critical hours --- api/app/db/crud/auto_spatial_advisory.py | 6 +++--- api/app/schemas/fba.py | 4 ++-- web/src/api/fbaAPI.ts | 4 ++-- web/src/features/fba/components/viz/CriticalHours.tsx | 5 +++-- .../features/fba/components/viz/criticalHours.test.tsx | 10 ++++++++++ 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/api/app/db/crud/auto_spatial_advisory.py b/api/app/db/crud/auto_spatial_advisory.py index a54750a85..b34654ef5 100644 --- a/api/app/db/crud/auto_spatial_advisory.py +++ b/api/app/db/crud/auto_spatial_advisory.py @@ -165,9 +165,9 @@ async def get_precomputed_stats_for_shape(session: AsyncSession, run_type: RunTy AdvisoryFuelStats.area, ) .distinct(AdvisoryFuelStats.fuel_type, AdvisoryFuelStats.run_parameters) - .join(RunParameters, AdvisoryFuelStats.run_parameters == RunParameters.id) - .join(CriticalHours, CriticalHours.run_parameters == RunParameters.id) - .join(Shape, AdvisoryFuelStats.advisory_shape_id == Shape.id) + .outerjoin(RunParameters, AdvisoryFuelStats.run_parameters == RunParameters.id) + .outerjoin(CriticalHours, CriticalHours.run_parameters == RunParameters.id) + .outerjoin(Shape, AdvisoryFuelStats.advisory_shape_id == Shape.id) .where( Shape.source_identifier == str(advisory_shape_id), RunParameters.run_type == run_type.value, diff --git a/api/app/schemas/fba.py b/api/app/schemas/fba.py index 19c864dc1..ec8740950 100644 --- a/api/app/schemas/fba.py +++ b/api/app/schemas/fba.py @@ -95,8 +95,8 @@ class SFMSFuelType(BaseModel): class AdvisoryCriticalHours(BaseModel): """Critical Hours for an advisory.""" - start_time: float - end_time: float + start_time: Optional[float] + end_time: Optional[float] class ClassifiedHfiThresholdFuelTypeArea(BaseModel): diff --git a/web/src/api/fbaAPI.ts b/web/src/api/fbaAPI.ts index e214f8be4..e294be237 100644 --- a/web/src/api/fbaAPI.ts +++ b/web/src/api/fbaAPI.ts @@ -26,8 +26,8 @@ export interface FBAResponse { } export interface AdvisoryCriticalHours { - start_time: number - end_time: number + start_time?: number + end_time?: number } diff --git a/web/src/features/fba/components/viz/CriticalHours.tsx b/web/src/features/fba/components/viz/CriticalHours.tsx index e3177ba1c..3e25ff649 100644 --- a/web/src/features/fba/components/viz/CriticalHours.tsx +++ b/web/src/features/fba/components/viz/CriticalHours.tsx @@ -1,6 +1,6 @@ import { Typography } from '@mui/material' import React from 'react' -import { isUndefined } from 'lodash' +import { isNull, isUndefined } from 'lodash' interface CriticalHoursProps { start?: number @@ -8,9 +8,10 @@ interface CriticalHoursProps { } const CriticalHours = ({ start, end }: CriticalHoursProps) => { + const formattedCriticalHours = isNull(start) || isUndefined(start) || isNull(end) || isUndefined(end) ? "-" : `${start}:00 - ${end}:00` return ( - {isUndefined(start) || isUndefined(end) ? "-" : `${start}:00 - ${end}:00`} + {formattedCriticalHours} ) } diff --git a/web/src/features/fba/components/viz/criticalHours.test.tsx b/web/src/features/fba/components/viz/criticalHours.test.tsx index 22f528832..006612b7c 100644 --- a/web/src/features/fba/components/viz/criticalHours.test.tsx +++ b/web/src/features/fba/components/viz/criticalHours.test.tsx @@ -12,4 +12,14 @@ describe('CriticalHours', () => { expect(element).toBeInTheDocument() expect(element).toHaveTextContent("8:00 - 11:00") }) + + it('should render no critical hours', () => { + const { getByTestId } = render( + + ) + + const element = getByTestId('critical-hours') + expect(element).toBeInTheDocument() + expect(element).toHaveTextContent("-") + }) }) From 6a2c057097a689204cd684a411e253f8226f6e05 Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Tue, 10 Sep 2024 14:58:22 -0700 Subject: [PATCH 41/42] Ran formatting with prettier --- web/src/features/fba/components/viz/CriticalHours.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/web/src/features/fba/components/viz/CriticalHours.tsx b/web/src/features/fba/components/viz/CriticalHours.tsx index 3e25ff649..02a1cfbbf 100644 --- a/web/src/features/fba/components/viz/CriticalHours.tsx +++ b/web/src/features/fba/components/viz/CriticalHours.tsx @@ -8,11 +8,12 @@ interface CriticalHoursProps { } const CriticalHours = ({ start, end }: CriticalHoursProps) => { - const formattedCriticalHours = isNull(start) || isUndefined(start) || isNull(end) || isUndefined(end) ? "-" : `${start}:00 - ${end}:00` + const formattedCriticalHours = + isNull(start) || isUndefined(start) || isNull(end) || isUndefined(end) ? '-' : `${start}:00 - ${end}:00` return ( - - {formattedCriticalHours} - + + {formattedCriticalHours} + ) } From 79e4d2937495c714b45050fecf6cbf654a3b68a1 Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Tue, 10 Sep 2024 15:23:24 -0700 Subject: [PATCH 42/42] Adjust styling of fuel table to remove horizontal scroll --- web/src/features/fba/components/viz/FuelSummary.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/web/src/features/fba/components/viz/FuelSummary.tsx b/web/src/features/fba/components/viz/FuelSummary.tsx index 83d38a46c..09394449c 100644 --- a/web/src/features/fba/components/viz/FuelSummary.tsx +++ b/web/src/features/fba/components/viz/FuelSummary.tsx @@ -38,7 +38,7 @@ const columns: GridColDef[] = [ headerClassName: 'fuel-summary-header', headerName: 'Fuel Type', sortable: false, - width: 120, + minWidth: 80, renderHeader: (params: GridColumnHeaderParams) => {params.colDef.headerName}, renderCell: (params: GridRenderCellParams) => ( @@ -48,10 +48,9 @@ const columns: GridColDef[] = [ }, { field: 'area', - flex: 3, + flex: 1, headerClassName: 'fuel-summary-header', headerName: 'Distribution > 4k kW/m', - minWidth: 200, sortable: false, renderHeader: (params: GridColumnHeaderParams) => {params.colDef.headerName}, renderCell: (params: GridRenderCellParams) => { @@ -60,10 +59,9 @@ const columns: GridColDef[] = [ }, { field: 'criticalHours', - flex: 3, headerClassName: 'fuel-summary-header', headerName: 'Critical Hours', - minWidth: 120, + minWidth: 110, sortable: false, renderHeader: (params: GridColumnHeaderParams) => {params.colDef.headerName}, renderCell: (params: GridRenderCellParams) => {