Skip to content

Commit c2be92b

Browse files
feat: Weather playground
1 parent 8fb7e8e commit c2be92b

File tree

9 files changed

+403
-2
lines changed

9 files changed

+403
-2
lines changed

examples/playground/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@navigraph/charts": "*",
1717
"@navigraph/leaflet": "*",
1818
"@navigraph/packages": "*",
19+
"@navigraph/weather": "*",
1920
"@tanstack/react-query": "^5.52.2",
2021
"clsx": "^2.1.1",
2122
"leaflet": "^1.9.4",

examples/playground/src/Root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Auth from "./pages/Auth"
1010
import Charts from "./pages/Charts"
1111
import Packages from "./pages/Packages"
1212
import Tiles from "./pages/Tiles"
13+
import Weather from "./pages/Weather"
1314

1415
export default function Root() {
1516
const { isInitialized } = useNavigraphAuth()
@@ -33,6 +34,7 @@ export default function Root() {
3334
<Route path="/charts" element={<Charts />} />
3435
<Route path="/amdb/*" element={<Amdb />} />
3536
<Route path="/packages" element={<Packages />} />
37+
<Route path="/weather" element={<Weather />} />
3638
</Routes>
3739
<MainWindow />
3840
</main>

examples/playground/src/components/Button.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,24 @@ import { PropsWithChildren } from "react"
44
interface Props {
55
selected?: boolean
66
className?: string
7+
selectedColor?: string
78
disabled?: boolean
89
onClick: () => void
910
}
1011

11-
export default function Button({ selected, onClick, children, className, disabled }: PropsWithChildren<Props>) {
12+
export default function Button({
13+
selected,
14+
onClick,
15+
children,
16+
selectedColor,
17+
className,
18+
disabled,
19+
}: PropsWithChildren<Props>) {
1220
return (
1321
<button
1422
disabled={disabled}
1523
onClick={onClick}
24+
style={{ backgroundColor: selected ? selectedColor : undefined }}
1625
className={clsx(
1726
"p-1 rounded-lg shadow-lg border-blue-gray-400 border-[1px] text-white text-sm font-semibold disabled:text-gray-200",
1827
selected ? "bg-blue-50 enabled:hover:bg-blue-25" : "bg-blue-gray-200 enabled:hover:bg-blue-gray-50",

examples/playground/src/components/SideBar.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import clsx from "clsx"
33
import { IconType } from "react-icons"
44
import { FaDatabase, FaDownload, FaGlobe, FaMap, FaUser } from "react-icons/fa"
55
import { MdOutlineSettings } from "react-icons/md"
6+
import { TiWeatherPartlySunny } from "react-icons/ti"
67
import { NavLink } from "react-router-dom"
78
import { useNavigraphAuth } from "../hooks/useNavigraphAuth"
89

@@ -61,6 +62,9 @@ export default function SideBar() {
6162
<SideBarLink path="/packages" icon={FaDownload} disabled={!user?.scope.includes(Scope.FMSDATA)}>
6263
Packages
6364
</SideBarLink>
65+
<SideBarLink path="/weather" icon={TiWeatherPartlySunny} disabled={!user}>
66+
Weather
67+
</SideBarLink>
6468
</div>
6569
)
6670
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { AVWXSource, getWeatherApi } from "@navigraph/weather"
2+
import { useQueries } from "@tanstack/react-query"
3+
import { circleMarker, GeoJSON as LeafletGeoJSON } from "leaflet"
4+
import { memo, useRef } from "react"
5+
import { renderToString } from "react-dom/server"
6+
import { GeoJSON } from "react-leaflet"
7+
import { useRecoilValue } from "recoil"
8+
import { avwxState } from "../../state/weather"
9+
import JsonView from "../JsonView"
10+
11+
export const weatherColors: Record<AVWXSource, string> = {
12+
AIREP: "rgb(195, 48, 34)",
13+
AIRMET: "rgb(128, 128, 128)",
14+
CWA: "rgb(0, 80, 160)",
15+
GAIRMET: "rgb(160, 96, 0)",
16+
ISIGMET: "rgb(153, 0, 153)",
17+
METAR: "rgb(50, 185, 243)",
18+
SIGMET: "rgb(27, 136, 136)",
19+
}
20+
21+
const Avwx = memo(() => {
22+
const weatherApi = getWeatherApi()
23+
24+
const sources = useRecoilValue(avwxState)
25+
26+
const layers = useQueries({
27+
queries: sources.map(source => ({
28+
queryKey: ["avwx", source],
29+
queryFn: async () => [source, await weatherApi.getAvwxReports([source])] as const,
30+
})),
31+
})
32+
33+
const layersRef = useRef<Partial<Record<AVWXSource, LeafletGeoJSON>>>({})
34+
35+
return layers.map(({ data }) => {
36+
if (!data) return null
37+
38+
return (
39+
<GeoJSON
40+
ref={value => {
41+
layersRef.current[data[0]] = value ?? undefined
42+
}}
43+
key={data[0]}
44+
data={data[1]}
45+
style={{
46+
color: weatherColors[data[0]],
47+
}}
48+
pointToLayer={(feature, latlng) => {
49+
const marker = circleMarker(latlng)
50+
51+
marker.feature = feature
52+
53+
return marker
54+
}}
55+
onEachFeature={(feature, _layer) => {
56+
_layer.on("click", e => {
57+
const target = e.target as LeafletGeoJSON
58+
59+
const feature = target.feature
60+
61+
if (feature?.type === "Feature") {
62+
Object.values(layersRef.current).forEach(layer => {
63+
layer?.resetStyle()
64+
})
65+
66+
target.setStyle({ color: "blue" })
67+
}
68+
})
69+
70+
if (feature.properties) {
71+
_layer.bindPopup(
72+
renderToString(
73+
<div className="flex flex-col items-center gap-2">
74+
<span className="text-ng-background-200">{data[0]}</span>
75+
<JsonView content={feature.properties} />
76+
</div>,
77+
),
78+
)
79+
}
80+
}}
81+
/>
82+
)
83+
})
84+
})
85+
86+
export default Avwx

examples/playground/src/components/map/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
} from "../../state/map"
2222
import Button from "../Button"
2323
import AmdbManager from "./AmdbManager"
24+
import Avwx from "./avwx"
25+
import WeatherRouteManager from "./weatherRoute"
2426

2527
/**
2628
* Creates a Navigraph tiles preset config based on the 4 properties needed
@@ -154,7 +156,7 @@ export default function MapPane() {
154156
<MapContainer
155157
center={[51.505, -0.09]}
156158
zoom={13}
157-
className="h-screen bg-black"
159+
className="h-screen bg-black cursor-pointer"
158160
zoomControl={false}
159161
ref={mapRef}
160162
whenReady={() => {
@@ -170,6 +172,8 @@ export default function MapPane() {
170172
/>
171173
))}
172174
{user?.scope.includes(Scope.CHARTS) && <ChartOverlay />}
175+
<WeatherRouteManager />
176+
<Avwx />
173177
{user?.scope.includes(Scope.AMDB) && <AmdbManager />}
174178
</MapContainer>
175179
<OverlayControls />
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { LatLng, LeafletMouseEvent } from "leaflet"
2+
import { useEffect, useState } from "react"
3+
import { Marker, Polyline, useMap } from "react-leaflet"
4+
import { useRecoilState } from "recoil"
5+
import { weatherRouteEditState, weatherRouteState } from "../../state/weather"
6+
7+
export default function WeatherRouteManager() {
8+
const map = useMap()
9+
10+
const [editActive, setEditActive] = useRecoilState(weatherRouteEditState)
11+
12+
const [route, setRoute] = useRecoilState(weatherRouteState)
13+
14+
const [nextPosition, setNextPosition] = useState<LatLng | null>(null)
15+
16+
useEffect(() => {
17+
const clickCallback = (e: LeafletMouseEvent) => {
18+
if (editActive) {
19+
setRoute([...route, e.latlng])
20+
}
21+
}
22+
23+
const mouseMoveCallback = (e: LeafletMouseEvent) => {
24+
if (editActive) {
25+
setNextPosition(e.latlng)
26+
}
27+
}
28+
29+
if (!editActive) {
30+
setNextPosition(null)
31+
}
32+
33+
map.on("click", clickCallback)
34+
map.on("mousemove", mouseMoveCallback)
35+
36+
return () => {
37+
map.off("click", clickCallback)
38+
map.off("mousemove", mouseMoveCallback)
39+
}
40+
}, [editActive, map, route, setRoute])
41+
42+
useEffect(() => {
43+
const callback = (e: KeyboardEvent) => {
44+
if (e.key === "Enter" && editActive) {
45+
setEditActive(false)
46+
}
47+
}
48+
49+
document.addEventListener("keydown", callback)
50+
51+
return () => {
52+
document.removeEventListener("keydown", callback)
53+
}
54+
}, [editActive, setEditActive])
55+
56+
return (
57+
<>
58+
<Polyline positions={route} />
59+
{nextPosition && route.length >= 1 && (
60+
<Polyline color="red" positions={[route[route.length - 1], nextPosition]} />
61+
)}
62+
{nextPosition && <Marker position={nextPosition} />}
63+
</>
64+
)
65+
}

0 commit comments

Comments
 (0)