diff --git a/.github/workflows/pr_description.js b/.github/workflows/pr_description.js index b1bc12016..432d2879b 100644 --- a/.github/workflows/pr_description.js +++ b/.github/workflows/pr_description.js @@ -21,6 +21,7 @@ module.exports = async ({ github, context }) => { body += `[FireBat bookmark](${prBaseUrl}/fire-behaviour-calculator?s=266&f=c5&c=NaN&w=20,s=286&f=c7&c=NaN&w=16,s=1055&f=c7&c=NaN&w=NaN,s=305&f=c7&c=NaN&w=NaN,s=344&f=c5&c=NaN&w=NaN,s=346&f=c7&c=NaN&w=NaN,s=328&f=c7&c=NaN&w=NaN,s=1399&f=c7&c=NaN&w=NaN,s=334&f=c7&c=NaN&w=NaN,s=1082&f=c3&c=NaN&w=NaN,s=388&f=c7&c=NaN&w=NaN,s=309&f=c7&c=NaN&w=16,s=306&f=c7&c=NaN&w=NaN,s=1029&f=c7&c=NaN&w=NaN,s=298&f=c7&c=NaN&w=NaN,s=836&f=c7&c=NaN&w=NaN,s=9999&f=c7&c=NaN&w=NaN)\n`; body += `[Auto Spatial Advisory (ASA)](${prBaseUrl}/auto-spatial-advisory)\n`; body += `[HFI Calculator](${prBaseUrl}/hfi-calculator)\n`; + body += `[PSU Insights](${prBaseUrl}/insights)\n`; github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, 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/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/landingPage/toolInfo.tsx b/web/src/features/landingPage/toolInfo.tsx index 9c4d3884c..8efd37822 100644 --- a/web/src/features/landingPage/toolInfo.tsx +++ b/web/src/features/landingPage/toolInfo.tsx @@ -4,6 +4,7 @@ import CalculateOutlinedIcon from '@mui/icons-material/CalculateOutlined' import LocalFireDepartmentIcon from '@mui/icons-material/LocalFireDepartment' import PercentIcon from '@mui/icons-material/Percent' import PublicIcon from '@mui/icons-material/Public' +import InsightsIcon from '@mui/icons-material/Insights' import WhatshotOutlinedIcon from '@mui/icons-material/WhatshotOutlined' import Link from '@mui/material/Link' import Typography from '@mui/material/Typography' @@ -22,7 +23,9 @@ import { PERCENTILE_CALC_NAME, PERCENTILE_CALC_ROUTE, MORE_CAST_NAME, - MORECAST_ROUTE + MORECAST_ROUTE, + PSU_INSIGHTS_NAME, + PSU_INSIGHTS_ROUTE } from 'utils/constants' const ICON_FONT_SIZE = 'large' @@ -132,11 +135,26 @@ export const fbpGoInfo: ToolInfo = { isBeta: false } +export const psuInsightsInfo: ToolInfo = { + name: PSU_INSIGHTS_NAME, + route: PSU_INSIGHTS_ROUTE, + description: ( + + A visualization tool providing an interactive map-based interface to analyze and understand critical + wildfire-related data. The tool offers a comprehensive view of key datasets, allowing users to visualize and + explore valuable information. + + ), + icon: , + isBeta: true +} + // The order of items in this array determines the order of items as they appear in the landing page // side bar and order of CardTravelSharp. export const toolInfos = [ moreCastInfo, fireBehaviourAdvisoryInfo, + ...(import.meta.env.MODE === 'development' ? [psuInsightsInfo] : []), cHainesInfo, fireBehaviourCalcInfo, hfiCalcInfo, 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..8daa205c3 --- /dev/null +++ b/web/src/features/psuInsights/components/map/PSUMap.tsx @@ -0,0 +1,76 @@ +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/psuFeatureStylers' +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, + opacity: 0.6 + }) + ) + + 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/psuFeatureStylers.test.ts b/web/src/features/psuInsights/components/map/psuFeatureStylers.test.ts new file mode 100644 index 000000000..a55da1678 --- /dev/null +++ b/web/src/features/psuInsights/components/map/psuFeatureStylers.test.ts @@ -0,0 +1,14 @@ +import { getColorForRasterValue } from '@/features/psuInsights/components/map/psuFeatureStylers' + +describe('getColorForRasterValue', () => { + it('should get the correct colour for the specified raster value', () => { + const rasterValue = 1 + const colour = getColorForRasterValue(rasterValue) + expect(colour).toBe('rgb(209, 255, 115)') + }) + it('should return a transparent colour if no colour is found', () => { + const rasterValue = 1000 + const colour = getColorForRasterValue(rasterValue) + expect(colour).toBe('rgba(0, 0, 0, 0)') + }) +}) diff --git a/web/src/features/psuInsights/components/map/psuFeatureStylers.ts b/web/src/features/psuInsights/components/map/psuFeatureStylers.ts new file mode 100644 index 000000000..d5fb88abc --- /dev/null +++ b/web/src/features/psuInsights/components/map/psuFeatureStylers.ts @@ -0,0 +1,40 @@ +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'] +]) + +export const getColorForRasterValue = (rasterValue: number): string => { + const fuelTypeCode = rasterValueToFuelTypeCode.get(rasterValue) + return fuelTypeCode ? colorByFuelTypeCode.get(fuelTypeCode) : 'rgba(0, 0, 0, 0)' +} + +export const styleFuelGrid = () => { + const style = (feature: RenderFeature | ol.Feature) => { + const fuelTypeInt = feature.getProperties().fuel + const fillColour = getColorForRasterValue(fuelTypeInt) + + return new Style({ + fill: new Fill({ color: fillColour }) + }) + } + return style +} 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() + }) +}) diff --git a/web/src/features/psuInsights/pages/PSUInsightsPage.tsx b/web/src/features/psuInsights/pages/PSUInsightsPage.tsx new file mode 100644 index 000000000..45d02f1e0 --- /dev/null +++ b/web/src/features/psuInsights/pages/PSUInsightsPage.tsx @@ -0,0 +1,17 @@ +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' + +export const PSUInsightsPage = () => { + return ( + + + + + +