1
2
3
4
5
6
7import { MapView } from "mapgpu";
8import { GeoJSONLayer, RasterTileLayer } from "mapgpu/layers";
9import { RenderEngine } from "mapgpu/render";
10import { SimpleRenderer } from "mapgpu/core";
11
12import type { RunResultObject } from "@/components/examples/ExampleCanvas";
13
14type LonLat = [number, number];
15
16
17
18
19function greatCircle(a: LonLat, b: LonLat, steps: number): LonLat[] {
20 const rad = Math.PI / 180;
21 const lon1 = a[0] * rad, lat1 = a[1] * rad;
22 const lon2 = b[0] * rad, lat2 = b[1] * rad;
23
24 const p1 = [Math.cos(lat1) * Math.cos(lon1), Math.cos(lat1) * Math.sin(lon1), Math.sin(lat1)];
25 const p2 = [Math.cos(lat2) * Math.cos(lon2), Math.cos(lat2) * Math.sin(lon2), Math.sin(lat2)];
26 const dot = Math.max(-1, Math.min(1, p1[0] * p2[0] + p1[1] * p2[1] + p1[2] * p2[2]));
27 const omega = Math.acos(dot);
28 if (omega < 1e-6) return [a, b];
29 const sinOmega = Math.sin(omega);
30 const out: LonLat[] = [];
31 for (let i = 0; i <= steps; i++) {
32 const t = i / steps;
33 const s1 = Math.sin((1 - t) * omega) / sinOmega;
34 const s2 = Math.sin(t * omega) / sinOmega;
35 const x = s1 * p1[0] + s2 * p2[0];
36 const y = s1 * p1[1] + s2 * p2[1];
37 const z = s1 * p1[2] + s2 * p2[2];
38 const lat = Math.asin(z) / rad;
39 const lon = Math.atan2(y, x) / rad;
40 out.push([lon, lat]);
41 }
42 return out;
43}
44
45const HUBS: Record<string, LonLat> = {
46 IST: [28.979, 41.015],
47 LHR: [-0.454, 51.470],
48 JFK: [-73.779, 40.642],
49 NRT: [140.386, 35.765],
50 DXB: [55.364, 25.253],
51 SYD: [151.177, -33.946],
52};
53
54const ROUTES: [string, string][] = [
55 ["IST", "LHR"],
56 ["IST", "JFK"],
57 ["IST", "NRT"],
58 ["IST", "DXB"],
59 ["IST", "SYD"],
60 ["LHR", "JFK"],
61];
62
63export async function run(container: HTMLElement): Promise<RunResultObject> {
64 const view = new MapView({
65 container,
66 renderEngine: new RenderEngine(),
67 mode: "3d",
68 center: [30, 30],
69 zoom: 2,
70 pitch: 0,
71 bearing: 0,
72 backgroundColor: "transparent",
73 globeEffects: {
74 atmosphere: {
75 enabled: true,
76 colorInner: [0.55, 0.78, 1.0, 0.9],
77 colorOuter: [0.4, 0.6, 1.0, 0.3],
78 strength: 1.4,
79 falloff: 3.0,
80 },
81 },
82 });
83
84 view.map.add(
85 new RasterTileLayer({
86 id: "carto-dark",
87 urlTemplate: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
88 subdomains: ["a", "b", "c", "d"],
89 maxZoom: 19,
90 attribution: "© CARTO · © OpenStreetMap contributors",
91 }),
92 );
93
94
95 const routeFeatures = ROUTES.map(([from, to]) => ({
96 type: "Feature" as const,
97 properties: { from, to },
98 geometry: {
99 type: "LineString" as const,
100 coordinates: greatCircle(HUBS[from]!, HUBS[to]!, 96),
101 },
102 }));
103
104 const routes = new GeoJSONLayer({
105 id: "routes",
106 data: { type: "FeatureCollection", features: routeFeatures },
107 });
108 routes.renderer = new SimpleRenderer({
109 type: "simple-line",
110 color: [255, 92, 26, 230],
111 width: 1.8,
112 style: "solid",
113 });
114 view.map.add(routes);
115
116 const hubFeatures = Object.entries(HUBS).map(([iata, coord]) => ({
117 type: "Feature" as const,
118 properties: { iata },
119 geometry: { type: "Point" as const, coordinates: coord },
120 }));
121 const hubs = new GeoJSONLayer({
122 id: "hubs",
123 data: { type: "FeatureCollection", features: hubFeatures },
124 });
125 hubs.renderer = new SimpleRenderer({
126 type: "simple-marker",
127 color: [255, 210, 120, 250],
128 size: 10,
129 outlineColor: [255, 92, 26, 255],
130 outlineWidth: 2,
131 });
132 view.map.add(hubs);
133
134 await view.when();
135
136 return {
137 dispose: () => view.destroy(),
138 controls: [
139 {
140 kind: "toggle",
141 id: "routes",
142 labels: ["hide routes", "show routes"],
143 initial: true,
144 onChange: (on) => {
145 routes.visible = on;
146 },
147 },
148 ],
149 };
150}