From 38ec422c67175f283bab1889ada9ec1a1955cd5e Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Wed, 1 Nov 2023 11:15:27 -0700 Subject: [PATCH] Task/zone units frontend (#3204) - Generalizes the concept of `FireZone` to `FireShape` throughout the codebase - Adds migration for adding fire zone unit shape type - Adds migration for calculating fire zone unit combustible area - Points frontend layers to the fire zone unit pmtiles files --- .github/workflows/deployment.yml | 2 +- ...1e39_compute_zone_unit_combustible_area.py | 66 ++++++++++++++ api/app/routers/fba.py | 20 ++-- api/app/schemas/fba.py | 16 ++-- api/app/tests/fba/test_fba_endpoint.py | 2 +- web/src/api/fbaAPI.ts | 18 ++-- web/src/app/rootReducer.ts | 6 +- .../fba/components/ZoneSummaryPanel.tsx | 10 +- .../features/fba/components/map/FBAMap.tsx | 91 ++++++++++--------- .../features/fba/components/map/Legend.tsx | 12 +-- .../fba/components/map/fbaMap.test.tsx | 6 +- .../fba/components/map/featureStylers.ts | 34 ++++--- .../fba/components/map/legend.test.tsx | 8 +- .../fba/components/viz/CombustibleAreaViz.tsx | 4 +- .../fba/components/viz/ElevationInfoViz.tsx | 4 +- .../fba/components/viz/FuelTypesBreakdown.tsx | 8 +- .../fba/pages/FireBehaviourAdvisoryPage.tsx | 24 ++--- .../features/fba/slices/fireZoneAreasSlice.ts | 38 ++++---- 18 files changed, 219 insertions(+), 150 deletions(-) create mode 100644 api/alembic/versions/d5115b761e39_compute_zone_unit_combustible_area.py diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 3be0c7aa4..b22f446c1 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -22,7 +22,7 @@ jobs: shell: bash run: | oc login "${{ secrets.OPENSHIFT_CLUSTER }}" --token="${{ secrets.OC4_DEV_TOKEN }}" - EPHEMERAL_STORAGE=True bash openshift/scripts/oc_provision_db.sh ${SUFFIX} apply + EPHEMERAL_STORAGE=True CPU_REQUEST=75m CPU_LIMIT=2000m bash openshift/scripts/oc_provision_db.sh ${SUFFIX} apply prepare-dev-database-backups: name: Prepare Dev Database Backups diff --git a/api/alembic/versions/d5115b761e39_compute_zone_unit_combustible_area.py b/api/alembic/versions/d5115b761e39_compute_zone_unit_combustible_area.py new file mode 100644 index 000000000..bb630d0a3 --- /dev/null +++ b/api/alembic/versions/d5115b761e39_compute_zone_unit_combustible_area.py @@ -0,0 +1,66 @@ +"""compute zone unit combustible area + +Revision ID: d5115b761e39 +Revises: 5b745fe0bd7a +Create Date: 2023-10-31 12:24:36.889483 + +""" +from alembic import op +import sqlalchemy as sa +import geoalchemy2 +from sqlalchemy.orm.session import Session +from app.auto_spatial_advisory.calculate_combustible_land_area import calculate_combustible_area_by_fire_zone, get_fuel_types_from_object_store + +# revision identifiers, used by Alembic. +revision = 'd5115b761e39' +down_revision = 'f2e027a47a3f' +branch_labels = None +depends_on = None + +shape_type_table = sa.Table('advisory_shape_types', sa.MetaData(), + sa.Column('id', sa.Integer), + sa.Column('name', sa.String)) + +shape_table = sa.Table('advisory_shapes', sa.MetaData(), + sa.Column('id', sa.Integer), + sa.Column('source_identifier', sa.String), + sa.Column('shape_type', sa.Integer), + sa.Column('geom', geoalchemy2.Geometry)) + + +def get_fire_zone_unit_shape_type_id(session: Session): + statement = shape_type_table.select().where(shape_type_table.c.name == 'fire_zone_unit') + result = session.execute(statement).fetchone() + return result.id + + +def get_fire_zone_units(session: Session, fire_zone_type_id: int): + statement = shape_table.select().where(shape_table.c.shape_type == fire_zone_type_id) + result = session.execute(statement).fetchall() + return result + + +def upgrade(): + session = Session(bind=op.get_bind()) + + with get_fuel_types_from_object_store() as fuel_types: + # fetch all fire zones from DB + fire_zone_shape_type_id = get_fire_zone_unit_shape_type_id(session) + zone_units = get_fire_zone_units(session, fire_zone_shape_type_id) + + zone_areas = calculate_combustible_area_by_fire_zone(fuel_types, zone_units) + for tuple in zone_areas: + op.execute('UPDATE advisory_shapes SET combustible_area={} WHERE source_identifier LIKE \'{}\''.format( + tuple[1], tuple[0]) + ) + + +def downgrade(): + session = Session(bind=op.get_bind()) + fire_zone_shape_id = get_fire_zone_unit_shape_type_id(session) + zones = get_fire_zone_units(session, fire_zone_shape_id) + + for zone in zones: + op.execute('UPDATE advisory_shapes SET combustible_area = NULL WHERE source_identifier LIKE \'{}\''.format( + str(zone.source_identifier) + )) diff --git a/api/app/routers/fba.py b/api/app/routers/fba.py index d02e543b9..c539569fb 100644 --- a/api/app/routers/fba.py +++ b/api/app/routers/fba.py @@ -14,8 +14,8 @@ get_run_datetimes, get_zonal_elevation_stats) from app.db.models.auto_spatial_advisory import RunTypeEnum -from app.schemas.fba import (ClassifiedHfiThresholdFuelTypeArea, FireCenterListResponse, FireZoneAreaListResponse, - FireZoneArea, FireZoneElevationStats, FireZoneElevationStatsByThreshold, +from app.schemas.fba import (ClassifiedHfiThresholdFuelTypeArea, FireCenterListResponse, FireShapeAreaListResponse, + FireShapeArea, FireZoneElevationStats, FireZoneElevationStatsByThreshold, FireZoneElevationStatsListResponse, SFMSFuelType, HfiThreshold) from app.auth import authentication_required, audit from app.wildfire_one.wfwx_api import (get_auth_header, get_fire_centers) @@ -39,12 +39,12 @@ async def get_all_fire_centers(_=Depends(authentication_required)): return FireCenterListResponse(fire_centers=fire_centers) -@router.get('/fire-zone-areas/{run_type}/{run_datetime}/{for_date}', - response_model=FireZoneAreaListResponse) -async def get_zones(run_type: RunType, run_datetime: datetime, for_date: date, _=Depends(authentication_required)): - """ Return area of each zone, and percentage of area of zone with high hfi. """ +@router.get('/fire-shape-areas/{run_type}/{run_datetime}/{for_date}', + response_model=FireShapeAreaListResponse) +async def get_shapes(run_type: RunType, run_datetime: datetime, for_date: date, _=Depends(authentication_required)): + """ Return area of each zone unit shape, and percentage of area of zone unit shape with high hfi. """ async with get_async_read_session_scope() as session: - zones = [] + shapes = [] rows = await get_hfi_area(session, RunTypeEnum(run_type.value), @@ -55,13 +55,13 @@ async def get_zones(run_type: RunType, run_datetime: datetime, for_date: date, _ for row in rows: combustible_area = row.combustible_area # type: ignore hfi_area = row.hfi_area # type: ignore - zones.append(FireZoneArea( - mof_fire_zone_id=row.source_identifier, # type: ignore + shapes.append(FireShapeArea( + fire_shape_id=row.source_identifier, # type: ignore threshold=row.threshold, # type: ignore combustible_area=row.combustible_area, # type: ignore elevated_hfi_area=row.hfi_area, # type: ignore elevated_hfi_percentage=hfi_area / combustible_area * 100)) - return FireZoneAreaListResponse(zones=zones) + return FireShapeAreaListResponse(shapes=shapes) @router.get('/hfi-fuels/{run_type}/{for_date}/{run_datetime}/{zone_id}', diff --git a/api/app/schemas/fba.py b/api/app/schemas/fba.py index f09c310a7..fee05bd32 100644 --- a/api/app/schemas/fba.py +++ b/api/app/schemas/fba.py @@ -24,30 +24,30 @@ class FireCenterListResponse(BaseModel): fire_centers: List[FireCentre] -class FireZoneArea(BaseModel): +class FireShapeArea(BaseModel): """ A zone is a grouping of planning areas within a fire centre. """ - mof_fire_zone_id: int + fire_shape_id: int threshold: int combustible_area: float elevated_hfi_area: float elevated_hfi_percentage: float -class FireZoneAreaListResponse(BaseModel): +class FireShapeAreaListResponse(BaseModel): """ Response for all planning areas, in a list """ - zones: List[FireZoneArea] + shapes: List[FireShapeArea] -class FireZoneHighHfiAreas(BaseModel): +class FireShapeHighHfiAreas(BaseModel): """ A fire zone and the area exceeding HFI thresholds. """ - mof_fire_zone_id: int + fire_shape_id: int advisory_area: float warn_area: float -class FireZoneHighHfiAreasListResponse(BaseModel): +class FireShapeHighHfiAreasListResponse(BaseModel): """ Response for all fire zones and their areas exceeding high HFI thresholds. """ - zones: List[FireZoneHighHfiAreas] + zones: List[FireShapeHighHfiAreas] class HfiThresholdAreaByFuelType(BaseModel): diff --git a/api/app/tests/fba/test_fba_endpoint.py b/api/app/tests/fba/test_fba_endpoint.py index fc82f809d..93bb4c55a 100644 --- a/api/app/tests/fba/test_fba_endpoint.py +++ b/api/app/tests/fba/test_fba_endpoint.py @@ -5,7 +5,7 @@ from app.tests.utils.mock_jwt_decode_role import MockJWTDecodeWithRole get_fire_centres_url = '/api/fba/fire-centers' -get_fire_zone_areas_url = '/api/fba/fire-zone-areas/forecast/2022-09-27/2022-09-27' +get_fire_zone_areas_url = '/api/fba/fire-shape-areas/forecast/2022-09-27/2022-09-27' decode_fn = "jwt.decode" diff --git a/web/src/api/fbaAPI.ts b/web/src/api/fbaAPI.ts index 2346edcb2..3d30ac5b5 100644 --- a/web/src/api/fbaAPI.ts +++ b/web/src/api/fbaAPI.ts @@ -14,8 +14,8 @@ export interface FireCenter { stations: FireCenterStation[] } -export interface FireZone { - mof_fire_zone_id: number +export interface FireShape { + fire_shape_id: number mof_fire_zone_name: string mof_fire_centre_name?: string area_sqm?: number @@ -31,8 +31,8 @@ export interface FireZoneThresholdFuelTypeArea { area: number } -export interface FireZoneArea { - mof_fire_zone_id: number +export interface FireShapeArea { + fire_shape_id: number threshold: number combustible_area: number elevated_hfi_area: number @@ -56,8 +56,8 @@ export interface FireZoneElevationInfoResponse { hfi_elevation_info: ElevationInfoByThreshold[] } -export interface ZoneAreaListResponse { - zones: FireZoneArea[] +export interface FireShapeAreaListResponse { + shapes: FireShapeArea[] } export interface HfiThresholdFuelTypeArea { @@ -85,12 +85,12 @@ export async function getFBAFireCenters(): Promise { return data } -export async function getFireZoneAreas( +export async function getFireShapeAreas( run_type: RunType, run_datetime: string, for_date: string -): Promise { - const url = `/fba/fire-zone-areas/${run_type.toLowerCase()}/${encodeURI(run_datetime)}/${for_date}` +): Promise { + const url = `/fba/fire-shape-areas/${run_type.toLowerCase()}/${encodeURI(run_datetime)}/${for_date}` const { data } = await axios.get(url, {}) return data } diff --git a/web/src/app/rootReducer.ts b/web/src/app/rootReducer.ts index 0619fd491..cd0b8c18f 100644 --- a/web/src/app/rootReducer.ts +++ b/web/src/app/rootReducer.ts @@ -11,7 +11,7 @@ import hfiStationsReducer from 'features/hfiCalculator/slices/stationsSlice' import hfiReadyReducer, { HFIReadyState } from 'features/hfiCalculator/slices/hfiReadySlice' import fbaCalculatorSlice from 'features/fbaCalculator/slices/fbaCalculatorSlice' import fireCentersSlice from 'commonSlices/fireCentersSlice' -import fireZoneAreasSlice from 'features/fba/slices/fireZoneAreasSlice' +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' @@ -34,7 +34,7 @@ const rootReducer = combineReducers({ hfiReady: hfiReadyReducer, fbaCalculatorResults: fbaCalculatorSlice, fireCenters: fireCentersSlice, - fireZoneAreas: fireZoneAreasSlice, + fireShapeAreas: fireShapeAreasSlice, runDates: runDatesSlice, valueAtCoordinate: valueAtCoordinateSlice, hfiFuelTypes: hfiFuelTypesSlice, @@ -63,7 +63,7 @@ export const selectToken = (state: RootState) => state.authentication.token export const selectFireBehaviourCalcResult = (state: RootState) => state.fbaCalculatorResults export const selectHFIStations = (state: RootState) => state.hfiStations export const selectFireCenters = (state: RootState) => state.fireCenters -export const selectFireZoneAreas = (state: RootState) => state.fireZoneAreas +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 diff --git a/web/src/features/fba/components/ZoneSummaryPanel.tsx b/web/src/features/fba/components/ZoneSummaryPanel.tsx index f9918a079..d9e24fdf0 100644 --- a/web/src/features/fba/components/ZoneSummaryPanel.tsx +++ b/web/src/features/fba/components/ZoneSummaryPanel.tsx @@ -3,7 +3,7 @@ import { styled } from '@mui/material/styles' import CombustibleAreaViz from 'features/fba/components/viz/CombustibleAreaViz' import { Grid, IconButton, Typography } from '@mui/material' import { isUndefined } from 'lodash' -import { ElevationInfoByThreshold, FireZone, FireZoneArea, FireZoneThresholdFuelTypeArea } from 'api/fbaAPI' +import { ElevationInfoByThreshold, FireShape, FireShapeArea, FireZoneThresholdFuelTypeArea } from 'api/fbaAPI' import ElevationInfoViz from 'features/fba/components/viz/ElevationInfoViz' import FuelTypesBreakdown from 'features/fba/components/viz/FuelTypesBreakdown' import CloseIcon from '@mui/icons-material/Close' @@ -29,10 +29,10 @@ const CentreName = styled(Typography)({ }) interface Props { - selectedFireZone: FireZone | undefined + selectedFireZone: FireShape | undefined fuelTypeInfo: Record hfiElevationInfo: ElevationInfoByThreshold[] - fireZoneAreas: FireZoneArea[] + fireShapeAreas: FireShapeArea[] showSummaryPanel: boolean setShowSummaryPanel: React.Dispatch> } @@ -63,8 +63,8 @@ const ZoneSummaryPanel = React.forwardRef((props: Props, ref: React.ForwardedRef area.mof_fire_zone_id == props.selectedFireZone?.mof_fire_zone_id + fireZoneAreas={props.fireShapeAreas.filter( + area => area.fire_shape_id == props.selectedFireZone?.fire_shape_id )} /> diff --git a/web/src/features/fba/components/map/FBAMap.tsx b/web/src/features/fba/components/map/FBAMap.tsx index 4ba49f9f5..2c04e7ecf 100644 --- a/web/src/features/fba/components/map/FBAMap.tsx +++ b/web/src/features/fba/components/map/FBAMap.tsx @@ -14,13 +14,13 @@ import { ErrorBoundary } from 'components' import { selectFireWeatherStations, selectRunDates } from 'app/rootReducer' import { source as baseMapSource } from 'features/fireWeather/components/maps/constants' import Tile from 'ol/layer/Tile' -import { FireCenter, FireZone, FireZoneArea } from 'api/fbaAPI' +import { FireCenter, FireShape, FireShapeArea } from 'api/fbaAPI' import { extentsMap } from 'features/fba/fireCentreExtents' import { fireCentreStyler, fireCentreLabelStyler, - fireZoneStyler, - fireZoneLabelStyler, + fireShapeStyler, + fireShapeLabelStyler, stationStyler, hfiStyler } from 'features/fba/components/map/featureStylers' @@ -40,10 +40,10 @@ const zoom = 6 export interface FBAMapProps { testId?: string selectedFireCenter: FireCenter | undefined - selectedFireZone: FireZone | undefined + selectedFireShape: FireShape | undefined forDate: DateTime - setSelectedFireZone: React.Dispatch> - fireZoneAreas: FireZoneArea[] + setSelectedFireShape: React.Dispatch> + fireShapeAreas: FireShapeArea[] runType: RunType advisoryThreshold: number showSummaryPanel: boolean @@ -62,7 +62,7 @@ const removeLayerByName = (map: ol.Map, layerName: string) => { const FBAMap = (props: FBAMapProps) => { const { stations } = useSelector(selectFireWeatherStations) - const [showZoneStatus, setShowZoneStatus] = useState(true) + const [showShapeStatus, setShowShapeStatus] = useState(true) const [showHFI, setShowHFI] = React.useState(false) const [map, setMap] = useState(null) const mapRef = useRef(null) @@ -71,14 +71,14 @@ const FBAMap = (props: FBAMapProps) => { const fireCentreVectorSource = new olpmtiles.PMTilesVectorSource({ url: `${PMTILES_BUCKET}fireCentres.pmtiles` }) - const fireZoneVectorSource = new olpmtiles.PMTilesVectorSource({ - url: `${PMTILES_BUCKET}fireZones.pmtiles` + const fireShapeVectorSource = new olpmtiles.PMTilesVectorSource({ + url: `${PMTILES_BUCKET}fireZoneUnits.pmtiles` }) const fireCentreLabelVectorSource = new olpmtiles.PMTilesVectorSource({ url: `${PMTILES_BUCKET}fireCentreLabels.pmtiles` }) - const fireZoneLabelVectorSource = new olpmtiles.PMTilesVectorSource({ - url: `${PMTILES_BUCKET}fireZoneLabels.pmtiles` + const fireShapeLabelVectorSource = new olpmtiles.PMTilesVectorSource({ + url: `${PMTILES_BUCKET}fireZoneUnitLabels.pmtiles` }) const handleToggleLayer = (layerName: string, isVisible: boolean) => { @@ -88,9 +88,9 @@ const FBAMap = (props: FBAMapProps) => { .getArray() .find(l => l.getProperties()?.name === layerName) - if (layerName === 'fireZoneVector') { - fireZoneVTL.setStyle( - fireZoneStyler(cloneDeep(props.fireZoneAreas), props.advisoryThreshold, props.selectedFireZone, isVisible) + if (layerName === 'fireShapeVector') { + fireShapeVTL.setStyle( + fireShapeStyler(cloneDeep(props.fireShapeAreas), props.advisoryThreshold, props.selectedFireShape, isVisible) ) } else if (layer) { layer.setVisible(isVisible) @@ -105,17 +105,17 @@ const FBAMap = (props: FBAMapProps) => { zIndex: 50 }) ) - const [fireZoneVTL] = useState( + const [fireShapeVTL] = useState( new VectorTileLayer({ - source: fireZoneVectorSource, - style: fireZoneStyler( - cloneDeep(props.fireZoneAreas), + source: fireShapeVectorSource, + style: fireShapeStyler( + cloneDeep(props.fireShapeAreas), props.advisoryThreshold, - props.selectedFireZone, - showZoneStatus + props.selectedFireShape, + showShapeStatus ), zIndex: 49, - properties: { name: 'fireZoneVector' } + properties: { name: 'fireShapeVector' } }) ) // Seperate layer for polygons and for labels, to avoid duplicate labels. @@ -128,11 +128,11 @@ const FBAMap = (props: FBAMapProps) => { }) ) // Seperate layer for polygons and for labels, to avoid duplicate labels. - const [fireZoneLabelVTL] = useState( + const [fireShapeLabelVTL] = useState( new VectorTileLayer({ declutter: true, - source: fireZoneLabelVectorSource, - style: fireZoneLabelStyler(props.selectedFireZone), + source: fireShapeLabelVectorSource, + style: fireShapeLabelStyler(props.selectedFireShape), zIndex: 99, minZoom: 6 }) @@ -141,9 +141,9 @@ const FBAMap = (props: FBAMapProps) => { useEffect(() => { if (map) { map.on('click', event => { - fireZoneVTL.getFeatures(event.pixel).then(features => { + fireShapeVTL.getFeatures(event.pixel).then(features => { if (!features.length) { - props.setSelectedFireZone(undefined) + props.setSelectedFireShape(undefined) return } const feature = features[0] @@ -154,14 +154,14 @@ const FBAMap = (props: FBAMapProps) => { if (!isUndefined(zoneExtent)) { map.getView().fit(zoneExtent, { duration: 400, padding: [50, 50, 50, 50], maxZoom: 7.4 }) } - const fireZone: FireZone = { - mof_fire_zone_id: feature.get('MOF_FIRE_ZONE_ID'), - mof_fire_zone_name: feature.get('MOF_FIRE_ZONE_NAME'), - mof_fire_centre_name: feature.get('MOF_FIRE_CENTRE_NAME'), - area_sqm: feature.get('FEATURE_AREA_SQM') + const fireZone: FireShape = { + fire_shape_id: feature.getProperties().OBJECTID, + mof_fire_zone_name: feature.getProperties().OBJECTID.FIRE_ZONE, + mof_fire_centre_name: feature.getProperties().FIRE_CENTR, + area_sqm: feature.getProperties().Shape_Area } props.setShowSummaryPanel(true) - props.setSelectedFireZone(fireZone) + props.setSelectedFireShape(fireZone) }) }) } @@ -171,7 +171,7 @@ const FBAMap = (props: FBAMapProps) => { if (!map) return if (!props.showSummaryPanel) { - props.setSelectedFireZone(undefined) + props.setSelectedFireShape(undefined) } }, [props.showSummaryPanel]) // eslint-disable-line react-hooks/exhaustive-deps @@ -193,14 +193,19 @@ const FBAMap = (props: FBAMapProps) => { useEffect(() => { if (!map) return - fireZoneVTL.setStyle( - fireZoneStyler(cloneDeep(props.fireZoneAreas), props.advisoryThreshold, props.selectedFireZone, showZoneStatus) + fireShapeVTL.setStyle( + fireShapeStyler( + cloneDeep(props.fireShapeAreas), + props.advisoryThreshold, + props.selectedFireShape, + showShapeStatus + ) ) - fireZoneLabelVTL.setStyle(fireZoneLabelStyler(props.selectedFireZone)) - fireZoneVTL.changed() - fireZoneLabelVTL.changed() + fireShapeLabelVTL.setStyle(fireShapeLabelStyler(props.selectedFireShape)) + fireShapeVTL.changed() + fireShapeLabelVTL.changed() // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.selectedFireZone, props.fireZoneAreas, props.advisoryThreshold]) + }, [props.selectedFireShape, props.fireShapeAreas, props.advisoryThreshold]) useEffect(() => { if (!map) return @@ -246,9 +251,9 @@ const FBAMap = (props: FBAMapProps) => { source: baseMapSource }), fireCentreVTL, - fireZoneVTL, + fireShapeVTL, fireCentreLabelVTL, - fireZoneLabelVTL + fireShapeLabelVTL ], overlays: [], controls: defaultControls().extend([new FullScreen()]) @@ -299,8 +304,8 @@ const FBAMap = (props: FBAMapProps) => { diff --git a/web/src/features/fba/components/map/Legend.tsx b/web/src/features/fba/components/map/Legend.tsx index 486fb551e..939a0b947 100644 --- a/web/src/features/fba/components/map/Legend.tsx +++ b/web/src/features/fba/components/map/Legend.tsx @@ -75,13 +75,13 @@ const LegendItem: React.FC = ({ label, checked, onChange, subIt interface LegendProps { onToggleLayer: (layerName: string, isVisible: boolean) => void - showZoneStatus: boolean - setShowZoneStatus: React.Dispatch> + showShapeStatus: boolean + setShowShapeStatus: React.Dispatch> showHFI: boolean setShowHFI: React.Dispatch> } -const Legend = ({ onToggleLayer, showZoneStatus, setShowZoneStatus, showHFI, setShowHFI }: LegendProps) => { +const Legend = ({ onToggleLayer, showShapeStatus, setShowShapeStatus, showHFI, setShowHFI }: LegendProps) => { const handleLayerChange = ( layerName: string, isVisible: boolean, @@ -106,9 +106,9 @@ const Legend = ({ onToggleLayer, showZoneStatus, setShowZoneStatus, showHFI, set BC Fire Advisories handleLayerChange('fireZoneVector', showZoneStatus, setShowZoneStatus)} + label="Zone Unit Status" + checked={showShapeStatus} + onChange={() => handleLayerChange('fireShapeVector', showShapeStatus, setShowShapeStatus)} subItems={zoneStatusSubItems} /> { forDate={DateTime.fromISO('2016-05-25')} advisoryThreshold={0} selectedFireCenter={undefined} - selectedFireZone={undefined} - fireZoneAreas={[]} + selectedFireShape={undefined} + fireShapeAreas={[]} runType={RunType.FORECAST} - setSelectedFireZone={function (): void { + setSelectedFireShape={function (): void { throw new Error('Function not implemented.') }} showSummaryPanel={true} diff --git a/web/src/features/fba/components/map/featureStylers.ts b/web/src/features/fba/components/map/featureStylers.ts index 2c244db6c..5d0de1e9e 100644 --- a/web/src/features/fba/components/map/featureStylers.ts +++ b/web/src/features/fba/components/map/featureStylers.ts @@ -5,7 +5,7 @@ import CircleStyle from 'ol/style/Circle' import { Fill, Stroke, Text } from 'ol/style' import Style from 'ol/style/Style' import { range, startCase, lowerCase, isUndefined } from 'lodash' -import { FireZone, FireZoneArea } from 'api/fbaAPI' +import { FireShape, FireShapeArea } from 'api/fbaAPI' const EMPTY_FILL = 'rgba(0, 0, 0, 0.0)' export const ADVISORY_ORANGE_FILL = 'rgba(255, 147, 38, 0.4)' @@ -40,16 +40,16 @@ export const fireCentreStyler = (): Style => { }) } -export const fireZoneStyler = ( - fireZoneAreas: FireZoneArea[], +export const fireShapeStyler = ( + fireShapeAreas: FireShapeArea[], advisoryThreshold: number, - selectedFireZone: FireZone | undefined, + selectedFireShape: FireShape | undefined, showZoneStatus: boolean ) => { const a = (feature: RenderFeature | ol.Feature): Style => { - const mof_fire_zone_id = feature.get('MOF_FIRE_ZONE_ID') - const fireZoneAreaByThreshold = fireZoneAreas.filter(f => f.mof_fire_zone_id === mof_fire_zone_id) - const selected = !!(selectedFireZone?.mof_fire_zone_id && selectedFireZone.mof_fire_zone_id === mof_fire_zone_id) + const fire_shape_id = feature.getProperties().OBJECTID + const fireShapes = fireShapeAreas.filter(f => f.fire_shape_id === fire_shape_id) + const selected = !!(selectedFireShape?.fire_shape_id && selectedFireShape.fire_shape_id === fire_shape_id) let strokeValue = 'black' if (selected) { strokeValue = 'green' @@ -60,22 +60,20 @@ export const fireZoneStyler = ( color: strokeValue, width: selected ? 8 : 1 }), - fill: showZoneStatus - ? getAdvisoryColors(advisoryThreshold, fireZoneAreaByThreshold) - : new Fill({ color: EMPTY_FILL }) + fill: showZoneStatus ? getAdvisoryColors(advisoryThreshold, fireShapes) : new Fill({ color: EMPTY_FILL }) }) } return a } -export const getAdvisoryColors = (advisoryThreshold: number, fireZoneArea?: FireZoneArea[]) => { +export const getAdvisoryColors = (advisoryThreshold: number, fireShapeArea?: FireShapeArea[]) => { let fill = new Fill({ color: EMPTY_FILL }) - if (isUndefined(fireZoneArea) || fireZoneArea.length === 0) { + if (isUndefined(fireShapeArea) || fireShapeArea.length === 0) { return fill } - const advisoryThresholdArea = fireZoneArea.find(area => area.threshold == 1) - const warningThresholdArea = fireZoneArea.find(area => area.threshold == 2) + const advisoryThresholdArea = fireShapeArea.find(area => area.threshold == 1) + const warningThresholdArea = fireShapeArea.find(area => area.threshold == 2) const advisoryPercentage = advisoryThresholdArea?.elevated_hfi_percentage ?? 0 const warningPercentage = warningThresholdArea?.elevated_hfi_percentage ?? 0 @@ -92,12 +90,12 @@ export const getAdvisoryColors = (advisoryThreshold: number, fireZoneArea?: Fire return fill } -export const fireZoneLabelStyler = (selectedFireZone: FireZone | undefined) => { +export const fireShapeLabelStyler = (selectedFireShape: FireShape | undefined) => { const a = (feature: RenderFeature | ol.Feature): Style => { - const text = feature.get('mof_fire_zone_name').replace(' Fire Zone', '\nFire Zone') - const feature_mof_fire_zone_id = feature.get('mof_fire_zone_id') + const text = feature.getProperties().FIRE_ZONE.replace(' Fire Zone', '\nFire Zone') + const feature_fire_shape_id = feature.getProperties().OBJECTID const selected = - !isUndefined(selectedFireZone) && feature_mof_fire_zone_id === selectedFireZone.mof_fire_zone_id ? true : false + !isUndefined(selectedFireShape) && feature_fire_shape_id === selectedFireShape.fire_shape_id ? true : false return new Style({ text: new Text({ overflow: true, diff --git a/web/src/features/fba/components/map/legend.test.tsx b/web/src/features/fba/components/map/legend.test.tsx index 8cefcedca..54dcb75ca 100644 --- a/web/src/features/fba/components/map/legend.test.tsx +++ b/web/src/features/fba/components/map/legend.test.tsx @@ -11,10 +11,10 @@ describe('Legend', () => { const { getByTestId } = render( ) const legendComponent = getByTestId('asa-map-legend') @@ -38,10 +38,10 @@ describe('Legend', () => { const { getByTestId } = render( ) diff --git a/web/src/features/fba/components/viz/CombustibleAreaViz.tsx b/web/src/features/fba/components/viz/CombustibleAreaViz.tsx index 660991e03..d3f95b77a 100644 --- a/web/src/features/fba/components/viz/CombustibleAreaViz.tsx +++ b/web/src/features/fba/components/viz/CombustibleAreaViz.tsx @@ -1,6 +1,6 @@ import React from 'react' import { styled } from '@mui/material/styles' -import { FireZoneArea } from 'api/fbaAPI' +import { FireShapeArea } from 'api/fbaAPI' import { BarChart, CartesianGrid, XAxis, YAxis, Bar, Tooltip, ResponsiveContainer } from 'recharts' import { Typography } from '@mui/material' const PREFIX = 'CombustibleAreaViz' @@ -15,7 +15,7 @@ const StyledTypography = styled(Typography, { export interface AdvisoryMetadataProps { testId?: string - fireZoneAreas: FireZoneArea[] + fireZoneAreas: FireShapeArea[] } const CombustibleAreaViz = ({ fireZoneAreas }: AdvisoryMetadataProps) => { diff --git a/web/src/features/fba/components/viz/ElevationInfoViz.tsx b/web/src/features/fba/components/viz/ElevationInfoViz.tsx index 4d1c4726f..65ca80f4a 100644 --- a/web/src/features/fba/components/viz/ElevationInfoViz.tsx +++ b/web/src/features/fba/components/viz/ElevationInfoViz.tsx @@ -8,7 +8,7 @@ import TableContainer from '@mui/material/TableContainer' import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' import { isUndefined } from 'lodash' -import { ElevationInfoByThreshold, FireZone } from 'api/fbaAPI' +import { ElevationInfoByThreshold, FireShape } from 'api/fbaAPI' const PREFIX = 'ElevationInfoViz' @@ -30,7 +30,7 @@ const Root = styled('div')({ interface Props { className?: string - selectedFireZone: FireZone | undefined + selectedFireZone: FireShape | undefined hfiElevationInfo: ElevationInfoByThreshold[] } diff --git a/web/src/features/fba/components/viz/FuelTypesBreakdown.tsx b/web/src/features/fba/components/viz/FuelTypesBreakdown.tsx index fd4b25463..2798398d6 100644 --- a/web/src/features/fba/components/viz/FuelTypesBreakdown.tsx +++ b/web/src/features/fba/components/viz/FuelTypesBreakdown.tsx @@ -2,7 +2,7 @@ import React from 'react' import { styled } from '@mui/material/styles' import { Typography } from '@mui/material' import { isUndefined } from 'lodash' -import { FireZone, FireZoneThresholdFuelTypeArea } from 'api/fbaAPI' +import { FireShape, FireZoneThresholdFuelTypeArea } from 'api/fbaAPI' import { PieChart, Pie, ResponsiveContainer, Cell } from 'recharts' const PREFIX = 'FuelTypesBreakdown' @@ -25,7 +25,7 @@ const PieChartHeader = styled(Typography, { interface Props { className?: string - selectedFireZone: FireZone | undefined + selectedFireZone: FireShape | undefined fuelTypeInfo: Record } @@ -90,12 +90,12 @@ const FuelTypesBreakdown = (props: Props) => { ) } - if (isUndefined(props.selectedFireZone) || isUndefined(props.fuelTypeInfo[props.selectedFireZone.mof_fire_zone_id])) { + if (isUndefined(props.selectedFireZone) || isUndefined(props.fuelTypeInfo[props.selectedFireZone.fire_shape_id])) { return
} else { const advisories: FuelTypeDataForPieChart[] = [] const warnings: FuelTypeDataForPieChart[] = [] - props.fuelTypeInfo[props.selectedFireZone?.mof_fire_zone_id].forEach(record => { + props.fuelTypeInfo[props.selectedFireZone?.fire_shape_id].forEach(record => { if (record.threshold.id === 1) { advisories.push({ area: record.area, fuel_type_code: record.fuel_type.fuel_type_code }) } else if (record.threshold.id === 2) { diff --git a/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx b/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx index d699c05cb..1fa7ab466 100644 --- a/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx +++ b/web/src/features/fba/pages/FireBehaviourAdvisoryPage.tsx @@ -9,14 +9,14 @@ import { selectFireCenters, selectHFIFuelTypes, selectRunDates, - selectFireZoneAreas + selectFireShapeAreas } 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, FireZone } 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' @@ -25,7 +25,7 @@ import AdvisoryMetadata from 'features/fba/components/AdvisoryMetadata' import { fetchSFMSRunDates } from 'features/fba/slices/runDatesSlice' import { isNull, isUndefined } from 'lodash' import { fetchHighHFIFuels } from 'features/fba/slices/hfiFuelTypesSlice' -import { fetchFireZoneAreas } from 'features/fba/slices/fireZoneAreasSlice' +import { fetchFireShapeAreas } from 'features/fba/slices/fireZoneAreasSlice' import { fetchfireZoneElevationInfo } from 'features/fba/slices/fireZoneElevationInfoSlice' import ZoneSummaryPanel from 'features/fba/components/ZoneSummaryPanel' import { StyledFormControl } from 'components/StyledFormControl' @@ -53,7 +53,7 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { const [fireCenter, setFireCenter] = useState(undefined) const [advisoryThreshold, setAdvisoryThreshold] = useState(20) - const [selectedFireZone, setSelectedFireZone] = useState(undefined) + const [selectedFireZone, setSelectedFireZone] = useState(undefined) const [dateOfInterest, setDateOfInterest] = useState( DateTime.now().setZone(`UTC${PST_UTC_OFFSET}`).hour < 13 ? DateTime.now().setZone(`UTC${PST_UTC_OFFSET}`) @@ -62,7 +62,7 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { const [runType, setRunType] = useState(RunType.FORECAST) const [showSummaryPanel, setShowSummaryPanel] = useState(true) const { mostRecentRunDate } = useSelector(selectRunDates) - const { fireZoneAreas } = useSelector(selectFireZoneAreas) + const { fireShapeAreas } = useSelector(selectFireShapeAreas) useEffect(() => { const findCenter = (id: string | null): FireCenter | undefined => { @@ -114,9 +114,9 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { !isUndefined(mostRecentRunDate) && !isUndefined(selectedFireZone) ) { - dispatch(fetchHighHFIFuels(runType, doiISODate, mostRecentRunDate.toString(), selectedFireZone.mof_fire_zone_id)) + dispatch(fetchHighHFIFuels(runType, doiISODate, mostRecentRunDate.toString(), selectedFireZone.fire_shape_id)) dispatch( - fetchfireZoneElevationInfo(selectedFireZone.mof_fire_zone_id, runType, doiISODate, mostRecentRunDate.toString()) + fetchfireZoneElevationInfo(selectedFireZone.fire_shape_id, runType, doiISODate, mostRecentRunDate.toString()) ) } }, [mostRecentRunDate, selectedFireZone]) // eslint-disable-line react-hooks/exhaustive-deps @@ -124,7 +124,7 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { useEffect(() => { const doiISODate = dateOfInterest.toISODate() if (!isNull(mostRecentRunDate) && !isNull(doiISODate) && !isUndefined(mostRecentRunDate)) { - dispatch(fetchFireZoneAreas(runType, mostRecentRunDate.toString(), doiISODate)) + dispatch(fetchFireShapeAreas(runType, mostRecentRunDate.toString(), doiISODate)) } }, [mostRecentRunDate]) // eslint-disable-line react-hooks/exhaustive-deps @@ -225,7 +225,7 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { selectedFireZone={selectedFireZone} fuelTypeInfo={hfiThresholdsFuelTypes} hfiElevationInfo={fireZoneElevationInfo} - fireZoneAreas={fireZoneAreas} + fireShapeAreas={fireShapeAreas} showSummaryPanel={showSummaryPanel} setShowSummaryPanel={setShowSummaryPanel} /> @@ -234,11 +234,11 @@ const FireBehaviourAdvisoryPage: React.FunctionComponent = () => { diff --git a/web/src/features/fba/slices/fireZoneAreasSlice.ts b/web/src/features/fba/slices/fireZoneAreasSlice.ts index c6f2f296d..043d1490e 100644 --- a/web/src/features/fba/slices/fireZoneAreasSlice.ts +++ b/web/src/features/fba/slices/fireZoneAreasSlice.ts @@ -2,63 +2,63 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { AppThunk } from 'app/store' import { logError } from 'utils/error' -import { FireZoneArea, ZoneAreaListResponse, getFireZoneAreas } from 'api/fbaAPI' +import { FireShapeArea, FireShapeAreaListResponse, getFireShapeAreas } from 'api/fbaAPI' import { RunType } from 'features/fba/pages/FireBehaviourAdvisoryPage' interface State { loading: boolean error: string | null - fireZoneAreas: FireZoneArea[] + fireShapeAreas: FireShapeArea[] } const initialState: State = { loading: false, error: null, - fireZoneAreas: [] + fireShapeAreas: [] } -const fireZoneAreasSlice = createSlice({ - name: 'fireZoneAreas', +const fireShapeAreasSlice = createSlice({ + name: 'fireShapeAreas', initialState, reducers: { - getFireZoneAreasStart(state: State) { + getFireShapeAreasStart(state: State) { state.error = null state.loading = true - state.fireZoneAreas = [] + state.fireShapeAreas = [] }, - getFireZoneAreasFailed(state: State, action: PayloadAction) { + getFireShapeAreasFailed(state: State, action: PayloadAction) { state.error = action.payload state.loading = false }, - getFireZoneAreasSuccess(state: State, action: PayloadAction) { + getFireShapeAreasSuccess(state: State, action: PayloadAction) { state.error = null - state.fireZoneAreas = action.payload.zones + state.fireShapeAreas = action.payload.shapes state.loading = false } } }) -export const { getFireZoneAreasStart, getFireZoneAreasFailed, getFireZoneAreasSuccess } = fireZoneAreasSlice.actions +export const { getFireShapeAreasStart, getFireShapeAreasFailed, getFireShapeAreasSuccess } = fireShapeAreasSlice.actions -export default fireZoneAreasSlice.reducer +export default fireShapeAreasSlice.reducer -export const fetchFireZoneAreas = +export const fetchFireShapeAreas = (runType: RunType, run_datetime: string, for_date: string): AppThunk => async dispatch => { if (run_datetime != undefined && run_datetime !== ``) { try { - dispatch(getFireZoneAreasStart()) - const fireZoneAreas = await getFireZoneAreas(runType, run_datetime, for_date) - dispatch(getFireZoneAreasSuccess(fireZoneAreas)) + dispatch(getFireShapeAreasStart()) + const fireShapeAreas = await getFireShapeAreas(runType, run_datetime, for_date) + dispatch(getFireShapeAreasSuccess(fireShapeAreas)) } catch (err) { - dispatch(getFireZoneAreasFailed((err as Error).toString())) + dispatch(getFireShapeAreasFailed((err as Error).toString())) logError(err) } } else { try { - dispatch(getFireZoneAreasFailed('run_datetime cannot be undefined!')) + dispatch(getFireShapeAreasFailed('run_datetime cannot be undefined!')) } catch (err) { - dispatch(getFireZoneAreasFailed((err as Error).toString())) + dispatch(getFireShapeAreasFailed((err as Error).toString())) logError(err) } }