All examplessymbologyicon-symbology
§ examples · symbology

Icon Symbology

Three inline SVG icons (capital / port / airport) registered via `view.loadIcon(id, url)` and assigned per feature through a CallbackRenderer that reads the city's `category` attribute.

slugicon-symbology
source133 lines
statuslive
tsexamples-src/icon-symbology.ts
1/**
2 * Icon Symbology — custom SVG icons rendered via an offscreen canvas
3 * into an ImageBitmap, then registered with `view.loadIcon(id, bitmap)`.
4 * A CallbackRenderer picks the icon id per feature from `category`.
5 *
6 * Rendering SVG → canvas first (instead of handing the data URL to
7 * `loadIcon(id, url)`) avoids `createImageBitmap` sporadically rejecting
8 * SVG blobs in some browsers.
9 */
10 
11import { MapView } from "mapgpu";
12import { GeoJSONLayer, RasterTileLayer } from "mapgpu/layers";
13import { RenderEngine } from "mapgpu/render";
14import { UniqueValueRenderer } from "mapgpu/core";
15 
16import type { RunResultObject } from "@/components/examples/ExampleCanvas";
17 
18type Category = "capital" | "port" | "airport";
19 
20interface CityProps {
21 name: string;
22 category: Category;
23}
24 
25const DATA = {
26 type: "FeatureCollection" as const,
27 features: [
28 { type: "Feature" as const, properties: { name: "Ankara", category: "capital" }, geometry: { type: "Point" as const, coordinates: [32.866, 39.925] } },
29 { type: "Feature" as const, properties: { name: "Istanbul", category: "port" }, geometry: { type: "Point" as const, coordinates: [28.979, 41.015] } },
30 { type: "Feature" as const, properties: { name: "Izmir", category: "port" }, geometry: { type: "Point" as const, coordinates: [27.140, 38.423] } },
31 { type: "Feature" as const, properties: { name: "Antalya", category: "airport" }, geometry: { type: "Point" as const, coordinates: [30.714, 36.897] } },
32 { type: "Feature" as const, properties: { name: "Trabzon", category: "port" }, geometry: { type: "Point" as const, coordinates: [39.727, 41.005] } },
33 { type: "Feature" as const, properties: { name: "Gaziantep", category: "airport" }, geometry: { type: "Point" as const, coordinates: [37.378, 37.067] } },
34 ],
35};
36 
37const SIZE = 128;
38 
39function svg(path: string, fill: string): string {
40 return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 24 24">
41 <circle cx="12" cy="12" r="11" fill="${fill}" stroke="white" stroke-width="1.5"/>
42 <path d="${path}" fill="white"/>
43 </svg>`;
44}
45 
46const ICONS: Record<Category, string> = {
47 capital: svg("M12 6l2.5 5 5.5 0.8-4 3.9 1 5.3-5-2.6-5 2.6 1-5.3-4-3.9 5.5-0.8z", "#ff5c1a"),
48 port: svg("M12 4l1 4h3l-2.5 2.5L15 14l-3-2-3 2 1.5-3.5L8 8h3z", "#43b0ff"),
49 airport: svg(
50 "M21 16l-7-5V5a2 2 0 0 0-4 0v6l-7 5v2l7-2v5l-2 1v1l3-1 3 1v-1l-2-1v-5l7 2z",
51 "#78dc78",
52 ),
53};
54 
55// Rasterize an SVG to an ImageBitmap through a data-URL → <img> → canvas
56// path. Works across browsers because we use the canvas 2D API instead
57// of forcing createImageBitmap to decode SVG directly.
58async function svgToBitmap(svgSource: string): Promise<ImageBitmap> {
59 const dataUrl =
60 "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svgSource);
61 const img = new Image();
62 img.decoding = "async";
63 img.src = dataUrl;
64 await img.decode();
65 
66 const canvas = document.createElement("canvas");
67 canvas.width = SIZE;
68 canvas.height = SIZE;
69 const ctx = canvas.getContext("2d");
70 if (!ctx) throw new Error("Canvas 2D context unavailable");
71 ctx.drawImage(img, 0, 0, SIZE, SIZE);
72 return await createImageBitmap(canvas);
73}
74 
75export async function run(container: HTMLElement): Promise<RunResultObject> {
76 const view = new MapView({
77 container,
78 renderEngine: new RenderEngine(),
79 mode: "2d",
80 center: [33.0, 39.0],
81 zoom: 5,
82 minZoom: 2,
83 maxZoom: 18,
84 backgroundColor: "transparent",
85 });
86 
87 view.map.add(
88 new RasterTileLayer({
89 id: "carto-light",
90 urlTemplate: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",
91 subdomains: ["a", "b", "c", "d"],
92 maxZoom: 19,
93 attribution: "© CARTO · © OpenStreetMap contributors",
94 }),
95 );
96 
97 // The render engine must be initialised before `loadIcon` can upload
98 // bitmaps to its sprite atlas. `view.when()` resolves after first-frame.
99 await view.when();
100 
101 // Rasterize all icons and register them with the engine.
102 for (const [id, source] of Object.entries(ICONS) as [Category, string][]) {
103 const bitmap = await svgToBitmap(source);
104 await view.loadIcon(id, bitmap);
105 }
106 
107 const cities = new GeoJSONLayer({ id: "cities", data: DATA });
108 cities.renderer = new UniqueValueRenderer({
109 field: "category",
110 defaultSymbol: {
111 type: "icon",
112 src: "capital",
113 color: [255, 255, 255, 255],
114 size: 32,
115 },
116 uniqueValues: (Object.keys(ICONS) as Category[]).map((id) => ({
117 value: id,
118 symbol: {
119 type: "icon",
120 src: id,
121 color: [255, 255, 255, 255],
122 size: 32,
123 },
124 })),
125 });
126 view.map.add(cities);
127 
128 return {
129 dispose: () => view.destroy(),
130 controls: [],
131 };
132}