1
2
3
4
5
6
7
8import { MapView } from "mapgpu";
9import { GraphicsLayer, RasterTileLayer } from "mapgpu/layers";
10import { RenderEngine } from "mapgpu/render";
11import { ClassBreaksRenderer } from "mapgpu/core";
12
13import type { RunResultObject } from "@/components/examples/ExampleCanvas";
14
15type OverpassElement =
16 | { type: "node"; id: number; lat: number; lon: number }
17 | {
18 type: "way";
19 id: number;
20 nodes: number[];
21 tags?: Record<string, string>;
22 };
23
24interface OverpassResult {
25 elements: OverpassElement[];
26}
27
28const BUILDING_COLORS: Array<[number, number, number]> = [
29 [120, 170, 220], [100, 160, 200], [90, 180, 160],
30 [180, 200, 120], [220, 200, 100], [220, 160, 80],
31 [210, 110, 60], [180, 60, 60],
32];
33const BUILDING_BREAKS = [0, 4, 8, 12, 18, 25, 40, 80];
34
35async function fetchBuildings(bbox: [number, number, number, number]) {
36 const [minLon, minLat, maxLon, maxLat] = bbox;
37 const query = `[out:json][timeout:15];
38 way[building](${minLat},${minLon},${maxLat},${maxLon});
39 (._;>;);
40 out;`;
41 const res = await fetch("https://overpass-api.de/api/interpreter", {
42 method: "POST",
43 body: `data=${encodeURIComponent(query)}`,
44 });
45 if (!res.ok) throw new Error(`Overpass ${res.status}`);
46 const json = (await res.json()) as OverpassResult;
47
48 const nodes = new Map<number, [number, number]>();
49 for (const el of json.elements) {
50 if (el.type === "node") nodes.set(el.id, [el.lon, el.lat]);
51 }
52
53 const features: unknown[] = [];
54 for (const el of json.elements) {
55 if (el.type !== "way") continue;
56 const coords = el.nodes.map((id) => nodes.get(id)).filter((c): c is [number, number] => !!c);
57 if (coords.length < 4) continue;
58 const heightStr = el.tags?.height ?? el.tags?.["building:levels"];
59 const height = heightStr ? parseFloat(heightStr) : 6;
60 features.push({
61 id: `b-${el.id}`,
62 geometry: { type: "Polygon", coordinates: [coords] },
63 attributes: { height: Number.isFinite(height) ? height : 6 },
64 });
65 }
66 return features;
67}
68
69export async function run(container: HTMLElement): Promise<RunResultObject> {
70 const view = new MapView({
71 container,
72 renderEngine: new RenderEngine(),
73 mode: "3d",
74 center: [32.85, 39.92],
75 zoom: 16,
76 pitch: 55,
77 bearing: -25,
78 backgroundColor: "transparent",
79 globeEffects: {
80 atmosphere: { enabled: true, strength: 1.2, falloff: 3 },
81 },
82 });
83
84 view.map.add(
85 new RasterTileLayer({
86 id: "carto-light",
87 urlTemplate: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",
88 subdomains: ["a", "b", "c", "d"],
89 maxZoom: 19,
90 attribution: "© CARTO · © OpenStreetMap contributors",
91 }),
92 );
93
94 const layer = new GraphicsLayer({ id: "buildings" });
95 layer.renderer = new ClassBreaksRenderer({
96 field: "height",
97 defaultSymbol: {
98 type: "fill-extrusion",
99 color: [...BUILDING_COLORS[2]!, 210] as [number, number, number, number],
100 heightField: "height",
101 ambient: 0.4,
102 shininess: 24,
103 specularStrength: 0.18,
104 },
105 breaks: BUILDING_COLORS.map((c, i) => ({
106 min: BUILDING_BREAKS[i]!,
107 max: BUILDING_BREAKS[i + 1] ?? Infinity,
108 symbol: {
109 type: "fill-extrusion",
110 color: [...c, 210] as [number, number, number, number],
111 heightField: "height",
112 ambient: 0.4,
113 shininess: 24,
114 specularStrength: 0.18,
115 },
116 })),
117 });
118 view.map.add(layer);
119
120 await view.when();
121
122 const loadHere = async () => {
123 const features = await fetchBuildings([32.82, 39.905, 32.88, 39.94]);
124 layer.replaceAll(features as never[]);
125 };
126 void loadHere();
127
128 return {
129 dispose: () => view.destroy(),
130 controls: [
131 { kind: "button", id: "refetch", label: "Refetch buildings", onClick: loadHere },
132 ],
133 };
134}