Skip to content

Commit d0b433e

Browse files
feat: chart overlay
1 parent 77e63e8 commit d0b433e

File tree

4 files changed

+98
-6
lines changed

4 files changed

+98
-6
lines changed

examples/playground/src/components/Map.tsx

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { MapContainer, useMap, TileLayer } from "react-leaflet";
1+
import { MapContainer, useMap, TileLayer, ImageOverlay } from "react-leaflet";
22
import { useEffect, useMemo, useRef } from "react";
3-
import { Map } from "leaflet";
4-
import { useRecoilValue } from "recoil";
3+
import { LatLngBounds, Map } from "leaflet";
4+
import { useRecoilState, useRecoilValue } from "recoil";
55
import { appState } from "../state/app";
66
import { NavigraphRasterSource, NavigraphTheme, NavigraphTileLayer, PresetConfig } from "@navigraph/leaflet";
77
import { userState } from "../state/user";
@@ -10,6 +10,12 @@ import { Scope } from "@navigraph/app";
1010

1111
import "leaflet/dist/leaflet.css"
1212
import { mapFaaState, mapSourceState, mapTacState, mapThemeState } from "../state/mapStyle";
13+
import { chartOverlayOpacityState, chartOverlayState } from "../state/chartOverlay";
14+
import { calculateChartBounds } from "@navigraph/charts";
15+
import { useQuery } from "@tanstack/react-query";
16+
import { protectedPage } from "./protectedPage";
17+
import { TbCircleX } from "react-icons/tb";
18+
import Button from "./Button";
1319

