From 4cf11f4bad00c836ac88407150f287496f229431 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Mon, 2 Dec 2024 12:12:43 -0800 Subject: [PATCH 01/13] init --- web/src/app/Routes.tsx | 12 ++- web/src/features/fba/components/viz/color.ts | 2 +- .../psuInsights/components/map/PSUMap.tsx | 75 +++++++++++++++++++ .../components/map/psuFeatureStyler.ts | 51 +++++++++++++ .../psuInsights/pages/PSUInsightsPage.tsx | 15 ++++ web/src/utils/constants.ts | 3 + 6 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 web/src/features/psuInsights/components/map/PSUMap.tsx create mode 100644 web/src/features/psuInsights/components/map/psuFeatureStyler.ts create mode 100644 web/src/features/psuInsights/pages/PSUInsightsPage.tsx diff --git a/web/src/app/Routes.tsx b/web/src/app/Routes.tsx index b2ea10cda..efa931e8d 100644 --- a/web/src/app/Routes.tsx +++ b/web/src/app/Routes.tsx @@ -16,7 +16,8 @@ import { FIRE_BEHAVIOR_CALC_ROUTE, FIRE_BEHAVIOUR_ADVISORY_ROUTE, LANDING_PAGE_ROUTE, - MORE_CAST_2_ROUTE + MORE_CAST_2_ROUTE, + PSU_INSIGHTS_ROUTE } from 'utils/constants' import { NoMatchPage } from 'features/NoMatchPage' const FireBehaviourCalculator = lazy(() => import('features/fbaCalculator/pages/FireBehaviourCalculatorPage')) @@ -24,6 +25,7 @@ const FireBehaviourAdvisoryPage = lazy(() => import('features/fba/pages/FireBeha const LandingPage = lazy(() => import('features/landingPage/pages/LandingPage')) const MoreCast2Page = lazy(() => import('features/moreCast2/pages/MoreCast2Page')) import LoadingBackdrop from 'features/hfiCalculator/components/LoadingBackdrop' +import { PSUInsightsPage } from '@/features/psuInsights/pages/PSUInsightsPage' const shouldShowDisclaimer = HIDE_DISCLAIMER === 'false' || HIDE_DISCLAIMER === undefined @@ -89,6 +91,14 @@ const WPSRoutes: React.FunctionComponent = () => { } /> + + + + } + /> } /> diff --git a/web/src/features/fba/components/viz/color.ts b/web/src/features/fba/components/viz/color.ts index e20ffdf32..35634dd4e 100644 --- a/web/src/features/fba/components/viz/color.ts +++ b/web/src/features/fba/components/viz/color.ts @@ -1,5 +1,5 @@ // A Map of fuel type codes to the colour typically used in BCWS -const colorByFuelTypeCode = new Map() +export const colorByFuelTypeCode = new Map() colorByFuelTypeCode.set('C-1', 'rgb(209, 255, 115)') colorByFuelTypeCode.set('C-2', 'rgb(34, 102, 51)') colorByFuelTypeCode.set('C-3', 'rgb(131, 199, 149)') diff --git a/web/src/features/psuInsights/components/map/PSUMap.tsx b/web/src/features/psuInsights/components/map/PSUMap.tsx new file mode 100644 index 000000000..1d75e5f8d --- /dev/null +++ b/web/src/features/psuInsights/components/map/PSUMap.tsx @@ -0,0 +1,75 @@ +import { BC_EXTENT, CENTER_OF_BC } from '@/utils/constants' +import { Map, View } from 'ol' +import { PMTilesVectorSource } from 'ol-pmtiles' +import { defaults as defaultControls } from 'ol/control' +import { boundingExtent } from 'ol/extent' +import 'ol/ol.css' +import { fromLonLat } from 'ol/proj' +import { PMTILES_BUCKET } from 'utils/env' + +import React, { useEffect, useRef, useState } from 'react' + +import { styleFuelGrid } from '@/features/psuInsights/components/map/psuFeatureStyler' +import { Box } from '@mui/material' +import { ErrorBoundary } from '@sentry/react' +import { source as baseMapSource } from 'features/fireWeather/components/maps/constants' +import TileLayer from 'ol/layer/Tile' +import VectorTileLayer from 'ol/layer/VectorTile' + +const MapContext = React.createContext(null) + +const bcExtent = boundingExtent(BC_EXTENT.map(coord => fromLonLat(coord))) + +const PSUMap = () => { + const [map, setMap] = useState(null) + const mapRef = useRef(null) as React.MutableRefObject + + const fuelGridVectorSource = new PMTilesVectorSource({ + url: `${PMTILES_BUCKET}fuel/fbp2024.pmtiles` + }) + + const [fuelGridVTL] = useState( + new VectorTileLayer({ + source: fuelGridVectorSource, + style: styleFuelGrid(), + zIndex: 51 + }) + ) + + useEffect(() => { + if (!mapRef.current) return + + const mapObject = new Map({ + target: mapRef.current, + layers: [new TileLayer({ source: baseMapSource }), fuelGridVTL], + controls: defaultControls(), + view: new View({ + zoom: 5, + center: fromLonLat(CENTER_OF_BC) + }) + }) + mapObject.getView().fit(bcExtent, { padding: [50, 50, 50, 50] }) + setMap(mapObject) + + return () => { + mapObject.setTarget('') + } + }, []) + + return ( + + + + + + ) +} + +export default PSUMap diff --git a/web/src/features/psuInsights/components/map/psuFeatureStyler.ts b/web/src/features/psuInsights/components/map/psuFeatureStyler.ts new file mode 100644 index 000000000..f1a1242c9 --- /dev/null +++ b/web/src/features/psuInsights/components/map/psuFeatureStyler.ts @@ -0,0 +1,51 @@ +import { colorByFuelTypeCode } from '@/features/fba/components/viz/color' +import * as ol from 'ol' +import Geometry from 'ol/geom/Geometry' +import RenderFeature from 'ol/render/Feature' +import Fill from 'ol/style/Fill' +import Style from 'ol/style/Style' + +const rasterValueToFuelTypeCode = new Map([ + [1, 'C-1'], + [2, 'C-2'], + [3, 'C-3'], + [4, 'C-4'], + [5, 'C-5'], + [6, 'C-6'], + [7, 'C-7'], + [8, 'D-1/D-2'], + [9, 'S-1'], + [10, 'S-2'], + [11, 'S-3'], + [12, 'O-1a/O-1b'], + [13, 'M-3'], + [14, 'M-1/M-2'] +]) + +const getColorForRasterValue = (rasterValue: number) => { + const fuelTypeCode = rasterValueToFuelTypeCode.get(rasterValue) + return fuelTypeCode ? colorByFuelTypeCode.get(fuelTypeCode) : null +} + +export const setTransparency = (color: string, alpha: number): string => { + if (!color) return 'rgba(0, 0, 0, 0)' + const rgbMatch = color.match(/\d+/g) + if (!rgbMatch || rgbMatch.length < 3) { + throw new Error(`Invalid color format: ${color}`) + } + const [r, g, b] = rgbMatch.map(Number) + return `rgba(${r}, ${g}, ${b}, ${alpha})` +} + +export const styleFuelGrid = () => { + const style = (feature: RenderFeature | ol.Feature) => { + const fuelTypeInt = feature.getProperties().fuel + const fillColour = getColorForRasterValue(fuelTypeInt) + const fillColourTransparency = setTransparency(fillColour, 0.7) + + return new Style({ + fill: new Fill({ color: fillColourTransparency }) + }) + } + return style +} diff --git a/web/src/features/psuInsights/pages/PSUInsightsPage.tsx b/web/src/features/psuInsights/pages/PSUInsightsPage.tsx new file mode 100644 index 000000000..2c9c1e353 --- /dev/null +++ b/web/src/features/psuInsights/pages/PSUInsightsPage.tsx @@ -0,0 +1,15 @@ +import { GeneralHeader } from '@/components/GeneralHeader' +import PSUMap from '@/features/psuInsights/components/map/PSUMap' +import { PSU_INSIGHTS_NAME } from '@/utils/constants' +import Box from '@mui/material/Box' + +export const PSUInsightsPage = () => { + return ( + + + + + + + ) +} diff --git a/web/src/utils/constants.ts b/web/src/utils/constants.ts index 7205c3d99..2aaf38bd0 100644 --- a/web/src/utils/constants.ts +++ b/web/src/utils/constants.ts @@ -24,6 +24,7 @@ export const FBP_GO_ROUTE = 'https://psu.nrs.gov.bc.ca/fbp-go' export const FIRE_BEHAVIOR_CALC_ROUTE = '/fire-behaviour-calculator' export const FIRE_BEHAVIOUR_ADVISORY_ROUTE = '/auto-spatial-advisory' export const MORE_CAST_2_ROUTE = '/morecast-2' +export const PSU_INSIGHTS_ROUTE = '/insights' export const LANDING_PAGE_ROUTE = '/' // ExpandableContainer widths @@ -45,6 +46,7 @@ export const FIRE_BEHAVIOUR_CALC_NAME = 'FireBat' export const HFI_CALC_NAME = 'HFI Calculator' export const MORE_CAST_NAME = 'MoreCast' export const PERCENTILE_CALC_NAME = 'Percentile Calculator' +export const PSU_INSIGHTS_NAME = 'PSU Insights' // UI constants export const HEADER_HEIGHT = 56 @@ -58,6 +60,7 @@ export const FIREBAT_DOC_TITLE = 'FireBat | BCWS PSU' export const HFI_CALC_DOC_TITLE = 'HFI Calculator | BCWS PSU' export const MORE_CAST_DOC_TITLE = 'MoreCast | BCWS PSU' export const PERCENTILE_CALC_DOC_TITLE = 'Percentile Calculator | BCWS PSU' +export const PSU_INSIGHTS_DOC_TITLE = 'PSU Insights | BCWS PSU' export enum FireCentres { CARIBOO_FC = 'Cariboo Fire Centre', From 520f1343c9c9690552d11452780a0e1badeb444a Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Mon, 2 Dec 2024 13:30:07 -0800 Subject: [PATCH 02/13] map test --- .../components/map/psuMap.test.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 web/src/features/psuInsights/components/map/psuMap.test.tsx diff --git a/web/src/features/psuInsights/components/map/psuMap.test.tsx b/web/src/features/psuInsights/components/map/psuMap.test.tsx new file mode 100644 index 000000000..c13f63aa9 --- /dev/null +++ b/web/src/features/psuInsights/components/map/psuMap.test.tsx @@ -0,0 +1,22 @@ +import PSUMap from '@/features/psuInsights/components/map/PSUMap' +import { render } from '@testing-library/react' + +describe('PSUMap', () => { + it('should render the map', () => { + class ResizeObserver { + observe() { + // mock no-op + } + unobserve() { + // mock no-op + } + disconnect() { + // mock no-op + } + } + window.ResizeObserver = ResizeObserver + const { getByTestId } = render() + const map = getByTestId('psu-map') + expect(map).toBeVisible() + }) +}) From a45bf1adcb2d0fd1b4d519b74dc8609abd0b2cee Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Mon, 2 Dec 2024 13:40:17 -0800 Subject: [PATCH 03/13] add footer --- web/src/features/landingPage/components/Footer.tsx | 2 +- web/src/features/psuInsights/pages/PSUInsightsPage.tsx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/features/landingPage/components/Footer.tsx b/web/src/features/landingPage/components/Footer.tsx index 19512bee1..aae8bb582 100644 --- a/web/src/features/landingPage/components/Footer.tsx +++ b/web/src/features/landingPage/components/Footer.tsx @@ -10,7 +10,7 @@ const Root = styled('div', { name: `${PREFIX}-links` })({ backgroundColor: theme.palette.primary.main, - minHeight: '80px' + minHeight: '30px' }) const FooterLinks = styled('div', { diff --git a/web/src/features/psuInsights/pages/PSUInsightsPage.tsx b/web/src/features/psuInsights/pages/PSUInsightsPage.tsx index 2c9c1e353..45d02f1e0 100644 --- a/web/src/features/psuInsights/pages/PSUInsightsPage.tsx +++ b/web/src/features/psuInsights/pages/PSUInsightsPage.tsx @@ -1,4 +1,5 @@ import { GeneralHeader } from '@/components/GeneralHeader' +import Footer from '@/features/landingPage/components/Footer' import PSUMap from '@/features/psuInsights/components/map/PSUMap' import { PSU_INSIGHTS_NAME } from '@/utils/constants' import Box from '@mui/material/Box' @@ -10,6 +11,7 @@ export const PSUInsightsPage = () => { +