From 19028b1cd6abe5ec240f1d18e5324d6b1ca1afb8 Mon Sep 17 00:00:00 2001 From: Darren Boss Date: Thu, 30 Jan 2025 10:24:31 -0800 Subject: [PATCH 1/7] TPI area per fire zone --- .../1e6932096921_fuel_area_per_tpi_class.py | 41 ++++++ ...4b3ecb57fe_populate_tpi_fuel_area_table.py | 25 ++++ api/app/auto_spatial_advisory/elevation.py | 1 - .../process_tpi_fuel_area.py | 118 ++++++++++++++++++ api/app/db/crud/auto_spatial_advisory.py | 11 ++ api/app/db/models/auto_spatial_advisory.py | 17 +++ api/app/routers/fba.py | 40 ++++-- api/app/schemas/fba.py | 14 ++- web/src/api/fbaAPI.ts | 27 ++-- .../infoPanel/FireZoneUnitSummary.tsx | 15 ++- .../components/infoPanel/FireZoneUnitTabs.tsx | 6 +- .../infoPanel/fireZoneUnitTabs.test.tsx | 17 ++- .../fba/components/viz/ElevationStatus.tsx | 10 +- .../fba/slices/fireCentreTPIStatsSlice.ts | 7 +- 14 files changed, 299 insertions(+), 50 deletions(-) create mode 100644 api/alembic/versions/1e6932096921_fuel_area_per_tpi_class.py create mode 100644 api/alembic/versions/fa4b3ecb57fe_populate_tpi_fuel_area_table.py create mode 100644 api/app/auto_spatial_advisory/process_tpi_fuel_area.py diff --git a/api/alembic/versions/1e6932096921_fuel_area_per_tpi_class.py b/api/alembic/versions/1e6932096921_fuel_area_per_tpi_class.py new file mode 100644 index 0000000000..c466588759 --- /dev/null +++ b/api/alembic/versions/1e6932096921_fuel_area_per_tpi_class.py @@ -0,0 +1,41 @@ +"""Fuel area per TPI class + +Revision ID: 1e6932096921 +Revises: 4014ddf1f874 +Create Date: 2025-01-21 09:48:41.621501 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "1e6932096921" +down_revision = "4014ddf1f874" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "tpi_fuel_area", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("advisory_shape_id", sa.Integer(), nullable=False), + sa.Column("tpi_class", sa.Enum("valley_bottom", "mid_slope", "upper_slope", name="tpiclassenum"), nullable=False), + sa.Column("fuel_area", sa.Float(), nullable=False), + sa.ForeignKeyConstraint( + ["advisory_shape_id"], + ["advisory_shapes.id"], + ), + sa.PrimaryKeyConstraint("id"), + comment="Combustible area in each TPI class per fire zone unit.", + ) + op.create_index(op.f("ix_tpi_fuel_area_advisory_shape_id"), "tpi_fuel_area", ["advisory_shape_id"], unique=False) + op.create_index(op.f("ix_tpi_fuel_area_id"), "tpi_fuel_area", ["id"], unique=False) + + +def downgrade(): + op.drop_index(op.f("ix_tpi_fuel_area_id"), table_name="tpi_fuel_area") + op.drop_index(op.f("ix_tpi_fuel_area_advisory_shape_id"), table_name="tpi_fuel_area") + op.drop_table("tpi_fuel_area") diff --git a/api/alembic/versions/fa4b3ecb57fe_populate_tpi_fuel_area_table.py b/api/alembic/versions/fa4b3ecb57fe_populate_tpi_fuel_area_table.py new file mode 100644 index 0000000000..e74fb15435 --- /dev/null +++ b/api/alembic/versions/fa4b3ecb57fe_populate_tpi_fuel_area_table.py @@ -0,0 +1,25 @@ +"""Populate tpi_fuel_area table + +Revision ID: fa4b3ecb57fe +Revises: 1e6932096921 +Create Date: 2025-01-21 10:04:48.838953 + +""" + +from app.auto_spatial_advisory.process_tpi_fuel_area import process_tpi_fuel_area + + +# revision identifiers, used by Alembic. +revision = "fa4b3ecb57fe" +down_revision = "1e6932096921" +branch_labels = None +depends_on = None + + +def upgrade(): + process_tpi_fuel_area() + pass + + +def downgrade(): + pass diff --git a/api/app/auto_spatial_advisory/elevation.py b/api/app/auto_spatial_advisory/elevation.py index 4fc245009f..483e603d78 100644 --- a/api/app/auto_spatial_advisory/elevation.py +++ b/api/app/auto_spatial_advisory/elevation.py @@ -32,7 +32,6 @@ async def process_elevation_tpi(run_type: RunType, run_datetime: datetime, for_d """ Create new elevation statistics records for the given parameters. - :param hfi_s3_key: the object store key pointing to the hfi tif to intersect with tpi layer :param run_type: The type of run to process. (is it a forecast or actual run?) :param run_datetime: The date and time of the run to process. (when was the hfi file created?) :param for_date: The date of the hfi to process. (when is the hfi for?) diff --git a/api/app/auto_spatial_advisory/process_tpi_fuel_area.py b/api/app/auto_spatial_advisory/process_tpi_fuel_area.py new file mode 100644 index 0000000000..63e523f9eb --- /dev/null +++ b/api/app/auto_spatial_advisory/process_tpi_fuel_area.py @@ -0,0 +1,118 @@ +import numpy as np +from osgeo import gdal, ogr, osr + +from sqlalchemy import func, select +from sqlalchemy.orm import Session +from app import config +from app.auto_spatial_advisory.process_fuel_type_area import get_fuel_type_s3_key +from app.db.database import get_write_session_scope +from app.db.models.auto_spatial_advisory import Shape, TPIClassEnum, TPIFuelArea +from app.utils.geospatial import GDALResamplingMethod, warp_to_match_raster +from app.utils.s3 import set_s3_gdal_config + + +REPROJECTED_FUEL_LAYER_NAME = "warped_fuel.tif" +MASKED_TPI_NAME = "masked_tpi.tif" + + +def store_tpi_area_data(session: Session, advisory_shape_id: int, data: np.ndarray, pixel_size: int): + """ + Save TPIFuelArea records to the API database. + + :param session: A synchronous sqlalchemy session object. + :param advisory_shape_id: A fire zone id used as a foreign key to the advisory_shapes table. + :param data: A numpy ndarray containing classified TPI values that have been masked by the fuel layer. + :param pixel_size: The size of the cells in the TPI layer. + """ + unique_values, counts = np.unique(data, return_counts=True) + for value, count in zip(unique_values, counts): + if value in TPIClassEnum: + tpi_enum = TPIClassEnum(value) + fuel_area = count * pixel_size * pixel_size + tpi_fuel_area = TPIFuelArea(advisory_shape_id=advisory_shape_id, tpi_class=tpi_enum, fuel_area=fuel_area) + session.add(tpi_fuel_area) + + +def prepare_masked_tif(): + """ + Creates a masked TPI raster using a classified TPI raster from S3 storage and masking it using the fuel layer + also from S3 storage + """ + # Open up our rasters with gdal + set_s3_gdal_config() + bucket = config.get("OBJECT_STORE_BUCKET") + tpi_raster_name = config.get("CLASSIFIED_TPI_DEM_NAME") + fuel_raster_key = get_fuel_type_s3_key(bucket) + tpi_raster_key = f"/vsis3/{bucket}/dem/tpi/{tpi_raster_name}" + fuel_ds: gdal.Dataset = gdal.Open(fuel_raster_key, gdal.GA_ReadOnly) # LCC projection + tpi_ds: gdal.Dataset = gdal.Open(tpi_raster_key, gdal.GA_ReadOnly) # BC Albers 3005 projection + + # Warp the fuel raster to match extent, spatial reference and cell size of the TPI raster + warped_fuel_path = f"/vsimem/{REPROJECTED_FUEL_LAYER_NAME}" + warped_fuel_ds: gdal.Dataset = warp_to_match_raster(fuel_ds, tpi_ds, warped_fuel_path, GDALResamplingMethod.NEAREST_NEIGHBOUR) + + # Convert the warped fuel dataset into a binary mask by classifying fuel cells as 1 and non-fuel cells as 0. + warped_fuel_band: gdal.Band = warped_fuel_ds.GetRasterBand(1) + warped_fuel_data: np.ndarray = warped_fuel_band.ReadAsArray() + mask = np.where((warped_fuel_data > 0) & (warped_fuel_data < 99), 1, 0) + + # Some helpful things for creating the final masked TPI raster + geo_transform = tpi_ds.GetGeoTransform() + tpi_ds_srs = tpi_ds.GetProjection() + tpi_band: gdal.Band = tpi_ds.GetRasterBand(1) + tpi_data = tpi_band.ReadAsArray() + + # Apply the fuel layer mask to the classified TPI raster and store the result in an in-memory gdal dataset + masked_tpi_data = np.multiply(mask, tpi_data) + output_driver: gdal.Driver = gdal.GetDriverByName("MEM") + masked_tpi_dataset: gdal.Dataset = output_driver.Create(f"/vsimem/{MASKED_TPI_NAME}", xsize=tpi_band.XSize, ysize=tpi_band.YSize, bands=1, eType=gdal.GDT_Byte) + masked_tpi_dataset.SetGeoTransform(geo_transform) + masked_tpi_dataset.SetProjection(tpi_ds_srs) + masked_fuel_type_band: gdal.Band = masked_tpi_dataset.GetRasterBand(1) + masked_fuel_type_band.SetNoDataValue(0) + masked_fuel_type_band.WriteArray(masked_tpi_data) + fuel_ds = None + tpi_ds = None + return masked_tpi_dataset + + +def prepare_wkt_geom_for_gdal(wkt_geom: str): + """ + Given a wkt geometry as a string, convert it to an ogr.Geometry that can be used by gdal. + + :param wkt_geom: The wky geometry string. + :return: An osr.Geometry. + """ + geometry: ogr.Geometry = ogr.CreateGeometryFromWkt(wkt_geom) + source_srs = osr.SpatialReference() + source_srs.ImportFromEPSG(3005) + geometry.AssignSpatialReference(source_srs) + transform = osr.CoordinateTransformation(geometry.GetSpatialReference(), source_srs) + geometry.Transform(transform) + return geometry + + +def process_tpi_fuel_area(): + """ + Entry point for calculating the fuel layer masked area of each TPI class (valley bottom, mid slope and upper slope) per fire zone unit. + """ + masked_tpi_ds: gdal.Dataset = prepare_masked_tif() + masked_tpi_pixel_size = masked_tpi_ds.GetGeoTransform()[1] + + with get_write_session_scope() as session: + # Iterate through the fire zone units from the advisory_shapes table + stmt = select(Shape.id, func.ST_AsText(Shape.geom)) + result = session.execute(stmt) + for row in result: + # Convert the WKT geometry of a fire zone unit into a form that can be used by gdal + geometry = prepare_wkt_geom_for_gdal(row[1]) + + # Us gdal.Warp to clip out our fire zone unit from the masked tpi raster and then store areas in the tpi_fuel_area table + warp_options = gdal.WarpOptions(cutlineWKT=geometry, cutlineSRS=geometry.GetSpatialReference(), cropToCutline=True) + intersected_path = "/vsimem/intersected.tif" + intersected_ds: gdal.Dataset = gdal.Warp(intersected_path, masked_tpi_ds, options=warp_options) + intersected_band: gdal.Band = intersected_ds.GetRasterBand(1) + intersected_data: np.ndarray = intersected_band.ReadAsArray() + store_tpi_area_data(session, advisory_shape_id=row[0], data=intersected_data, pixel_size=masked_tpi_pixel_size) + intersected_ds = None + masked_tpi_ds = None diff --git a/api/app/db/crud/auto_spatial_advisory.py b/api/app/db/crud/auto_spatial_advisory.py index 1d955a4660..673b007706 100644 --- a/api/app/db/crud/auto_spatial_advisory.py +++ b/api/app/db/crud/auto_spatial_advisory.py @@ -23,6 +23,7 @@ AdvisoryElevationStats, AdvisoryTPIStats, ShapeType, + TPIFuelArea, ) from app.db.models.hfi_calc import FireCentre @@ -454,6 +455,16 @@ async def get_centre_tpi_stats(session: AsyncSession, fire_centre_name: str, run result = await session.execute(stmt) return result.all() +async def get_fire_centre_tpi_fuel_areas(session: AsyncSession, fire_centre_name: str): + stmt = ( + select(TPIFuelArea, Shape.source_identifier) + .join(Shape, Shape.id == TPIFuelArea.advisory_shape_id) + .join(FireCentre, FireCentre.id == Shape.fire_centre) + .where(FireCentre.name == fire_centre_name) + ) + 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") diff --git a/api/app/db/models/auto_spatial_advisory.py b/api/app/db/models/auto_spatial_advisory.py index 1c248613dd..17d1564ba9 100644 --- a/api/app/db/models/auto_spatial_advisory.py +++ b/api/app/db/models/auto_spatial_advisory.py @@ -30,6 +30,11 @@ class RunTypeEnum(enum.Enum): forecast = "forecast" actual = "actual" +class TPIClassEnum(enum.Enum): + valley_bottom = 1 + mid_slope = 2 + upper_slope = 3 + class ShapeType(Base): """Identify some kind of area type, e.g. "Zone", or "Fire" """ @@ -227,3 +232,15 @@ class CriticalHours(Base): fuel_type = Column(Integer, ForeignKey(SFMSFuelType.id), nullable=False, index=True) start_hour = Column(Integer, nullable=False) end_hour = Column(Integer, nullable=False) + +class TPIFuelArea(Base): + """ + Combustible area in each TPI class per fire zone unit. + """ + + __tablename__ = "tpi_fuel_area" + __table_args__ = {"comment": "Combustible area in each TPI class per fire zone unit."} + id = Column(Integer, primary_key=True, index=True) + advisory_shape_id = Column(Integer, ForeignKey(Shape.id), nullable=False, index=True) + tpi_class = Column(Enum(TPIClassEnum), nullable=False) + fuel_area = Column(Float, nullable=False) diff --git a/api/app/routers/fba.py b/api/app/routers/fba.py index 05082643f4..cfaf7f8218 100644 --- a/api/app/routers/fba.py +++ b/api/app/routers/fba.py @@ -10,6 +10,7 @@ from app.db.crud.auto_spatial_advisory import ( get_all_sfms_fuel_types, get_all_hfi_thresholds, + get_fire_centre_tpi_fuel_areas, get_hfi_area, get_precomputed_stats_for_shape, get_provincial_rollup, @@ -18,13 +19,15 @@ get_centre_tpi_stats, get_zone_ids_in_centre, ) -from app.db.models.auto_spatial_advisory import RunTypeEnum +from app.db.models.auto_spatial_advisory import RunTypeEnum, TPIClassEnum from app.schemas.fba import ( AdvisoryCriticalHours, ClassifiedHfiThresholdFuelTypeArea, FireCenterListResponse, + FireCentreTPIResponse, FireShapeAreaListResponse, FireShapeArea, + FireZoneTPIArea, FireZoneTPIStats, SFMSFuelType, HfiThreshold, @@ -174,24 +177,41 @@ async def get_fire_zone_tpi_stats(fire_zone_id: int, run_type: RunType, run_date ) -@router.get("/fire-centre-tpi-stats/{run_type}/{for_date}/{run_datetime}/{fire_centre_name}", response_model=dict[str, List[FireZoneTPIStats]]) +@router.get("/fire-centre-tpi-stats/{run_type}/{for_date}/{run_datetime}/{fire_centre_name}", response_model=FireCentreTPIResponse) 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) + tpi_fuel_stats = await get_fire_centre_tpi_fuel_areas(session, fire_centre_name) - data = [] + hfi_tpi_areas_by_zone = [] for row in tpi_stats_for_centre: + fire_zone_id = row.source_identifier square_metres = math.pow(row.pixel_size_metres, 2) - - data.append( + tpi_fuel_stats_for_zone = [stats[0] for stats in tpi_fuel_stats if stats[1] == fire_zone_id] + valley_bottom_tpi = 0 + mid_slope_tpi = 0 + upper_slope_tpi = 0 + + for tpi_fuel_stat in tpi_fuel_stats_for_zone: + if tpi_fuel_stat.tpi_class == TPIClassEnum.valley_bottom: + valley_bottom_tpi = tpi_fuel_stat.fuel_area + elif tpi_fuel_stat.tpi_class == TPIClassEnum.mid_slope: + mid_slope_tpi = tpi_fuel_stat.fuel_area + elif tpi_fuel_stat.tpi_class == TPIClassEnum.upper_slope: + upper_slope_tpi = tpi_fuel_stat.fuel_area + + hfi_tpi_areas_by_zone.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, + fire_zone_id=fire_zone_id, + valley_bottom_hfi=row.valley_bottom * square_metres, + valley_bottom_tpi=valley_bottom_tpi, + mid_slope_hfi=row.mid_slope * square_metres, + mid_slope_tpi=mid_slope_tpi, + upper_slope_hfi=row.upper_slope * square_metres, + upper_slope_tpi=upper_slope_tpi, ) ) - return {fire_centre_name: data} + return FireCentreTPIResponse(fire_centre_name=fire_centre_name, firezone_tpi_stats=hfi_tpi_areas_by_zone) diff --git a/api/app/schemas/fba.py b/api/app/schemas/fba.py index ef5583aa72..aa89ff0b43 100644 --- a/api/app/schemas/fba.py +++ b/api/app/schemas/fba.py @@ -124,9 +124,17 @@ class FireZoneTPIStats(BaseModel): """Classified TPI areas of the fire zone contributing to the HFI >4k. Each area is in square metres.""" fire_zone_id: int - valley_bottom: Optional[int] - mid_slope: Optional[int] - upper_slope: Optional[int] + valley_bottom_hfi: Optional[int] + valley_bottom_tpi: Optional[float] + mid_slope_hfi: Optional[int] + mid_slope_tpi: Optional[float] + upper_slope_hfi: Optional[int] + upper_slope_tpi: Optional[float] + + +class FireCentreTPIResponse(BaseModel): + fire_centre_name: str + firezone_tpi_stats: List[FireZoneTPIStats] class FireZoneElevationStatsByThreshold(BaseModel): diff --git a/web/src/api/fbaAPI.ts b/web/src/api/fbaAPI.ts index 191714980a..63f51d2634 100644 --- a/web/src/api/fbaAPI.ts +++ b/web/src/api/fbaAPI.ts @@ -68,9 +68,17 @@ export interface FireZoneElevationInfoResponse { export interface FireZoneTPIStats { fire_zone_id: number - valley_bottom?: number - mid_slope?: number - upper_slope?: number + valley_bottom_hfi?: number + valley_bottom_tpi?: number + mid_slope_hfi?: number + mid_slope_tpi?: number + upper_slope_hfi?: number + upper_slope_tpi?: number +} + +export interface FireCentreTPIResponse { + fire_centre_name: string + firezone_tpi_stats: FireZoneTPIStats[] } export interface FireShapeAreaListResponse { @@ -174,23 +182,12 @@ export async function getFireZoneElevationInfo( return data } -export async function getFireZoneTPIStats( - fire_zone_id: number, - run_type: RunType, - run_datetime: string, - for_date: string -): Promise { - const url = `fba/fire-zone-tpi-stats/${run_type.toLowerCase()}/${run_datetime}/${for_date}/${fire_zone_id}` - const { data } = await axios.get(url) - return data -} - export async function getFireCentreTPIStats( fire_centre_name: string, run_type: RunType, run_datetime: string, for_date: string -): Promise> { +): 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 diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx index a0dfc2d597..c40a841a62 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 { isNil, isNull, isUndefined } from 'lodash' import { FireShape, FireZoneTPIStats, FireZoneFuelStats } from 'api/fbaAPI' import ElevationStatus from 'features/fba/components/viz/ElevationStatus' import { useTheme } from '@mui/material/styles' @@ -14,13 +14,12 @@ interface FireZoneUnitSummaryProps { function hasRequiredFields(stats: FireZoneTPIStats): stats is Required { return ( - !isUndefined(stats.mid_slope) && - !isNull(stats.mid_slope) && - !isUndefined(stats.upper_slope) && - !isNull(stats.upper_slope) && - !isUndefined(stats.valley_bottom) && - !isNull(stats.valley_bottom) && - stats.mid_slope + stats.upper_slope + stats.mid_slope !== 0 + !isNil(stats.mid_slope_hfi) && + !isNil(stats.mid_slope_tpi) && + !isNil(stats.upper_slope_hfi) && + !isNil(stats.upper_slope_tpi) && + !isNil(stats.valley_bottom_hfi) && + !isNil(stats.valley_bottom_tpi) ) } diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx index 2f9a2cd558..95d49ca73a 100644 --- a/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx +++ b/web/src/features/fba/components/infoPanel/FireZoneUnitTabs.tsx @@ -7,7 +7,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 { useFireCentreDetails } from 'features/fba/hooks/useFireCentreDetails' -import { isEmpty, isNull, isUndefined } from 'lodash' +import { isEmpty, isNil, isNull, isUndefined } from 'lodash' import React, { useEffect, useMemo, useState } from 'react' import { useSelector } from 'react-redux' @@ -69,8 +69,8 @@ const FireZoneUnitTabs = ({ } const tpiStatsArray = useMemo(() => { - if (selectedFireCenter) { - return fireCentreTPIStats?.[selectedFireCenter.name] + if (selectedFireCenter && !isNil(fireCentreTPIStats)) { + return fireCentreTPIStats?.firezone_tpi_stats } }, [fireCentreTPIStats, selectedFireCenter]) diff --git a/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx b/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx index b0c90b036e..ddd4c21cc5 100644 --- a/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx +++ b/web/src/features/fba/components/infoPanel/fireZoneUnitTabs.test.tsx @@ -1,7 +1,7 @@ import React from 'react' import { render, screen, fireEvent } from '@testing-library/react' import FireZoneUnitTabs from './FireZoneUnitTabs' -import { FireCenter, FireCentreHFIStats, FireShape, FireShapeAreaDetail, FireZoneTPIStats } from 'api/fbaAPI' +import { FireCenter, FireCentreHFIStats, FireCentreTPIResponse, FireShape, FireShapeAreaDetail } from 'api/fbaAPI' import { vi } from 'vitest' import { ADVISORY_ORANGE_FILL, ADVISORY_RED_FILL } from '@/features/fba/components/map/featureStylers' import { combineReducers, configureStore } from '@reduxjs/toolkit' @@ -74,8 +74,19 @@ const mockSelectedFireCenter: FireCenter = { stations: [] } -const mockFireCentreTPIStats: Record = { - [fireCentre1]: [{ fire_zone_id: 1, valley_bottom: 10, mid_slope: 90, upper_slope: 10 }] +const mockFireCentreTPIStats: FireCentreTPIResponse = { + fire_centre_name: 'test_name', + firezone_tpi_stats: [ + { + fire_zone_id: 1, + valley_bottom_hfi: 10, + valley_bottom_tpi: 11, + mid_slope_hfi: 90, + mid_slope_tpi: 91, + upper_slope_hfi: 10, + upper_slope_tpi: 11 + } + ] } const mockFireCentreHFIFuelStats: FireCentreHFIStats = { diff --git a/web/src/features/fba/components/viz/ElevationStatus.tsx b/web/src/features/fba/components/viz/ElevationStatus.tsx index 9614ac3003..d2b762173e 100644 --- a/web/src/features/fba/components/viz/ElevationStatus.tsx +++ b/web/src/features/fba/components/viz/ElevationStatus.tsx @@ -20,10 +20,12 @@ interface ElevationStatusProps { const ElevationStatus = ({ tpiStats }: ElevationStatusProps) => { const theme = useTheme() - const total = tpiStats.mid_slope + tpiStats.upper_slope + tpiStats.valley_bottom - const mid_percent = tpiStats.mid_slope === 0 ? 0 : Math.round((tpiStats.mid_slope / total) * 100) - const upper_percent = tpiStats.upper_slope === 0 ? 0 : Math.round((tpiStats.upper_slope / total) * 100) - const bottom_percent = tpiStats.valley_bottom === 0 ? 0 : Math.round((tpiStats.valley_bottom / total) * 100) + const mid_percent = + tpiStats.mid_slope_tpi === 0 ? 0 : Math.round((tpiStats.mid_slope_hfi / tpiStats.mid_slope_tpi) * 100) + const upper_percent = + tpiStats.upper_slope_tpi === 0 ? 0 : Math.round((tpiStats.upper_slope_hfi / tpiStats.upper_slope_tpi) * 100) + const bottom_percent = + tpiStats.valley_bottom_tpi === 0 ? 0 : Math.round((tpiStats.valley_bottom_hfi / tpiStats.valley_bottom_tpi) * 100) return ( diff --git a/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts b/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts index 3f28f3c93b..a45509cc37 100644 --- a/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts +++ b/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts @@ -2,11 +2,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { AppThunk } from 'app/store' import { logError } from 'utils/error' -import { FireZoneTPIStats, getFireCentreTPIStats, RunType } from 'api/fbaAPI' +import { FireCentreTPIResponse, FireZoneTPIStats, getFireCentreTPIStats, RunType } from 'api/fbaAPI' export interface CentreTPIStatsState { error: string | null - fireCentreTPIStats: Record | null + fireCentreTPIStats: FireCentreTPIResponse | null } export const initialState: CentreTPIStatsState = { @@ -24,10 +24,11 @@ const fireCentreTPIStatsSlice = createSlice({ }, getFireCentreTPIStatsFailed(state: CentreTPIStatsState, action: PayloadAction) { state.error = action.payload + state.fireCentreTPIStats = null }, getFireCentreTPIStatsSuccess( state: CentreTPIStatsState, - action: PayloadAction> + action: PayloadAction ) { state.error = null state.fireCentreTPIStats = action.payload From 9c4c3f2246c58a4ff97f3690faf79942d2a3cbb0 Mon Sep 17 00:00:00 2001 From: Darren Boss Date: Thu, 30 Jan 2025 10:45:35 -0800 Subject: [PATCH 2/7] TPI tests --- .../infoPanel/fireZoneUnitSummary.test.tsx | 9 ++++--- .../components/viz/elevationStatus.test.tsx | 24 +++++++++++++++++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx b/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx index a1c5869148..e8ce7de56a 100644 --- a/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx +++ b/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx @@ -4,9 +4,12 @@ import { render } from '@testing-library/react' const fireZoneTPIStats = { fire_zone_id: 0, - valley_bottom: 0, - mid_slope: 100, - upper_slope: 0 + valley_bottom_hfi: 0, + valley_bottom_tpi: 0, + mid_slope_hfi: 100, + mid_slope_tpi: 100, + upper_slope_hfi: 0, + upper_slope_tpi: 0 } describe('FireZoneUnitSummary', () => { diff --git a/web/src/features/fba/components/viz/elevationStatus.test.tsx b/web/src/features/fba/components/viz/elevationStatus.test.tsx index 78c1d78322..4802210523 100644 --- a/web/src/features/fba/components/viz/elevationStatus.test.tsx +++ b/web/src/features/fba/components/viz/elevationStatus.test.tsx @@ -5,7 +5,17 @@ import ElevationStatus from 'features/fba/components/viz/ElevationStatus' describe('ElevationStatus', () => { it('should render all classifications and svg', () => { const { getByTestId } = render( - + ) const tpiMountain = getByTestId('tpi-mountain') @@ -26,7 +36,17 @@ describe('ElevationStatus', () => { it('should render all zero classifications', () => { const { getByTestId } = render( - + ) const valleyBottom = getByTestId('valley-bottom') From 07e3b31869a4cf85a1528eccfa586682919e9f93 Mon Sep 17 00:00:00 2001 From: Darren Boss Date: Thu, 30 Jan 2025 11:35:38 -0800 Subject: [PATCH 3/7] Remove invalid test --- .../infoPanel/fireZoneUnitSummary.test.tsx | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx b/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx index e8ce7de56a..456fa15a5a 100644 --- a/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx +++ b/web/src/features/fba/components/infoPanel/fireZoneUnitSummary.test.tsx @@ -66,32 +66,13 @@ describe('FireZoneUnitSummary', () => { area_sqm: 10 } const { getByTestId } = render( - - ) - const fireZoneUnitInfo = getByTestId('elevation-status') - expect(fireZoneUnitInfo).toBeInTheDocument() - }) - - it('should not render TPI stats all zero', () => { - const fireShape: FireShape = { - fire_shape_id: 1, - mof_fire_zone_name: 'foo', - mof_fire_centre_name: 'fizz', - area_sqm: 10 - } - const { queryByTestId } = render( ) - const fireZoneUnitInfo = queryByTestId('elevation-status') - expect(fireZoneUnitInfo).not.toBeInTheDocument() + const fireZoneUnitInfo = getByTestId('elevation-status') + expect(fireZoneUnitInfo).toBeInTheDocument() }) }) From 1a29110848f582bc94c0cb8f741f313bb891fc85 Mon Sep 17 00:00:00 2001 From: Darren Boss Date: Thu, 30 Jan 2025 12:02:15 -0800 Subject: [PATCH 4/7] Remove unused import --- api/app/routers/fba.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/app/routers/fba.py b/api/app/routers/fba.py index cfaf7f8218..2f64561c37 100644 --- a/api/app/routers/fba.py +++ b/api/app/routers/fba.py @@ -27,7 +27,6 @@ FireCentreTPIResponse, FireShapeAreaListResponse, FireShapeArea, - FireZoneTPIArea, FireZoneTPIStats, SFMSFuelType, HfiThreshold, From ffc2a0cc150357721cbe070ae32994c0b8d16af6 Mon Sep 17 00:00:00 2001 From: Darren Boss Date: Thu, 30 Jan 2025 15:19:23 -0800 Subject: [PATCH 5/7] Single zone stats, asa-go and tests --- api/app/db/crud/auto_spatial_advisory.py | 8 ++- api/app/routers/fba.py | 42 +++++++++----- api/app/tests/fba/test_fba_endpoint.py | 70 ++++++++++++++++++------ mobile/asa-go/src/api/fbaAPI.ts | 18 ++++-- 4 files changed, 102 insertions(+), 36 deletions(-) diff --git a/api/app/db/crud/auto_spatial_advisory.py b/api/app/db/crud/auto_spatial_advisory.py index 673b007706..d6adc2876b 100644 --- a/api/app/db/crud/auto_spatial_advisory.py +++ b/api/app/db/crud/auto_spatial_advisory.py @@ -455,9 +455,15 @@ async def get_centre_tpi_stats(session: AsyncSession, fire_centre_name: str, run result = await session.execute(stmt) return result.all() +async def get_fire_zone_tpi_fuel_areas(session: AsyncSession, fire_zone_id): + stmt = select(TPIFuelArea).join(Shape, Shape.id == TPIFuelArea.advisory_shape_id).where(Shape.source_identifier == fire_zone_id) + result = await session.execute(stmt) + return result.all() + + async def get_fire_centre_tpi_fuel_areas(session: AsyncSession, fire_centre_name: str): stmt = ( - select(TPIFuelArea, Shape.source_identifier) + select(TPIFuelArea.tpi_class, TPIFuelArea.fuel_area, Shape.source_identifier) .join(Shape, Shape.id == TPIFuelArea.advisory_shape_id) .join(FireCentre, FireCentre.id == Shape.fire_centre) .where(FireCentre.name == fire_centre_name) diff --git a/api/app/routers/fba.py b/api/app/routers/fba.py index 2f64561c37..92875582e7 100644 --- a/api/app/routers/fba.py +++ b/api/app/routers/fba.py @@ -11,6 +11,7 @@ get_all_sfms_fuel_types, get_all_hfi_thresholds, get_fire_centre_tpi_fuel_areas, + get_fire_zone_tpi_fuel_areas, get_hfi_area, get_precomputed_stats_for_shape, get_provincial_rollup, @@ -168,11 +169,26 @@ async def get_fire_zone_tpi_stats(fire_zone_id: int, run_type: RunType, run_date async with get_async_read_session_scope() as session: stats = await get_zonal_tpi_stats(session, fire_zone_id, run_type, run_datetime, for_date) square_metres = math.pow(stats.pixel_size_metres, 2) if stats is not None else None + tpi_fuel_stats = await get_fire_zone_tpi_fuel_areas(session, fire_zone_id) + valley_bottom_tpi = None + mid_slope_tpi = None + upper_slope_tpi = None + + for tpi_fuel_stat in tpi_fuel_stats: + if tpi_fuel_stat.tpi_class == TPIClassEnum.valley_bottom: + valley_bottom_tpi = tpi_fuel_stat.fuel_area + elif tpi_fuel_stat.tpi_class == TPIClassEnum.mid_slope: + mid_slope_tpi = tpi_fuel_stat.fuel_area + elif tpi_fuel_stat.tpi_class == TPIClassEnum.upper_slope: + upper_slope_tpi = tpi_fuel_stat.fuel_area return FireZoneTPIStats( fire_zone_id=fire_zone_id, - valley_bottom=stats.valley_bottom * square_metres if stats is not None else None, - mid_slope=stats.mid_slope * square_metres if stats is not None else None, - upper_slope=stats.upper_slope * square_metres if stats is not None else None, + valley_bottom_hfi=stats.valley_bottom * square_metres if stats is not None else None, + valley_bottom_tpi=valley_bottom_tpi, + mid_slope_hfi=stats.mid_slope * square_metres if stats is not None else None, + mid_slope_tpi=mid_slope_tpi, + upper_slope_hfi=stats.upper_slope * square_metres if stats is not None else None, + upper_slope_tpi=upper_slope_tpi, ) @@ -188,18 +204,18 @@ async def get_fire_centre_tpi_stats(fire_centre_name: str, run_type: RunType, ru for row in tpi_stats_for_centre: fire_zone_id = row.source_identifier square_metres = math.pow(row.pixel_size_metres, 2) - tpi_fuel_stats_for_zone = [stats[0] for stats in tpi_fuel_stats if stats[1] == fire_zone_id] - valley_bottom_tpi = 0 - mid_slope_tpi = 0 - upper_slope_tpi = 0 + tpi_fuel_stats_for_zone = [stats for stats in tpi_fuel_stats if stats[2] == fire_zone_id] + valley_bottom_tpi = None + mid_slope_tpi = None + upper_slope_tpi = None for tpi_fuel_stat in tpi_fuel_stats_for_zone: - if tpi_fuel_stat.tpi_class == TPIClassEnum.valley_bottom: - valley_bottom_tpi = tpi_fuel_stat.fuel_area - elif tpi_fuel_stat.tpi_class == TPIClassEnum.mid_slope: - mid_slope_tpi = tpi_fuel_stat.fuel_area - elif tpi_fuel_stat.tpi_class == TPIClassEnum.upper_slope: - upper_slope_tpi = tpi_fuel_stat.fuel_area + if tpi_fuel_stat[0] == TPIClassEnum.valley_bottom: + valley_bottom_tpi = tpi_fuel_stat[1] + elif tpi_fuel_stat[0] == TPIClassEnum.mid_slope: + mid_slope_tpi = tpi_fuel_stat[1] + elif tpi_fuel_stat[0] == TPIClassEnum.upper_slope: + upper_slope_tpi = tpi_fuel_stat[1] hfi_tpi_areas_by_zone.append( FireZoneTPIStats( diff --git a/api/app/tests/fba/test_fba_endpoint.py b/api/app/tests/fba/test_fba_endpoint.py index 5f2d6fe4d8..c4098a9771 100644 --- a/api/app/tests/fba/test_fba_endpoint.py +++ b/api/app/tests/fba/test_fba_endpoint.py @@ -4,7 +4,7 @@ from fastapi.testclient import TestClient from datetime import date, datetime, timezone from collections import namedtuple -from app.db.models.auto_spatial_advisory import AdvisoryTPIStats, HfiClassificationThreshold, RunParameters, SFMSFuelType +from app.db.models.auto_spatial_advisory import AdvisoryTPIStats, HfiClassificationThreshold, RunParameters, SFMSFuelType, TPIFuelArea, TPIClassEnum mock_fire_centre_name = "PGFireCentre" @@ -12,12 +12,15 @@ 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-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" 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_tpi_fuel_area_1 = TPIFuelArea(id=1, advisory_shape_id=1, tpi_class=TPIClassEnum.valley_bottom, fuel_area=1) +mock_tpi_fuel_area_2 = TPIFuelArea(id=2, advisory_shape_id=1, tpi_class=TPIClassEnum.mid_slope, fuel_area=2) +mock_tpi_fuel_area_3 = TPIFuelArea(id=3, advisory_shape_id=1, tpi_class=TPIClassEnum.upper_slope, fuel_area=3) +mock_tpi_fuel_areas = [mock_tpi_fuel_area_1, mock_tpi_fuel_area_2, mock_tpi_fuel_area_3] 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)) @@ -27,6 +30,11 @@ mock_centre_tpi_stats_1 = CentreHFIFuelResponse(advisory_shape_id=1, source_identifier=1, valley_bottom=1, mid_slope=2, upper_slope=3, pixel_size_metres=2) mock_centre_tpi_stats_2 = CentreHFIFuelResponse(advisory_shape_id=2, source_identifier=2, valley_bottom=1, mid_slope=2, upper_slope=3, pixel_size_metres=2) +CentreTPIFuelAreasResponse = namedtuple("CentreTPIFuelAreasResponse", ["tpi_class", "fuel_area", "source_identifier"]) +mock_centre_tpi_fuel_area_1 = CentreTPIFuelAreasResponse(tpi_class=TPIClassEnum.valley_bottom, fuel_area=1, source_identifier=1) +mock_centre_tpi_fuel_area_2 = CentreTPIFuelAreasResponse(tpi_class=TPIClassEnum.mid_slope, fuel_area=2, source_identifier=1) +mock_centre_tpi_fuel_area_3 = CentreTPIFuelAreasResponse(tpi_class=TPIClassEnum.upper_slope, fuel_area=3, source_identifier=1) + async def mock_get_fire_centres(*_, **__): return [] @@ -43,11 +51,18 @@ async def mock_get_auth_header(*_, **__): async def mock_get_tpi_stats(*_, **__): return mock_tpi_stats +async def mock_get_fire_zone_tpi_fuel_areas(*_, **__): + return mock_tpi_fuel_areas + async def mock_get_tpi_stats_none(*_, **__): return None +async def mock_get_fire_zone_tpi_fuel_areas_none(*_, **__): + return [] + + async def mock_get_fire_centre_info(*_, **__): return mock_fire_centre_info @@ -56,6 +71,10 @@ async def mock_get_centre_tpi_stats(*_, **__): return [mock_centre_tpi_stats_1, mock_centre_tpi_stats_2] +async def mock_get_fire_centre_tpi_fuel_areas(*_, **__): + return [mock_centre_tpi_fuel_area_1, mock_centre_tpi_fuel_area_2, mock_centre_tpi_fuel_area_3] + + async def mock_get_sfms_run_datetimes(*_, **__): return mock_sfms_run_datetimes @@ -128,6 +147,7 @@ def test_get_sfms_run_datetimes_authorized(client: TestClient): @patch("app.routers.fba.get_auth_header", mock_get_auth_header) @patch("app.routers.fba.get_zonal_tpi_stats", mock_get_tpi_stats) +@patch("app.routers.fba.get_fire_zone_tpi_fuel_areas", mock_get_fire_zone_tpi_fuel_areas) @pytest.mark.usefixtures("mock_jwt_decode") def test_get_fire_zone_tpi_stats_authorized(client: TestClient): """Allowed to get fire zone tpi stats when authorized""" @@ -135,38 +155,54 @@ def test_get_fire_zone_tpi_stats_authorized(client: TestClient): square_metres = math.pow(mock_tpi_stats.pixel_size_metres, 2) assert response.status_code == 200 assert response.json()["fire_zone_id"] == 1 - assert response.json()["valley_bottom"] == mock_tpi_stats.valley_bottom * square_metres - assert response.json()["mid_slope"] == mock_tpi_stats.mid_slope * square_metres - assert response.json()["upper_slope"] == mock_tpi_stats.upper_slope * square_metres + assert response.json()["valley_bottom_hfi"] == mock_tpi_stats.valley_bottom * square_metres + assert response.json()["mid_slope_hfi"] == mock_tpi_stats.mid_slope * square_metres + assert response.json()["upper_slope_hfi"] == mock_tpi_stats.upper_slope * square_metres + assert response.json()["valley_bottom_tpi"] == mock_tpi_fuel_area_1.fuel_area + assert response.json()["mid_slope_tpi"] == mock_tpi_fuel_area_2.fuel_area + assert response.json()["upper_slope_tpi"] == mock_tpi_fuel_area_3.fuel_area @patch("app.routers.fba.get_auth_header", mock_get_auth_header) @patch("app.routers.fba.get_zonal_tpi_stats", mock_get_tpi_stats_none) +@patch("app.routers.fba.get_fire_zone_tpi_fuel_areas", mock_get_fire_zone_tpi_fuel_areas_none) @pytest.mark.usefixtures("mock_jwt_decode") def test_get_fire_zone_tpi_stats_authorized_none(client: TestClient): """Returns none for TPI stats when there are no stats available""" response = client.get(get_fire_zone_tpi_stats_url) assert response.status_code == 200 assert response.json()["fire_zone_id"] == 1 - assert response.json()["valley_bottom"] == None - assert response.json()["mid_slope"] == None - assert response.json()["upper_slope"] == None + assert response.json()["valley_bottom_hfi"] is None + assert response.json()["mid_slope_hfi"] is None + assert response.json()["upper_slope_hfi"] is None + assert response.json()["valley_bottom_tpi"] is None + assert response.json()["mid_slope_tpi"] is None + assert response.json()["upper_slope_tpi"] is None @patch("app.routers.fba.get_auth_header", mock_get_auth_header) @patch("app.routers.fba.get_centre_tpi_stats", mock_get_centre_tpi_stats) +@patch("app.routers.fba.get_fire_centre_tpi_fuel_areas", mock_get_fire_centre_tpi_fuel_areas) @pytest.mark.usefixtures("mock_jwt_decode") def test_get_fire_centre_tpi_stats_authorized(client: TestClient): """Allowed to get fire zone tpi stats when authorized""" response = client.get(get_fire_centre_tpi_stats_url) square_metres = math.pow(mock_centre_tpi_stats_2.pixel_size_metres, 2) assert response.status_code == 200 - assert response.json()[mock_fire_centre_name][0]["fire_zone_id"] == 1 - assert response.json()[mock_fire_centre_name][0]["valley_bottom"] == mock_centre_tpi_stats_1.valley_bottom * square_metres - assert response.json()[mock_fire_centre_name][0]["mid_slope"] == mock_centre_tpi_stats_1.mid_slope * square_metres - assert response.json()[mock_fire_centre_name][0]["upper_slope"] == mock_centre_tpi_stats_1.upper_slope * square_metres - - assert response.json()[mock_fire_centre_name][1]["fire_zone_id"] == 2 - assert response.json()[mock_fire_centre_name][1]["valley_bottom"] == mock_centre_tpi_stats_2.valley_bottom * square_metres - assert response.json()[mock_fire_centre_name][1]["mid_slope"] == mock_centre_tpi_stats_2.mid_slope * square_metres - assert response.json()[mock_fire_centre_name][1]["upper_slope"] == mock_centre_tpi_stats_2.upper_slope * square_metres + json_response = response.json() + assert json_response["fire_centre_name"] == mock_fire_centre_name + assert json_response["firezone_tpi_stats"][0]["fire_zone_id"] == 1 + assert json_response["firezone_tpi_stats"][0]["valley_bottom_hfi"] == mock_centre_tpi_stats_1.valley_bottom * square_metres + assert json_response["firezone_tpi_stats"][0]["mid_slope_hfi"] == mock_centre_tpi_stats_1.mid_slope * square_metres + assert json_response["firezone_tpi_stats"][0]["upper_slope_hfi"] == mock_centre_tpi_stats_1.upper_slope * square_metres + assert json_response["firezone_tpi_stats"][0]["valley_bottom_tpi"] == mock_centre_tpi_fuel_area_1.fuel_area + assert json_response["firezone_tpi_stats"][0]["mid_slope_tpi"] == mock_centre_tpi_fuel_area_2.fuel_area + assert json_response["firezone_tpi_stats"][0]["upper_slope_tpi"] == mock_centre_tpi_fuel_area_3.fuel_area + + assert json_response["firezone_tpi_stats"][1]["fire_zone_id"] == 2 + assert json_response["firezone_tpi_stats"][1]["valley_bottom_hfi"] == mock_centre_tpi_stats_2.valley_bottom * square_metres + assert json_response["firezone_tpi_stats"][1]["mid_slope_hfi"] == mock_centre_tpi_stats_2.mid_slope * square_metres + assert json_response["firezone_tpi_stats"][1]["upper_slope_hfi"] == mock_centre_tpi_stats_2.upper_slope * square_metres + assert json_response["firezone_tpi_stats"][1]["valley_bottom_tpi"] is None + assert json_response["firezone_tpi_stats"][1]["mid_slope_tpi"] is None + assert json_response["firezone_tpi_stats"][1]["upper_slope_tpi"] is None diff --git a/mobile/asa-go/src/api/fbaAPI.ts b/mobile/asa-go/src/api/fbaAPI.ts index c20cef33ca..586868c8fd 100644 --- a/mobile/asa-go/src/api/fbaAPI.ts +++ b/mobile/asa-go/src/api/fbaAPI.ts @@ -68,9 +68,17 @@ export interface FireZoneElevationInfoResponse { export interface FireZoneTPIStats { fire_zone_id: number; - valley_bottom?: number; - mid_slope?: number; - upper_slope?: number; + valley_bottom_hfi?: number; + valley_bottom_tpi?: number; + mid_slope_hfi?: number; + mid_slope_tpi?: number; + upper_slope_hfi?: number; + upper_slope_tpi?: number; +} + +export interface FireCentreTPIResponse { + fire_centre_name: string + firezone_tpi_stats: FireZoneTPIStats[] } export interface FireShapeAreaListResponse { @@ -196,8 +204,8 @@ export async function getFireCentreTPIStats( run_type: RunType, run_datetime: string, for_date: string -): Promise> { +): 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; -} +} \ No newline at end of file From 6abccb8f3e6fcb223aa5d7dde72318ff81d6c759 Mon Sep 17 00:00:00 2001 From: Darren Boss Date: Thu, 30 Jan 2025 16:24:54 -0800 Subject: [PATCH 6/7] Include env in api pod --- openshift/templates/deploy.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openshift/templates/deploy.yaml b/openshift/templates/deploy.yaml index af0fa5e63a..5a9831cc44 100644 --- a/openshift/templates/deploy.yaml +++ b/openshift/templates/deploy.yaml @@ -301,6 +301,11 @@ objects: secretKeyRef: name: ${GLOBAL_NAME} key: object-store-bucket + - name: CLASSIFIED_TPI_DEM_NAME + valueFrom: + configMapKeyRef: + name: ${GLOBAL_NAME} + key: env.classified_tpi_dem_name - name: REDIS_HOST valueFrom: configMapKeyRef: From 4fd6023739a55f0e3c1aba42ff5bfda541539c04 Mon Sep 17 00:00:00 2001 From: Darren Boss Date: Thu, 30 Jan 2025 16:42:50 -0800 Subject: [PATCH 7/7] Remove unused imports --- .../features/fba/components/infoPanel/FireZoneUnitSummary.tsx | 2 +- web/src/features/fba/slices/fireCentreTPIStatsSlice.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx b/web/src/features/fba/components/infoPanel/FireZoneUnitSummary.tsx index c40a841a62..5706b6c3b2 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 { isNil, isNull, isUndefined } from 'lodash' +import { isNil, isUndefined } from 'lodash' import { FireShape, FireZoneTPIStats, FireZoneFuelStats } from 'api/fbaAPI' import ElevationStatus from 'features/fba/components/viz/ElevationStatus' import { useTheme } from '@mui/material/styles' diff --git a/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts b/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts index a45509cc37..b13f309616 100644 --- a/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts +++ b/web/src/features/fba/slices/fireCentreTPIStatsSlice.ts @@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { AppThunk } from 'app/store' import { logError } from 'utils/error' -import { FireCentreTPIResponse, FireZoneTPIStats, getFireCentreTPIStats, RunType } from 'api/fbaAPI' +import { FireCentreTPIResponse, getFireCentreTPIStats, RunType } from 'api/fbaAPI' export interface CentreTPIStatsState { error: string | null