1
2
3
4
5
6
7
8
9import { MapView } from "mapgpu";
10import { GeoJSONLayer, RasterTileLayer } from "mapgpu/layers";
11import { RenderEngine } from "mapgpu/render";
12import { CallbackRenderer } from "mapgpu/core";
13
14import type { RunResultObject } from "@/components/examples/ExampleCanvas";
15
16
17
18function hotspotPoints(
19 hotspots: Array<{ center: [number, number]; spread: number; weight: number }>,
20 n: number,
21) {
22 const features = [];
23 for (let i = 0; i < n; i++) {
24 const h = hotspots[Math.floor(Math.random() * hotspots.length)]!;
25 const dx = (Math.random() + Math.random() + Math.random() - 1.5) * h.spread;
26 const dy = (Math.random() + Math.random() + Math.random() - 1.5) * h.spread;
27 features.push({
28 id: `p-${i}`,
29 geometry: {
30 type: "Point" as const,
31 coordinates: [h.center[0] + dx, h.center[1] + dy],
32 },
33 attributes: { weight: h.weight },
34 });
35 }
36 return features;
37}
38
39const HOTSPOTS = [
40 { center: [28.979, 41.015] as [number, number], spread: 0.8, weight: 1.0 },
41 { center: [32.866, 39.925] as [number, number], spread: 0.9, weight: 0.8 },
42 { center: [27.140, 38.423] as [number, number], spread: 0.7, weight: 0.6 },
43];
44
45export async function run(container: HTMLElement): Promise<RunResultObject> {
46 const view = new MapView({
47 container,
48 renderEngine: new RenderEngine(),
49 mode: "2d",
50 center: [30, 39.5],
51 zoom: 5,
52 minZoom: 2,
53 maxZoom: 18,
54 backgroundColor: "transparent",
55 });
56
57 const basemap = new RasterTileLayer({
58 id: "carto-dark",
59 urlTemplate: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
60 subdomains: ["a", "b", "c", "d"],
61 maxZoom: 19,
62 attribution: "© CARTO · © OpenStreetMap contributors",
63 });
64 view.map.add(basemap);
65
66 const pointCount = { v: 10000 };
67 const layer = new GeoJSONLayer({
68 id: "heat",
69 data: {
70 type: "FeatureCollection",
71 features: hotspotPoints(HOTSPOTS, pointCount.v),
72 } as unknown as { type: "FeatureCollection"; features: never[] },
73 });
74
75
76 layer.renderer = new CallbackRenderer((feature) => {
77 const w = ((feature.attributes.weight as number) ?? 1) * 0.7;
78 return {
79 type: "simple-marker",
80 color: [255, Math.round(120 + 90 * w), 40, 35],
81 size: 18,
82 };
83 });
84 view.map.add(layer);
85
86 await view.when();
87
88 const rebuild = (size: number) => {
89 layer.renderer = new CallbackRenderer((feature) => {
90 const w = ((feature.attributes.weight as number) ?? 1) * 0.7;
91 return {
92 type: "simple-marker",
93 color: [255, Math.round(120 + 90 * w), 40, 35],
94 size,
95 };
96 });
97 };
98
99 return {
100 dispose: () => view.destroy(),
101 controls: [
102 {
103 kind: "segmented",
104 id: "radius",
105 initial: "18",
106 options: [
107 { value: "10", label: "small" },
108 { value: "18", label: "medium" },
109 { value: "30", label: "large" },
110 ],
111 onChange: (value) => {
112 rebuild(Number(value));
113 },
114 },
115 {
116 kind: "toggle",
117 id: "basemap",
118 labels: ["basemap off", "basemap on"],
119 initial: true,
120 onChange: (on) => {
121 basemap.visible = on;
122 },
123 },
124 ],
125 };
126}