Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PSU Insights #4147

Merged
merged 17 commits into from
Dec 4, 2024
1 change: 1 addition & 0 deletions .github/workflows/pr_description.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 11 additions & 1 deletion web/src/app/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ 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'))
const FireBehaviourAdvisoryPage = lazy(() => import('features/fba/pages/FireBehaviourAdvisoryPage'))
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

Expand Down Expand Up @@ -89,6 +91,14 @@ const WPSRoutes: React.FunctionComponent = () => {
</AuthWrapper>
}
/>
<Route
path={PSU_INSIGHTS_ROUTE}
element={
<AuthWrapper>
<PSUInsightsPage />
</AuthWrapper>
}
/>
<Route path="*" element={<NoMatchPage />} />
</Routes>
</Suspense>
Expand Down
2 changes: 1 addition & 1 deletion web/src/features/fba/components/viz/color.ts
Original file line number Diff line number Diff line change
@@ -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)')
Expand Down
2 changes: 1 addition & 1 deletion web/src/features/landingPage/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const Root = styled('div', {
name: `${PREFIX}-links`
})({
backgroundColor: theme.palette.primary.main,
minHeight: '80px'
minHeight: '30px'
})

const FooterLinks = styled('div', {
Expand Down
20 changes: 19 additions & 1 deletion web/src/features/landingPage/toolInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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'
Expand All @@ -22,7 +23,9 @@
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'
Expand Down Expand Up @@ -132,11 +135,26 @@
isBeta: false
}

export const psuInsightsInfo: ToolInfo = {

Check warning on line 138 in web/src/features/landingPage/toolInfo.tsx

View check run for this annotation

Codecov / codecov/patch

web/src/features/landingPage/toolInfo.tsx#L138

Added line #L138 was not covered by tests
name: PSU_INSIGHTS_NAME,
route: PSU_INSIGHTS_ROUTE,
description: (
<Typography>
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, explore,
and extract valuable information.
</Typography>
),
icon: <InsightsIcon color="primary" fontSize={ICON_FONT_SIZE} />,
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,
Expand Down
75 changes: 75 additions & 0 deletions web/src/features/psuInsights/components/map/PSUMap.tsx
Original file line number Diff line number Diff line change
@@ -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/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<Map | null>(null)

const bcExtent = boundingExtent(BC_EXTENT.map(coord => fromLonLat(coord)))

const PSUMap = () => {
const [map, setMap] = useState<Map | null>(null)
const mapRef = useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLElement>

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 (
<ErrorBoundary>
<MapContext.Provider value={map}>
<Box
ref={mapRef}
data-testid={'psu-map'}
sx={{
width: '100%',
height: '100%'
}}
></Box>
</MapContext.Provider>
</ErrorBoundary>
)
}

export default PSUMap
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getColorForRasterValue, setTransparency } from '@/features/psuInsights/components/map/psuFeatureStylers'

describe('setTransparency', () => {
it('should return an rgba value from rgb with correct alpha value', () => {
const rgb = 'rgb(1, 1, 1)'
const rgba = setTransparency(rgb, 0.5)
expect(rgba).toBe('rgba(1, 1, 1, 0.5)')
})

it('should return an updated rgba value from an rgba input', () => {
const rgb = 'rgb(1, 1, 1, 1)'
const rgba = setTransparency(rgb, 0.5)
expect(rgba).toBe('rgba(1, 1, 1, 0.5)')
})

it('should throw an error if fewer than 3 RGB values are provided', () => {
const incompleteColor = 'rgb(1, 2)'
expect(() => setTransparency(incompleteColor, 0.5)).toThrow(Error)
})

it('should return a completely transparent colour if no colour is provided as input', () => {
const rgba = setTransparency(undefined, 0.5)
expect(rgba).toBe('rgba(0, 0, 0, 0)')
})
})

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 undefined if no colour is found', () => {
const rasterValue = 1000
const colour = getColorForRasterValue(rasterValue)
expect(colour).toBe(undefined)
})
})
59 changes: 59 additions & 0 deletions web/src/features/psuInsights/components/map/psuFeatureStylers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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 | undefined => {
const fuelTypeCode = rasterValueToFuelTypeCode.get(rasterValue)
return fuelTypeCode ? colorByFuelTypeCode.get(fuelTypeCode) : undefined
}

/**
* Takes rgb or rgba values as input, sets or updates the alpha value, and returns a new rgba value
* @param color rgb or rgba colour string ex. rgb(1, 2, 3) or rgba(1, 2, 3, 0.2)
* @param alpha number value between 0 and 1
* @returns rgba value with alpha set ex. rgba(1, 2, 3, 0.5)
*/
export const setTransparency = (color: string | undefined, alpha: number): string => {
if (!color) return 'rgba(0, 0, 0, 0)'

const rgbMatch = color.match(/\d+/g)?.map(Number)
if (!rgbMatch || rgbMatch.length < 3) {
throw new Error(`Invalid color format: "${color}"`)
}

const [r, g, b] = rgbMatch
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}

export const styleFuelGrid = () => {
brettedw marked this conversation as resolved.
Show resolved Hide resolved
const style = (feature: RenderFeature | ol.Feature<Geometry>) => {
const fuelTypeInt = feature.getProperties().fuel
const fillColour = getColorForRasterValue(fuelTypeInt)
const fillRGBA = setTransparency(fillColour, 0.6)

Check warning on line 52 in web/src/features/psuInsights/components/map/psuFeatureStylers.ts

View check run for this annotation

Codecov / codecov/patch

web/src/features/psuInsights/components/map/psuFeatureStylers.ts#L50-L52

Added lines #L50 - L52 were not covered by tests

return new Style({

Check warning on line 54 in web/src/features/psuInsights/components/map/psuFeatureStylers.ts

View check run for this annotation

Codecov / codecov/patch

web/src/features/psuInsights/components/map/psuFeatureStylers.ts#L54

Added line #L54 was not covered by tests
fill: new Fill({ color: fillRGBA })
})
}
return style
}
22 changes: 22 additions & 0 deletions web/src/features/psuInsights/components/map/psuMap.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<PSUMap />)
const map = getByTestId('psu-map')
expect(map).toBeVisible()
})
})
17 changes: 17 additions & 0 deletions web/src/features/psuInsights/pages/PSUInsightsPage.tsx
Original file line number Diff line number Diff line change
@@ -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 (

Check warning on line 8 in web/src/features/psuInsights/pages/PSUInsightsPage.tsx

View check run for this annotation

Codecov / codecov/patch

web/src/features/psuInsights/pages/PSUInsightsPage.tsx#L8

Added line #L8 was not covered by tests
<Box sx={{ height: '100vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<GeneralHeader isBeta={true} spacing={1} title={PSU_INSIGHTS_NAME} productName={PSU_INSIGHTS_NAME} />
<Box sx={{ flex: 1, position: 'relative' }}>
<PSUMap />
</Box>
<Footer />
</Box>
)
}
3 changes: 3 additions & 0 deletions web/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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',
Expand Down
Loading