All examplesinteractionsmeasurement
§ examples · interactions

Measurement Tools

MeasureLineTool with haversine segments, geodesic area, live readouts via UnitManager.

slugmeasurement
source123 lines
statuslive
tsexamples-src/measurement.ts
1/**
2 * Measurement Tools — `setupMeasurementTools` registers measure-point /
3 * measure-line / measure-area on the ToolManager, wiring up label
4 * management, preview layer, and a shared UnitManager. Toggle between
5 * the three tools from the control bar; labels place themselves near
6 * the drawn geometry and update on view change.
7 */
8 
9import { MapView, UnitManager } from "mapgpu";
10import { GraphicsLayer, RasterTileLayer } from "mapgpu/layers";
11import { RenderEngine } from "mapgpu/render";
12import { setupMeasurementTools } from "mapgpu/tools";
13 
14import type { RunResultObject } from "@/components/examples/ExampleCanvas";
15 
16type Tool = "measure-point" | "measure-line" | "measure-area";
17 
18export async function run(container: HTMLElement): Promise<RunResultObject> {
19 const view = new MapView({
20 container,
21 renderEngine: new RenderEngine(),
22 mode: "2d",
23 center: [29.0, 41.0],
24 zoom: 10,
25 minZoom: 4,
26 maxZoom: 19,
27 backgroundColor: "transparent",
28 });
29 
30 view.map.add(
31 new RasterTileLayer({
32 id: "osm",
33 urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
34 maxZoom: 19,
35 attribution: "© OpenStreetMap contributors",
36 }),
37 );
38 
39 const measureLayer = new GraphicsLayer({ id: "measurements" });
40 const previewLayer = new GraphicsLayer({ id: "__measure-preview__" });
41 view.map.add(measureLayer);
42 view.map.add(previewLayer);
43 
44 const unitManager = new UnitManager({
45 distanceUnit: "metric",
46 areaUnit: "metric",
47 coordinateFormat: "DD",
48 });
49 
50 view.toolManager.setPreviewLayer(previewLayer);
51 const { labelManager } = setupMeasurementTools(view.toolManager, {
52 measurementLayer: measureLayer,
53 labelContainer: container,
54 unitManager,
55 toScreen: (lon, lat) => view.toScreen?.(lon, lat) ?? null,
56 });
57 view.on("view-change", () => labelManager.updatePositions());
58 
59 await view.when();
60 
61 const state: { tool: Tool; active: boolean } = { tool: "measure-line", active: false };
62 const sync = () => {
63 if (state.active) view.toolManager.activateTool(state.tool);
64 else view.toolManager.deactivateTool();
65 };
66 
67 return {
68 dispose: () => {
69 (labelManager as unknown as { destroy?: () => void }).destroy?.();
70 view.destroy();
71 },
72 controls: [
73 {
74 kind: "segmented",
75 id: "tool",
76 initial: "measure-line",
77 options: [
78 { value: "measure-point", label: "Point" },
79 { value: "measure-line", label: "Distance" },
80 { value: "measure-area", label: "Area" },
81 ],
82 onChange: (value) => {
83 state.tool = value as Tool;
84 sync();
85 },
86 },
87 {
88 kind: "toggle",
89 id: "active",
90 labels: ["idle", "measuring"],
91 initial: false,
92 onChange: (on) => {
93 state.active = on;
94 sync();
95 },
96 },
97 {
98 kind: "segmented",
99 id: "unit",
100 initial: "metric",
101 options: [
102 { value: "metric", label: "Metric" },
103 { value: "imperial", label: "Imperial" },
104 ],
105 onChange: (value) => {
106 unitManager.distanceUnit = value as "metric" | "imperial";
107 unitManager.areaUnit = value as "metric" | "imperial";
108 },
109 },
110 {
111 kind: "button",
112 id: "clear",
113 label: "Clear",
114 onClick: () => {
115 measureLayer.clear();
116 previewLayer.clear();
117 (labelManager as unknown as { clear?: () => void }).clear?.();
118 },
119 },
120 ],
121 };
122}