1420
export function createPreset(source: NavigraphRasterSource, theme: NavigraphTheme, faa: boolean, tac: boolean): PresetConfig {
1521
if (source === 'WORLD') {
@@ -22,6 +28,51 @@ export function createPreset(source: NavigraphRasterSource, theme: NavigraphThem
2228
return { source, theme, type: faa ? 'FAA' : 'Navigraph' }
2329
}
2430

31+
const ChartOverlay = protectedPage(({ charts }) => {
32+
const theme = useRecoilValue(mapThemeState);
33+
34+
const opacity = useRecoilValue(chartOverlayOpacityState);
35+
36+
const chart = useRecoilValue(chartOverlayState);
37+
38+
const map = useMap();
39+
40+
const bounds = useMemo(() => {
41+
if (!chart || !chart.is_georeferenced) return;
42+
43+
const { sw, ne } = calculateChartBounds(chart);
44+
45+
return new LatLngBounds(sw, ne);
46+
}, [chart]);
47+
48+
useEffect(() => {
49+
if (bounds) {
50+
map.flyToBounds(bounds)
51+
}
52+
}, [bounds]);
53+
54+
const { data: urls } = useQuery({
55+
queryKey: ['chart-overlay-urls', chart],
56+
queryFn: async () => {
57+
if (!chart) return null;
58+
59+
const blobs = await Promise.all([charts.getChartImage({ chart, theme: 'light' }), charts.getChartImage({ chart, theme: 'dark' })]);
60+
61+
return blobs.map((blob) => blob ? URL.createObjectURL(blob) : null);
62+
}
63+
})
64+
65+
if (!bounds || !urls?.[0] || !urls?.[1]) return null;
66+
67+
return (
68+
<ImageOverlay
69+
url={theme === 'DAY' ? urls[0] : urls[1]}
70+
bounds={bounds}
71+
opacity={opacity}
72+
/>
73+
)
74+
}, [Scope.CHARTS]);
75+
2576
function NavigraphTiles({ auth }: { auth: NavigraphAuth }) {
2677
const map = useMap();
2778

@@ -51,6 +102,28 @@ function NavigraphTiles({ auth }: { auth: NavigraphAuth }) {
51102
return null;
52103
}
53104

105+
function OverlayControls() {
106+
const [opacity, setOpacity] = useRecoilState(chartOverlayOpacityState);
107+
108+
const [chart, setChart] = useRecoilState(chartOverlayState);
109+
110+
if (!chart) return null;
111+
112+
return (
113+
<div className='absolute right-5 top-5 bg-blue-gray-500 z-[999] p-2 rounded-md flex flex-col gap-2'>
114+
<div className="flex justify-between items-center gap-3">
115+
<span className="text-xs">{chart.index_number}: {chart.name}</span>
116+
<TbCircleX className="text-white hover:text-blue-25 cursor-pointer" size={25} onClick={() => setChart(null)} />
117+
</div>
118+
<div className="flex gap-2">
119+
<Button selected={opacity === 1} onClick={() => setOpacity(1)}><span className="text-white text-xs">100%</span></Button>
120+
<Button selected={opacity === 0.9} onClick={() => setOpacity(0.9)}><span className="text-white text-xs">90%</span></Button>
121+
<Button selected={opacity === 0.7} onClick={() => setOpacity(0.7)}><span className="text-white text-xs">70%</span></Button>
122+
</div>
123+
</div>
124+
)
125+
}
126+
54127
export default function MapPane() {
55128
const mapRef = useRef<Map>(null);
56129

@@ -62,6 +135,7 @@ export default function MapPane() {
62135
<MapContainer center={[51.505, -0.09]} zoom={13} className='h-screen' zoomControl={false} ref={mapRef} whenReady={() => {
63136
setInterval(() => mapRef.current?.invalidateSize(), 1000)
64137
}}>
138+
<OverlayControls />
65139
{app && user?.scope.includes(Scope.TILES) ? (
66140
<NavigraphTiles auth={app.auth} />
67141
) : (
@@ -70,6 +144,7 @@ export default function MapPane() {
70144
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
71145
/>
72146
)}
147+
<ChartOverlay />
73148
</MapContainer>
74149
</div>
75150
)

examples/playground/src/components/protectedPage.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ export function protectedPage<P extends {}, S extends Scope[]>(Component: (props
2424
return null
2525
}
2626

27-
const charts = requiredScopes.includes(Scope.CHARTS) ? getChartsAPI() : undefined;
27+
const charts = user.scope.includes(Scope.CHARTS) ? getChartsAPI() : undefined;
2828

29-
return Component({ user, auth: app.auth, charts, ...props } as unknown as any);
29+
30+
return <Component user={user} auth={app.auth} charts={charts} {...props as unknown as any} />;
3031
};
3132
}

examples/playground/src/pages/Charts.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import clsx from "clsx";
1212
import Button from "../components/Button";
1313
import { IoLayers } from "react-icons/io5";
1414
import { LuTableProperties } from "react-icons/lu";
15+
import { useRecoilState } from "recoil";
16+
import { chartOverlayState } from "../state/chartOverlay";
1517

1618
enum ChartCategory {
1719
STAR,
@@ -32,13 +34,15 @@ const categoryKeys = [
3234
function ChartRow({ chart }: { chart: Chart }) {
3335
const [open, setOpen] = useState(false);
3436

37+
const [chartOverlay, setChartOverlay] = useRecoilState(chartOverlayState);
38+
3539
return (
3640
<div className="max-h-96 flex flex-col gap-1">
3741
<div className="py-1 px-4 border-y-[0.5px] border-ng-background-500 flex gap-2 items-center justify-between hover:bg-ng-background-300 cursor-pointer">
3842
<span className="text-sm font-semibold">{chart.name}<br />{chart.index_number}</span>
3943
<div className="flex gap-2">
44+
{chart.is_georeferenced && <Button selected={chartOverlay?.id === chart.id} onClick={() => setChartOverlay(chart)}><IoLayers className="text-gray-25" size={20} /></Button>}
4045
<Button selected={open} onClick={() => setOpen((x) => !x)}><LuTableProperties className="text-gray-25" size={20} /></Button>
41-
<Button onClick={() => null}><IoLayers className="text-gray-25" size={20} /></Button>
4246
</div>
4347
</div>
4448
{open && <JsonView content={chart} />}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Chart } from "@navigraph/charts";
2+
import { atom } from "recoil";
3+
4+
export const chartOverlayState = atom<Chart | null>({
5+
key: 'chartOverlayState',
6+
default: null
7+
});
8+
9+
export const chartOverlayOpacityState = atom<number>({
10+
key: 'chartOverlayOpacityState',
11+
default: 1,
12+
})

0 commit comments

Comments
 (0)