Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: CI

on: [push, pull_request]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
- run: npm ci
- uses: actions/setup-python@v5
- uses: pre-commit/action@v3.0.1

typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 22.x
- run: npm ci
- run: npm run typecheck

build:
runs-on: ubuntu-latest
needs: [lint, typecheck]
steps:
- uses: actions/checkout@v4
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 22.x
- run: npm ci
- run: npm run build
25 changes: 25 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
repos:
- repo: local
hooks:
- id: prettier
name: prettier
entry: npx prettier --write
language: system
files: "\\.(\
js|jsx\
|ts|tsx\
|json\
)$"
- id: lint
name: lint
entry: npx eslint --fix
language: system
files: "\\.(\
js|jsx\
|ts|tsx\
)$"
- id: typecheck
name: typecheck
entry: npm run typecheck
language: system
pass_filenames: false
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@

**explore building scale climate risk data**

This tool allows exploration of climate risk data we [developed](https://github.com/carbonplan/ocr). Read our [explainer](https://carbonplan.org/research/climate-risk-explainer), [methods](https://carbonplan.org/research/climate-risk-fire-methods), and [FAQ](https://carbonplan.org/research/climate-risk-faq) for more information.
This tool allows exploration of climate risk data we [developed](https://github.com/carbonplan/ocr). Read our [explainer](https://carbonplan.org/research/climate-risk-explainer), [methods](https://carbonplan.org/research/climate-risk-fire-methods), and [FAQ](https://carbonplan.org/research/climate-risk-faq) for more information.

[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)


## local development

```shell
Expand Down
25 changes: 5 additions & 20 deletions components/building-points.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { useCallback, useEffect, useMemo } from 'react'
import {
ExpressionSpecification,
MapMouseEvent,
MapSourceDataEvent,
} from 'maplibre-gl'
import { ExpressionSpecification, MapMouseEvent } from 'maplibre-gl'
import { useStore } from '@/lib/store'
import { LAYERS } from '@/lib/config'
import { ensureSourceLoaded } from '@/lib/map-utils'
import { useColormap } from '@/lib/colormaps'
import { getBuildingRiskKey } from '@/lib/risk-utils'
import { useBuildingUtils } from '@/hooks/useBuildingUtils'
Expand Down Expand Up @@ -71,21 +68,9 @@ const BuildingPoints = () => {
if (feature.geometry.type !== 'Point') return
const [lng, lat] = feature.geometry.coordinates

const handleMoveEnd = () => {
if (map.isSourceLoaded(LAYERS.buildings.sourceId)) {
highlightBuildingAtLocation(lng, lat, { easeTo: false })
} else {
const handleSourceData = (e: MapSourceDataEvent) => {
if (
e.sourceId === LAYERS.buildings.sourceId &&
e.isSourceLoaded
) {
map.off('sourcedata', handleSourceData)
highlightBuildingAtLocation(lng, lat, { easeTo: false })
}
}
map.on('sourcedata', handleSourceData)
}
const handleMoveEnd = async () => {
await ensureSourceLoaded(map, LAYERS.buildings.sourceId)
highlightBuildingAtLocation(lng, lat, { easeTo: false })
}

map.once('moveend', handleMoveEnd)
Expand Down
35 changes: 9 additions & 26 deletions components/geocode/geocode.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState, useRef, useEffect } from 'react'
import { Box, Flex } from 'theme-ui'
import { mix } from '@theme-ui/color'
import { MapSourceDataEvent } from 'maplibre-gl'
import { ensureSourceLoaded } from '@/lib/map-utils'
//@ts-expect-error - carbonplan components types not available
import { Button, Input, Row, Column } from '@carbonplan/components'
//@ts-expect-error - carbonplan layouts types not available
Expand Down Expand Up @@ -204,31 +204,14 @@ const Geocode = () => {

// Highlight building after map movement completes
if (location.address.houseNumber) {
const handleMoveEnd = () => {
if (map.isSourceLoaded(LAYERS.buildings.sourceId)) {
const success = highlightBuildingAtLocation(
location.position.lng,
location.position.lat,
{ easeTo: false, fetchAddress: false },
)
if (success) setSelectedLocation(location)
} else {
const handleSourceData = (e: MapSourceDataEvent) => {
if (
e.sourceId === LAYERS.buildings.sourceId &&
e.isSourceLoaded
) {
map.off('sourcedata', handleSourceData)
const success = highlightBuildingAtLocation(
location.position.lng,
location.position.lat,
{ easeTo: false, fetchAddress: false },
)
if (success) setSelectedLocation(location)
}
}
map.on('sourcedata', handleSourceData)
}
const handleMoveEnd = async () => {
await ensureSourceLoaded(map, LAYERS.buildings.sourceId)
const success = highlightBuildingAtLocation(
location.position.lng,
location.position.lat,
{ easeTo: false, fetchAddress: false },
)
if (success) setSelectedLocation(location)
}
map.once('moveend', handleMoveEnd)
}
Expand Down
14 changes: 12 additions & 2 deletions components/geocode/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,23 @@ interface Props {

type Ref = HTMLDivElement
const Menu = forwardRef<Ref, Props>(
({ suggestions, selectedIndex, errorMessage, onSelectSuggestion, listboxId, errorId }, ref) => {
(
{
suggestions,
selectedIndex,
errorMessage,
onSelectSuggestion,
listboxId,
errorId,
},
ref,
) => {
return (
<Box ref={ref}>
{(suggestions.length > 0 || errorMessage) && (
<Row
id={listboxId}
role="listbox"
role='listbox'
columns={4}
sx={{
position: 'absolute',
Expand Down
4 changes: 2 additions & 2 deletions components/map-layers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const LayersSelector = () => {
<EyeCheckbox
checked={riskRaster}
onChange={(e) => setRiskRaster(e.target.checked)}
aria-label="Toggle risk raster visibility"
aria-label='Toggle risk raster visibility'
/>
<Box
sx={{
Expand All @@ -97,7 +97,7 @@ const LayersSelector = () => {
<EyeCheckbox
checked={satellite}
onChange={(e) => setSatellite(e.target.checked)}
aria-label="Toggle satellite imagery visibility"
aria-label='Toggle satellite imagery visibility'
/>
<Box
sx={{
Expand Down
38 changes: 15 additions & 23 deletions components/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
removeProtocol,
LayerSpecification,
SourceSpecification,
MapSourceDataEvent,
} from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css'
import { Protocol } from 'pmtiles'
Expand All @@ -28,6 +27,7 @@ import {
useMapControlStyles,
} from './'
import { LAYERS } from '@/lib/config'
import { ensureSourceLoaded } from '@/lib/map-utils'
import { getRiskSources, insertRiskLayers } from '@/lib/risk-layers'
import {
getMapViewFromQuery,
Expand All @@ -52,7 +52,9 @@ const MapComponent = () => {
const getInitialZoom = useCallback((): number => {
const width = window.innerWidth
const sidebarBreakpoint = theme?.breakpoints?.[1] ?? '64em'
const hasSidebar = window.matchMedia(`(min-width: ${sidebarBreakpoint})`).matches
const hasSidebar = window.matchMedia(
`(min-width: ${sidebarBreakpoint})`,
).matches
const mapWidth = hasSidebar ? width * (2 / 3) : width
return Math.log2(mapWidth) - 6.3
}, [theme.breakpoints])
Expand Down Expand Up @@ -199,19 +201,11 @@ const MapComponent = () => {
// initial region query
useEffect(() => {
if (!map) return
const handleIdle = () => {
const layerExists = map.getLayer(LAYERS.counties.layerIds.fill)
const sourceLoaded = map.isSourceLoaded('regions')

if (layerExists && sourceLoaded) {
map.off('idle', handleIdle)
updateGeographies()
}
}
map.on('idle', handleIdle)
return () => {
map.off('idle', handleIdle)
const init = async () => {
await ensureSourceLoaded(map, LAYERS.regions.sourceId)
updateGeographies()
}
init()
}, [map, updateGeographies])

useEffect(() => {
Expand All @@ -237,17 +231,15 @@ const MapComponent = () => {
if (!selectionCoordinates) return
const { lat, lng } = selectionCoordinates

const handleSourceData = (e: MapSourceDataEvent) => {
if (e.sourceId === LAYERS.buildings.sourceId && e.isSourceLoaded) {
map.off('sourcedata', handleSourceData)
const found = highlightBuildingAtLocation(lng, lat)
if (!found) {
clearSelections()
updateGeographies()
}
const init = async () => {
await ensureSourceLoaded(map, LAYERS.buildings.sourceId)
const found = highlightBuildingAtLocation(lng, lat)
if (!found) {
clearSelections()
updateGeographies()
}
}
map.on('sourcedata', handleSourceData)
init()
}, [
map,
router.isReady,
Expand Down
12 changes: 7 additions & 5 deletions components/results/download.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,13 @@ export const Download = () => {
document.body.removeChild(a)

setLoading((prev) => ({ ...prev, [format]: false }))
} catch {
track('data_download_error', {
geography: selectedGeographyLevel,
geoid: geoid ?? '',
})
} catch (error) {
if (!(error instanceof DOMException && error.name === 'AbortError')) {
track('data_download_error', {
geography: selectedGeographyLevel,
geoid: geoid ?? '',
})
}
setLoading((prev) => ({ ...prev, [format]: false }))
}
}
Expand Down
3 changes: 2 additions & 1 deletion components/results/other-factors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ const OtherFactors = () => {
The risk described above does not account for several important factors,
including those below, which could influence the actual wildfire risk of
a given location. For more information on these factors, see our{' '}
<Link href='https://carbonplan.org/research/climate-risk-faq'>FAQ</Link>.
<Link href='https://carbonplan.org/research/climate-risk-faq'>FAQ</Link>
.
</Box>
<Table
columns={3}
Expand Down
3 changes: 1 addition & 2 deletions components/zarr-layer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ const ZarrLayer = () => {
const setZarrLoading = useStore((state) => state.setZarrLoading)
const layerRef = useRef<ZarrLayerClass | null>(null)

const riskAttribute =
timePeriod === 'current' ? 'rps_2011' : 'rps_2047'
const riskAttribute = timePeriod === 'current' ? 'rps_2011' : 'rps_2047'

const customFrag = useMemo(() => {
const boundaries = colorLimits.binBoundaries || []
Expand Down
3 changes: 3 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const DATA_URLS = {
}

export const LAYERS = {
regions: {
sourceId: 'regions',
},
buildings: {
layerName: 'risk',
sourceId: 'buildings',
Expand Down
15 changes: 15 additions & 0 deletions lib/map-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Map, MapSourceDataEvent } from 'maplibre-gl'

export const ensureSourceLoaded = (map: Map, sourceId: string) => {
if (map.getSource(sourceId) && map.isSourceLoaded(sourceId))
return Promise.resolve()
return new Promise<void>((resolve) => {
const handleSourceData = (e: MapSourceDataEvent) => {
if (e.sourceId === sourceId && e.isSourceLoaded) {
map.off('sourcedata', handleSourceData)
resolve()
}
}
map.on('sourcedata', handleSourceData)
})
}
3 changes: 1 addition & 2 deletions lib/risk-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ export const getGeographyMedianRiskKey: (
) => (typeof GEOGRAPHY_ATTRIBUTE_KEYS)[keyof typeof GEOGRAPHY_ATTRIBUTE_KEYS] = (
timePeriod: ScenarioKey,
) => {
const key =
timePeriod === 'current' ? 'rps_2011_median' : 'rps_2047_median'
const key = timePeriod === 'current' ? 'rps_2011_median' : 'rps_2047_median'
return GEOGRAPHY_ATTRIBUTE_KEYS[key]
}

Expand Down
8 changes: 6 additions & 2 deletions lib/store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { create } from 'zustand'
import { Map } from 'maplibre-gl'
import { ensureSourceLoaded } from './map-utils'
import { Location, Building, Geography, GeographyKey } from '../types/location'
import { GEOGRAPHY_MIN_ZOOM, LAYERS, RISKS } from './config'
import { clearSelectedBuildingUrl, updateMapViewUrl } from './url-utils'
Expand Down Expand Up @@ -104,7 +105,8 @@ export const useStore = create<Store>((set, get) => ({
selectedGeographyLevel: 'nation',
setSelectedGeographyLevel: (level) => set({ selectedGeographyLevel: level }),
hasManuallySelectedGeography: false,
setHasManuallySelectedGeography: (value) => set({ hasManuallySelectedGeography: value }),
setHasManuallySelectedGeography: (value) =>
set({ hasManuallySelectedGeography: value }),
showGeographyHighlight: false,
setShowGeographyHighlight: (show) => set({ showGeographyHighlight: show }),
geographyLayerVisibility: {
Expand Down Expand Up @@ -141,10 +143,12 @@ export const useStore = create<Store>((set, get) => ({
advancedMode: process.env.NEXT_PUBLIC_ADVANCED_MODE === 'true',
toggleAdvancedMode: () =>
set((state) => ({ advancedMode: !state.advancedMode })),
queryGeographiesAtPoint: (lng: number, lat: number) => {
queryGeographiesAtPoint: async (lng: number, lat: number) => {
const { map } = get()
if (!map) return

await ensureSourceLoaded(map, LAYERS.regions.sourceId)

const zoom = map.getZoom()
const point = map.project([lng, lat])

Expand Down
Loading
Loading