---
title: Measure Tool
source: https://vjmap.com/en/guide/measureTool.html
---

# Measure Tool

CAD is now widely used across various fields and has greatly improved the efficiency of engineers. On the desktop, AutoCAD's measure tools are already very powerful; but on the web, how can we accurately and quickly measure CAD drawings?

# Features

- Open AutoCAD drawings online in the web
- Measure distance
- Measure area
- Measure angle
- Coordinate annotation
- Snap to coordinates on web CAD graphics during measurement for improved accuracy
- Toggle snap on/off during measurement
- Enable orthogonal mode during measurement
- Customize right-click menu during measurement
- Continuous measurement
- Delete measurement results after completion

# Demo


## Measure Distance

![image-20220623192548087](https://vjmap.com/blogimages/image-20220623192548087.png)

## Measure Area

![image-20220623192725011](https://vjmap.com/blogimages/image-20220623192725011.png)

## Measure Angle

![image-20220623193100555](https://vjmap.com/blogimages/image-20220623193100555.png)

## Coordinate Annotation

![image-20220623193145638](https://vjmap.com/blogimages/image-20220623193145638.png)

## Other Features

During measurement: press `Alt` to toggle snap; press `Ctrl` to enable orthogonal mode; press `Backspace` to delete the last point; press `ESC` to cancel; press `Enter` to finish; press `Right-click` to open the context menu

![image-20220623194559185](https://vjmap.com/blogimages/image-20220623194559185.png)


<mapFrameEx>
<pre>
// --Distance measurement--
// Map service object
let svc = new vjmap.Service(env.serviceUrl, env.accessToken)
// Open map
let res = await svc.openMap({
    mapid: env.exampleMapId, // Map ID
    mapopenway: vjmap.MapOpenWay.GeomRender, // Open with geometry render mode
    style: vjmap.openMapDarkStyle() // When div has dark background, pass dark background style here
})
if (res.error) {
    // If open fails
    message.error(res.error)
}
// Get map bounds
let mapExtent = vjmap.GeoBounds.fromString(res.bounds);
// Establish geometric projection coordinate system based on map bounds
let prj = new vjmap.GeoProjection(mapExtent);
// Map object
let map = new vjmap.Map({
    container: 'map', // DIV container ID
    style: svc.rasterStyle(), // Style, raster style here
    center: prj.toLngLat(mapExtent.center()), // Set map center
    zoom: 2, // Set map zoom level
    renderWorldCopies: false, // Don't show multi-screen map
    doubleClickZoom: false // Disable double-click zoom
});
// Associate service object and projection object
map.attach(svc, prj);
// Fit map bounds to full view
map.fitMapBounds();
const addStyle = cssStyle =&gt; {
    let style = `&lt;style&gt;${cssStyle }&lt;/style&gt;`;
    let ele = document.createElement('div');
    ele.innerHTML = style;
    document.getElementsByTagName('head')[0].appendChild(ele.firstElementChild)
}
// Add custom popup style
addStyle(`
                .custom-popup .vjmapgis-popup-content {
                    background-color: #0EFFC2;
                    opacity: 0.7;
                }
                .custom-popup .vjmapgis-popup-tip {
                    border-top-color: #0EFFC2;
                    opacity: 0.7;
                }
            `)
// Popup that follows the mouse
let popup = null;
const setPopupText = text =&gt; {
    if (text) {
        if (!popup) {
            popup = new vjmap.Popup({
                className: "custom-popup",
                closeOnClick: false,
                closeButton: false
            })
                .setHTML(text)
                .setMaxWidth("500px")
                .trackPointer()
                .addTo(map)
        }
        else {
            popup.setHTML(text);
        }
    } else {
        // If empty, remove popup
        if (popup) {
            popup.setLngLat([0,0]); // Cancel trackPointer
            popup.remove();
            popup = null;
        }
    }
}
let snapObj = {}; // Snap entities to use
// Measure distance
const measureDist = async ()=&gt; {
    let isDrawing = false;
    let line = await vjmap.Draw.actionDrawLineSting(map, {
        api: {
            getSnapFeatures: snapObj //Snap data items assigned via features property later
        },
        updatecoordinate: (e) =&gt; {
            if (!e.lnglat) return;
            /*
            // If you need to specify a start point in advance, use the following code
            if (e.state.line.coordinates.length == 1) {
                // Add the initial point as start when first point is placed
                e.state.line.coordinates.unshift(map.toLngLat([
                    587662045.9814225, // cad coordinate x
                    3103973655.658176 // cad coordinate y
                ]))
                e.state.currentVertexPosition = 1;
            }*/
            isDrawing = true;
            const co = map.fromLngLat(e.feature.coordinates[e.feature.coordinates.length - 1]);
            let html = `Current coordinate:&lt;span style="color: #ff0000"&gt; ${co.x.toFixed(2)}, ${co.y.toFixed(2)}&lt;/span&gt;`;
            if (e.feature.coordinates.length == 1) {
                html += "&lt;r/&gt;Please specify the coordinate position for the first measurement point"
            } else {
                let len = e.feature.coordinates.length;
                html += `&lt;br/&gt;Alt: snap; Ctrl: orthogonal; Backspace: delete last point`
                html += `&lt;br/&gt;Distance from last point: &lt;span style="color: #ff0000"&gt;${getDist([e.feature.coordinates[len - 2], e.feature.coordinates[len -1]])}&lt;/span&gt;`
                html += `;  Total distance: &lt;span style="color: #ff0000"&gt;${getDist(e.feature.coordinates)}&lt;/span&gt;`
            }
            setPopupText(html)
        },
        contextMenu: (e) =&gt; {
            new vjmap.ContextMenu({
                event: e.event.originalEvent,
                theme: "dark", //light
                width: "250px",
                items: [
                    {
                        label: 'Confirm',
                        onClick: () =&gt; {
                            // Fire Enter key to finish, simulate pressing Enter
                            map.fire("keyup", {keyCode:13})
                            setPopupText("");
                        }
                    },
                    {
                        label: 'Cancel',
                        onClick: () =&gt; {
                            // Fire ESC key to cancel, simulate pressing ESC
                            map.fire("keyup", {keyCode:27})
                            setPopupText("");
                        }
                    },{
                        label: 'Delete last point',
                        onClick: () =&gt; {
                            // Fire Backspace to delete last point, simulate pressing Backspace
                            map.fire("keyup", {keyCode:8})
                        }
                    },{
                        label: 'End distance measurement',
                        onClick: () =&gt; {
                            // Fire ESC key to cancel, simulate pressing ESC
                            map.fire("keyup", {keyCode:27})
                            isDrawing = false;
                            setPopupText("");
                        }
                    }
                ]
            });
        }
    });
    if (line.cancel) {
        setPopupText("");
        return {
            cancel: true,
            exit: isDrawing === false // If cancelled before drawing, end measurement
        };// Cancel operation
    }
    let color = vjmap.randomColor();
    let polyline = new vjmap.Polyline({
        data: line.features[0].geometry.coordinates,
        lineColor: color,
        lineWidth: 2
    });
    polyline.addTo(map);
    addMarkersToLine(line.features[0].geometry.coordinates, color, polyline.sourceId);
    return {
        polyline
    };
}
// Distance measurement loop, continue until ESC to cancel, otherwise measure next line after finishing one
const measureDistLoop = async ()=&gt; {
    while(true) {
        let res = await measureDist();
        if (res.exit === true) break;
    }
}
measureDistLoop();
const clearAll  = (map)=&gt; {
    // Clear drawn layers; if worried about accidental deletion, record sourceId and marker after each draw, then delete them
    // @ts-ignore
    Object.keys(map.getStyle().sources).filter(e=&gt;e.indexOf("custom_") == 0).forEach(e=&gt; map.removeSourceEx(e));
    // Remove all markers
    map.removeMarkers();
}
// Add measurement result labels to each point on the line
const addMarkersToLine = (coordinates, color, sourceId) =&gt; {
    let markerTexts = [];
    for(let i = 1; i &lt; coordinates.length; i++) {
        let text = new vjmap.Text({
            text: getDist(coordinates.slice(0, i + 1)),
            anchor: "left",
            offset: [3, 0], // x,y pixel offset
            style:{     // Custom style
                'cursor': 'pointer',
                'opacity': 0.8,
                'padding': '6px',
                'border-radius': '12px',
                'background-color': color,
                'border-width': 0,
                'box-shadow': '0px 2px 6px 0px rgba(97,113,166,0.2)',
                'text-align': 'center',
                'font-size': '14px',
                'color': `#${color.substr(1).split("").map(c =&gt; (15 - parseInt(c,16)).toString(16)).join("")}`,
            }
        });
        text.setLngLat(coordinates[i]).addTo(map);
        markerTexts.push(text);
    }
    // Add a delete marker at the first point
    const deletePng = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAbNJREFUWEftlrFOAkEQhv9Z4XgJOCrlTgp5ABOhMBoTCws7EzUaeAx5A0sDiYmFxpLGaAkU2KqFArHi4CU4cMccAQN6HHcIwZi7cnd25tt/5maHsOCPFhwfPoAnBarhQFIIccjAkV3qCMhKKct6q1tym1pPAHVVOWMg6+ScgKuYYR7PBaCmKmw5XgLiy4b5NhzkXVVWP4BXa00zTNcXc21oOR4AjAswaX9M2n4u1yLBIoiSbmV0Zcdc0pqd1HdbWwUWDuDqRjMy8lQDM4o54sYRoJcKq6qHclePKnuScQ0pd/RWt2ztVyOhLUFckIwTvWneDiLYnXdVA18O+r/dcNXXIsozCGsMzuhGJ98DUINpAuUAqmhGe33k/JjiG9g4K2ADUFdDOQan7QAIlI8Z7YwP4CvgK/C/FahFlQcwtm0bEeNFa5qJuSpgdT2AzkFyU290H3udMBzYgBD3gnAQa5iFuQJ4eZB6A8ovW/ETgARLmfIyaPZVSZIQRTBdas326Thwx7fAzRA6SREB2l0x2ndTAfTnwH0CrGk4PinY6D5VQHyhNcwbp3N/eyDxduPprH0FPgHEyH4wGIHUYwAAAABJRU5ErkJggg==";
    let el = document.createElement('div');
    el.className = 'marker';
    el.style.backgroundImage =
        `url(${deletePng})`;
    el.style.width = '20px';
    el.style.height = '20px';
    el.style.backgroundSize = '100%';
    el.style.cursor = "pointer";
    el.addEventListener('click', function (e) {
        map.removeSourceEx(sourceId); // Remove drawn line
        markerTexts.forEach(m =&gt; m.remove());
        markerTexts = [];
        // Extra click, fire Backspace to delete last point, simulate pressing Backspace
        map.fire("keyup", {keyCode:8})
    });
    // Add markers to the map.
    let deleteMarker = new vjmap.Marker({
        element: el,
        anchor: 'right'
    });
    deleteMarker.setLngLat(coordinates[0])
        .setOffset([-5, 0])
        .addTo(map);
    markerTexts.push(deleteMarker)
    // Add coordinates to snap array
    addSnapCoordinates(coordinates);
}
// Get distance value
const getDist = (coordinates) =&gt; {
    let result = vjmap.Math2D.lineDist(map.fromLngLat(coordinates));
    let unit = "m";
    if (result &gt;= 1000) {
        result /= 1000;
        unit = "km";
    } else if (result &lt; 0.01) {
        result *= 100;
        unit = "cm";
    }
    return result.toFixed(2) + " " + unit;
}
// Add snap points
const addSnapCoordinates = (coordinates) =&gt; {
    snapObj.features.push({
        type: "Feature",
        geometry: {
            type: "LineString",
            coordinates: [...coordinates]
        }
    })
}
// Get snap points
if (map.hasVectorLayer()) {
    // Vector style
    // Vector style: get snap points
    snapObj.features = map.queryRenderedFeatures({layers: ['vector-layer-lines']})
} else {
    // Raster style: get coordinate data from server
    // Raster style: get snap points
    // Query all coordinate data, field meanings: https://vjmap.com/en/guide/svrStyleVar.html
    let res = await svc.conditionQueryFeature({ fields:"s3", condition:"s3 != ''", limit: 100000})
    res = res.result.map(e =&gt; e.s3.split(";"))
    snapObj.features = []
    for(let item of res) {
        let coordinates = []
        for(let pt of item) {
            const p = pt.split(",")
            if (p.length &gt;= 2) {
                coordinates.push(map.toLngLat([+p[0], +p[1]]))
            }
        }
        if (coordinates.length == 1) {
            snapObj.features.push({
                type: "Feature",
                geometry: {
                    type: "Point",
                    coordinates: coordinates[0]
                }
            })
        }
        else if (coordinates.length &gt; 1) {
            snapObj.features.push({
                type: "Feature",
                geometry: {
                    type: "LineString",
                    coordinates: coordinates
                }
            })
        }
    }
}
</pre>
</mapFrameEx>
<br/>
<br/>


# Code Implementation

The case code above is open source. Visit ([Weijie Map Cloud Drawing Management Platform](https://vjmap.com/app/cloud) https://vjmap.com/app/cloud), and click `Download this case source code`.

```typescript
import vjmap, { Map } from 'vjmap'
import { sleep } from '~/utils/ui';
import { getMapSnapPoints } from './snap';
let snapObj: any; // Snap entities to use
let curMeasureCmd: string; // Current measure command
export async function runMeasureCmd(map: Map, cmd: string) {
  curMeasureCmd = cmd;
  if (cmd != "measureCancel") {
    // End current measurement first
    await measureCancel(map);
    if (!snapObj) {
        // Get snap points on the map
        snapObj = {};
        getMapSnapPoints(map, snapObj);
    }
  }
  switch (cmd) {
    case "measureDist":
      measureDistLoop(map, snapObj);
      break;
    case "measureArea":
      measureAreaLoop(map, snapObj);
      break;
    case "measureAngle":
      measureAngleLoop(map, snapObj);
      break;
    case "measureCoordinate":
      measureCoordinateLoop(map, snapObj);
      break;
    case "measureCancel":
      await measureCancel(map);
      break;
  }
}

// End drawing
const measureCancel = async (map: Map)=> {
    // Fire cancel key repeatedly: first cancels current draw, second exits measurement
    map.fire("keyup", {keyCode:27});
    await sleep(100);
    map.fire("keyup", {keyCode:27});
    await sleep(100);
    map.setIsInteracting(false); // No longer interacting
}

let popup: vjmap.Popup | null;
const setPopupText = (text: string, map: Map) => {
    if (text) {
        if (!popup) {
            popup = new vjmap.Popup({
                className: "my-custom-popup",
                closeOnClick: false,
                closeButton: false
            })
                .setHTML(text)
                .setMaxWidth("500px")
                .trackPointer()
                .addTo(map)
        }
        else {
            popup.setHTML(text);
        }
    } else {
        // If empty, remove popup
        if (popup) {
            popup.setLngLat([0,0]); // Cancel trackPointer
            popup.remove();
            popup = null;
        }
    }

}
// Distance measurement loop, continue until ESC to cancel, otherwise measure next line after finishing one
const measureDistLoop = async (map: Map, snapObj: any)=> {
    while(true) {
        let res = await measureDist(map, snapObj);
        if (res.exit === true) break;
        if (curMeasureCmd != "measureDist") break;
    }
}

// Measure distance
const measureDist = async (map: Map, snapObj: any)=> {
    let isDrawing = false;
    let line = await vjmap.Draw.actionDrawLineSting(map, {
        api: {
            getSnapFeatures: snapObj //Snap data items assigned via features property later
        },
        updatecoordinate: (e: any) => {
            if (!e.lnglat) return;
            isDrawing = true;
            const co = map.fromLngLat(e.feature.coordinates[e.feature.coordinates.length - 1]);
            let html = `【Measure Distance】Current coordinate:<span style="color: #ff0000"> ${co.x.toFixed(2)}, ${co.y.toFixed(2)}</span>`;
            if (e.feature.coordinates.length == 1) {
                html += "<br/>Please specify the coordinate position for the first measurement point"
            } else {
                let len = e.feature.coordinates.length;
                html += `<br/>Alt: snap; Ctrl: orthogonal; Backspace: delete last point`
                html += `<br/>Distance from last point: <span style="color: #ff0000">${getDist(map, [e.feature.coordinates[len - 2], e.feature.coordinates[len -1]])}</span>`
                html += `;  Total distance: <span style="color: #ff0000">${getDist(map, e.feature.coordinates)}</span>`
            }
            setPopupText(html, map)
        },
        contextMenu: (e: any) => {
            new vjmap.ContextMenu({
                event: e.event.originalEvent,
                theme: "dark", //light
                width: "250px",
                items: [
                    {
                        label: 'Confirm',
                        onClick: () => {
                            // Fire Enter key to finish, simulate pressing Enter
                            map.fire("keyup", {keyCode:13})
                            setPopupText("", map);
                        }
                    },
                    {
                        label: 'Cancel',
                        onClick: () => {
                            // Fire ESC key to cancel, simulate pressing ESC
                            map.fire("keyup", {keyCode:27})
                            setPopupText("", map);
                        }
                    },{
                        label: 'Delete last point',
                        onClick: () => {
                            // Fire Backspace to delete last point, simulate pressing Backspace
                            map.fire("keyup", {keyCode:8})
                        }
                    },{
                        label: 'End distance measurement',
                        onClick: () => {
                            // Fire ESC key to cancel, simulate pressing ESC
                            map.fire("keyup", {keyCode:27})
                            isDrawing = false;
                            setPopupText("", map);
                        }
                    }
                ]
            });

        }
    });
    if (line.cancel) {
        setPopupText("", map);
        return {
            cancel: true,
            exit: isDrawing === false // If cancelled before drawing, end measurement
        };// Cancel operation
    }

    let color = vjmap.randomColor();
    let polyline = new vjmap.Polyline({
        data: line.features[0].geometry.coordinates,
        lineColor: color,
        lineWidth: 2
    });
    polyline.addTo(map);
    addMarkersToLine(map, line.features[0].geometry.coordinates, color, polyline.sourceId || "", snapObj);
    return {
        polyline
    };
}


// Add measurement result labels to each point on the line
const addMarkersToLine = (map: Map, coordinates: Array<[number, number]>, color: string, sourceId: string, snapObj: any) => {
    let markerTexts: any = [];
    for(let i = 1; i < coordinates.length; i++) {
        let text = new vjmap.Text({
            text: getDist(map, coordinates.slice(0, i + 1)),
            anchor: "left",
            offset: [3, 0], // x,y pixel offset
            style:{     // Custom style
                'cursor': 'pointer',
                'opacity': 0.8,
                'padding': '6px',
                'border-radius': '12px',
                'background-color': color,
                'border-width': 0,
                'box-shadow': '0px 2px 6px 0px rgba(97,113,166,0.2)',
                'text-align': 'center',
                'font-size': '14px',
                'color': `#${color.substring(1).split("").map(c => (15 - parseInt(c,16)).toString(16)).join("")}`,
            }
        });
        text.setLngLat(coordinates[i]).addTo(map);
        markerTexts.push(text);
    }
    // Add a delete marker at the first point
    const deletePng = "delete.png";
    let el = document.createElement('div');
    el.className = 'marker';
    el.style.backgroundImage =
        `url(${deletePng})`;
    el.style.width = '20px';
    el.style.height = '20px';
    el.style.backgroundSize = '100%';
    el.style.cursor = "pointer";

    el.addEventListener('click', function (e) {
        map.removeSourceEx(sourceId); // Remove drawn line
        markerTexts.forEach((m: any) => m.remove());
        markerTexts = [];

        // Extra click, fire Backspace to delete last point, simulate pressing Backspace
        map.fire("keyup", {keyCode:8})
    });
    // Add markers to the map.
    let deleteMarker = new vjmap.Marker({
        element: el,
        anchor: 'right'
    });
    deleteMarker.setLngLat(coordinates[0])
        .setOffset([-5, 0])
        .addTo(map);
    markerTexts.push(deleteMarker)

    // Add coordinates to snap array
    addSnapCoordinates(snapObj, coordinates);
}
// Get distance value
const getDist = (map: Map, coordinates: Array<[number, number]>) => {
    let result = vjmap.Math2D.lineDist(map.fromLngLat(coordinates));
    let unit = "m";
    if (result >= 1000) {
        result /= 1000;
        unit = "km";
    } else if (result < 0.01) {
        result *= 100;
        unit = "cm";
    }
    return result.toFixed(2) + " " + unit;
}
// Add snap points
const addSnapCoordinates = (snapObj: any, coordinates: Array<[number, number]>) => {
    snapObj.features.push({
        type: "Feature",
        geometry: {
            type: "LineString",
            coordinates: [...coordinates]
        }
    })
}



// Measure area
const measureArea = async (map: Map, snapObj: any)=> {
    let isDrawing = false;
    let poly = await vjmap.Draw.actionDrawPolygon(map, {
        api: {
            getSnapFeatures: snapObj //Snap data items assigned via features property later
        },
        updatecoordinate: (e: any) => {
            if (!e.lnglat) return;
            isDrawing = true;
            const co = map.fromLngLat(e.feature.coordinates[0][e.feature.coordinates.length - 1]);
            let html = `【Measure Area】Current coordinate:<span style="color: #ff0000"> ${co.x.toFixed(2)}, ${co.y.toFixed(2)}</span>`;
            if (e.feature.coordinates[0].length == 1) {
                html += "<br/>Please specify the coordinate position for the first measurement point"
            } else {
                html += `<br/>Alt: snap; Ctrl: orthogonal; Backspace: delete last point`
                html += `<br/>Current area: <span style="color: #ff0000">${getArea(map, e.feature.coordinates[0])}</span>`
            }
            setPopupText(html, map)
        },
        contextMenu: (e: any) => {
            new vjmap.ContextMenu({
                event: e.event.originalEvent,
                theme: "dark", //light
                width: "250px",
                items: [
                    {
                        label: 'Confirm',
                        onClick: () => {
                            // Fire Enter key to finish, simulate pressing Enter
                            map.fire("keyup", {keyCode:13})
                            setPopupText("", map);
                        }
                    },
                    {
                        label: 'Cancel',
                        onClick: () => {
                            // Fire ESC key to cancel, simulate pressing ESC
                            map.fire("keyup", {keyCode:27})
                            setPopupText("", map);
                        }
                    },{
                        label: 'Delete last point',
                        onClick: () => {
                            // Fire Backspace to delete last point, simulate pressing Backspace
                            map.fire("keyup", {keyCode:8})
                        }
                    },{
                        label: 'End area measurement',
                        onClick: () => {
                            // Fire ESC key to cancel, simulate pressing ESC
                            map.fire("keyup", {keyCode:27})
                            isDrawing = false;
                            setPopupText("", map);
                        }
                    }
                ]
            });

        }
    });
    if (poly.cancel) {
        debugger
        setPopupText("", map);
        return {
            cancel: true,
            exit: isDrawing === false // If cancelled before drawing, end measurement
        };// Cancel operation
    }

    let color = vjmap.randomColor();
    let polygon = new vjmap.Polygon({
        data: poly.features[0].geometry.coordinates[0],
        fillColor: color,
        fillOpacity: 0.4,
        fillOutlineColor: color,
    });
    polygon.addTo(map);
    addMarkersToPolygon(map, poly.features[0].geometry.coordinates[0], color, polygon.sourceId || "", snapObj);
    return {
        polygon
    };
}

// Area measurement loop, continue until ESC to cancel, otherwise measure next polygon after finishing one
const measureAreaLoop = async (map: Map, snapObj: any)=> {
    while(true) {
        let res = await measureArea(map, snapObj);
        if (res.exit === true) break;
        if (curMeasureCmd != "measureArea") break;
    }
}

// Add measurement result labels
const addMarkersToPolygon = (map: Map, coordinates: Array<[number, number]>, color: string, sourceId: string, snapObj: any) => {
    let markerTexts: any = [];
    const center = vjmap.polygonCentroid(map.fromLngLat(coordinates));
    let text = new vjmap.Text({
        text: getArea(map, coordinates),
        anchor: "center",
        offset: [0, 0], // x,y pixel offset
        style:{     // Custom style
            'cursor': 'pointer',
            'opacity': 0.8,
            'padding': '6px',
            'border-radius': '12px',
            'background-color': `#${color.substring(1).split("").map(c => (15 - parseInt(c,16)).toString(16)).join("")}`,
            'border-width': 0,
            'box-shadow': '0px 2px 6px 0px rgba(97,113,166,0.2)',
            'text-align': 'center',
            'font-size': '14px',
            'color': color,
        }
    });
    text.setLngLat(map.toLngLat(center)).addTo(map);
    markerTexts.push(text);
    // Add a delete marker at the first point
    const deletePng = "delete.png";
    let el = document.createElement('div');
    el.className = 'marker';
    el.style.backgroundImage =
        `url(${deletePng})`;
    el.style.width = '20px';
    el.style.height = '20px';
    el.style.backgroundSize = '100%';
    el.style.cursor = "pointer";

    el.addEventListener('click', function (e) {
        map.removeSourceEx(sourceId); // Remove drawn line
        markerTexts.forEach((m: any) => m.remove());
        markerTexts = [];
    });
    // Add markers to the map.
    let deleteMarker = new vjmap.Marker({
        element: el,
        anchor: 'right'
    });
    deleteMarker.setLngLat(coordinates[0])
        .setOffset([-5, 0])
        .addTo(map);
    markerTexts.push(deleteMarker)

    // Add coordinates to snap array
    addSnapCoordinates(snapObj, coordinates);
}
// Get area value
const getArea = (map: Map, coordinates: Array<[number, number]>) => {
    let result = vjmap.calcPolygonArea(map.fromLngLat(coordinates));
    let unit = "m²";
    if (result >= 1e6) {
        result /= 1e6;
        unit = "km²";
    } else if (result < 1.0/1e4) {
        result *= 1e4;
        unit = "cm²";
    }
    return result.toFixed(2) + " " + unit;
}



// Measure angle
const measureAngle = async (map: Map, snapObj: any)=> {
    let isDrawing = false;
    let line = await vjmap.Draw.actionDrawLineSting(map, {
        pointCount: 3,// Only three points needed, auto-finish after drawing three points
        api: {
            getSnapFeatures: snapObj //Snap data items assigned via features property later
        },
        updatecoordinate: (e: any) => {
            if (!e.lnglat) return;
            isDrawing = true;
            const co = map.fromLngLat(e.feature.coordinates[e.feature.coordinates.length - 1]);
            let html = `【Measure Angle】Current coordinate:<span style="color: #ff0000"> ${co.x.toFixed(2)}, ${co.y.toFixed(2)}</span>`;
            if (e.feature.coordinates.length == 1) {
                html += "<br/>Please specify the coordinate position for the first measurement point"
            } else {
                let len = e.feature.coordinates.length;
                html += `<br/>Alt: snap; Ctrl: orthogonal; Backspace: delete last point`
                html += `<br/>Current angle: <span style="color: #ff0000">${getAngle(map, e.feature.coordinates).angle}</span>`
            }
            setPopupText(html, map)
        },
        contextMenu: (e: any) => {
            new vjmap.ContextMenu({
                event: e.event.originalEvent,
                theme: "dark", //light
                width: "250px",
                items: [
                    {
                        label: 'Confirm',
                        onClick: () => {
                            // Fire Enter key to finish, simulate pressing Enter
                            map.fire("keyup", {keyCode:13})
                            setPopupText("", map);
                        }
                    },
                    {
                        label: 'Cancel',
                        onClick: () => {
                            // Fire ESC key to cancel, simulate pressing ESC
                            map.fire("keyup", {keyCode:27})
                            setPopupText("", map);
                        }
                    },{
                        label: 'Delete last point',
                        onClick: () => {
                            // Fire Backspace to delete last point, simulate pressing Backspace
                            map.fire("keyup", {keyCode:8})
                        }
                    },{
                        label: 'End angle measurement',
                        onClick: () => {
                            // Fire ESC key to cancel, simulate pressing ESC
                            map.fire("keyup", {keyCode:27})
                            isDrawing = false;
                            setPopupText("", map);
                        }
                    }
                ]
            });

        }
    });
    if (line.cancel) {
        setPopupText("", map);
        return {
            cancel: true,
            exit: isDrawing === false // If cancelled before drawing, end measurement
        };// Cancel operation
    }

    let color = vjmap.randomColor();
    let polyline = new vjmap.Polyline({
        data: line.features[0].geometry.coordinates,
        lineColor: color,
        lineWidth: 2
    });
    polyline.addTo(map);
    addMarkersToAngle(map, line.features[0].geometry.coordinates, color, polyline.sourceId || "", snapObj);
    return {
        polyline
    };
}

// Angle measurement loop, continue until ESC to cancel, otherwise measure next angle after finishing one
const measureAngleLoop = async (map: Map, snapObj: any)=> {
    while(true) {
        let res = await measureAngle(map, snapObj);
        if (res.exit === true) break;
        if (curMeasureCmd != "measureAngle") break;
    }
}



// Add measurement result labels
const addMarkersToAngle = (map: Map, coordinates: Array<[number, number]>, color: string, sourceId: string, snapObj: any) => {
    if (coordinates.length < 3) return;
    let markerTexts: any = [];
    let points = map.fromLngLat(coordinates);
    let textPoint = coordinates[1];
    let ang = getAngle(map, coordinates);
    // Draw annotation arc
    const cirleArcPath = vjmap.getCirclePolygonCoordinates(
        points[1],
        points[1].distanceTo(points[0]) / 4.0, 36,
        ang.startAngle,  ang.endAngle, false);
    let path = new vjmap.Polyline({
        data: map.toLngLat(cirleArcPath),
        lineColor: color,
        lineWidth: 2
    });
    path.addTo(map);
    markerTexts.push(path)

    // @ts-ignore
    let arcPoints = path.getData().features[0].geometry.coordinates;
    let arcMid = arcPoints[Math.ceil(arcPoints.length / 2)];// Get midpoint
    let textAngle = vjmap.radiansToDegrees(-map.fromLngLat(arcMid).angleTo(points[1])) + 90;
    if (textAngle > 90) textAngle += 180;
    else if (textAngle > 270) textAngle -= 180;
    let text = new vjmap.Text({
        text: ang.angle as string,
        anchor: "center",
        rotation: textAngle,
        offset: [0, 0], // x,y pixel offset
        style:{     // Custom style
            'cursor': 'pointer',
            'opacity': 0.8,
            'padding': '6px',
            'border-radius': '12px',
            'background-color': color,
            'border-width': 0,
            'box-shadow': '0px 2px 6px 0px rgba(97,113,166,0.2)',
            'text-align': 'center',
            'font-size': '14px',
            'color': `#${color.substring(1).split("").map(c => (15 - parseInt(c,16)).toString(16)).join("")}`,
        }
    });
    text.setLngLat(arcMid).addTo(map);
    markerTexts.push(text);
    // Add a delete marker at the first point
    const deletePng = "delete.png";
    let el = document.createElement('div');
    el.className = 'marker';
    el.style.backgroundImage =
        `url(${deletePng})`;
    el.style.width = '20px';
    el.style.height = '20px';
    el.style.backgroundSize = '100%';
    el.style.cursor = "pointer";

    el.addEventListener('click', function (e) {
        map.removeSourceEx(sourceId); // Remove drawn line
        markerTexts.forEach((m: any) => m.remove());
        markerTexts = [];
    });
    // Add markers to the map.
    let deleteMarker = new vjmap.Marker({
        element: el,
        anchor: 'right'
    });
    deleteMarker.setLngLat(coordinates[1])
        .setOffset([-5, 0])
        .addTo(map);
    markerTexts.push(deleteMarker)

    // Add coordinates to snap array
    addSnapCoordinates(snapObj, coordinates);
}
// Get angle value
const getAngle = (map: Map, coordinates: Array<[number, number]>) => {
    let points = map.fromLngLat(coordinates);
    if (points.length < 3) return {  angle: 0.0 }
    let angle1 = points[0].angleTo(points[1]);
    let angle2 = points[2].angleTo(points[1]);
    let angle = angle1 - angle2;
    let deg = vjmap.radiansToDegrees(angle);// Radians to degrees
    let dir = true;
    if (deg < 0) {
        deg = -deg;
        dir = !dir;
    }
    if (deg > 180) {
        deg = 360 - deg;
        dir = !dir;
    }
    let startAngle = !dir ? vjmap.radiansToDegrees(angle1) : vjmap.radiansToDegrees(angle2);
    let endAngle = dir ? vjmap.radiansToDegrees(angle1) : vjmap.radiansToDegrees(angle2);
    startAngle = startAngle < 0 ? 360 + startAngle : startAngle;
    endAngle = endAngle < 0 ? 360 + endAngle : endAngle;
    if (endAngle < startAngle) {
        endAngle += 360;
    }
    return {
        angle: deg.toFixed(2) + "°",
        dir,
        startAngle,
        endAngle
    }
}


// Measure coordinate
const measureCoordinate = async (map: Map, snapObj: any)=> {
    let isDrawing = false;
    let point = await vjmap.Draw.actionDrawPoint(map, {
        api: {
            getSnapFeatures: snapObj //Snap data items assigned via features property later
        },
        updatecoordinate: (e: any) => {
            if (!e.lnglat) return;
            isDrawing = true;
            const co = map.fromLngLat(e.lnglat);
            let html = `【Measure Coordinate】Current coordinate:<span style="color: #ff0000"> ${co.x.toFixed(2)}, ${co.y.toFixed(2)}</span>`;
            setPopupText(html, map)
        },
        contextMenu: (e: any) => {
            new vjmap.ContextMenu({
                event: e.event.originalEvent,
                theme: "dark", //light
                width: "250px",
                items: [
                    {
                        label: 'Confirm',
                        onClick: () => {
                            // Fire Enter key to finish, simulate pressing Enter
                            map.fire("keyup", {keyCode:13})
                            setPopupText("", map);
                        }
                    },
                    {
                        label: 'Cancel',
                        onClick: () => {
                            // Fire ESC key to cancel, simulate pressing ESC
                            map.fire("keyup", {keyCode:27})
                            setPopupText("", map);
                        }
                    },
                    {
                        label: 'End coordinate measurement',
                        onClick: () => {
                            // Fire ESC key to cancel, simulate pressing ESC
                            map.fire("keyup", {keyCode:27})
                            isDrawing = false;
                            setPopupText("", map);
                        }
                    }
                ]
            });

        }
    });
    if (point.cancel) {
        setPopupText("", map);
        return {
            cancel: true,
            exit: isDrawing === false
        };// Cancel operation
    }

    addMarkersToCoord(map, point.features[0].geometry.coordinates);
    return {
        point
    };
}

// Coordinate measurement loop, continue until ESC to cancel
const measureCoordinateLoop = async (map: Map, snapObj: any)=> {
    while(true) {
        let res = await measureCoordinate(map, snapObj);
        if (res.exit === true) break;
        if (curMeasureCmd != "measureCoordinate") break;
    }
}



// Add measurement result labels to point
const addMarkersToCoord = (map: Map, coordinates: [number, number]) => {
    let markerTexts: any = [];
    let co = map.fromLngLat(coordinates);
    let content = `X: ${co.x.toFixed(2)}, Y: ${co.y.toFixed(2)}`
    let marker = createLeaderMarker(map, coordinates, content);
    markerTexts.push(marker);

    // Add a delete marker at the first point
    const deletePng = "delete.png";
    let el = document.createElement('div');
    el.className = 'marker';
    el.style.backgroundImage =
        `url(${deletePng})`;
    el.style.width = '20px';
    el.style.height = '20px';
    el.style.backgroundSize = '100%';
    el.style.cursor = "pointer";

    el.addEventListener('click', function (e) {
        markerTexts.forEach((m: any) => m.remove());
        markerTexts = [];

    });
    // Add markers to the map.
    let deleteMarker = new vjmap.Marker({
        element: el,
        anchor: 'right'
    });
    deleteMarker.setLngLat(coordinates)
        .setOffset([-5, 0])
        .addTo(map);
    markerTexts.push(deleteMarker)

}

// Leader marker
const createLeaderMarker = (map: Map, lnglat: [number, number], content: string) => {
    let el = document.createElement('div');
    el.className = 'marker';
    el.style.position = 'absolute'

    let img = document.createElement("div");
    img.style.backgroundImage = 'bk.png';
    img.style.backgroundRepeat = "no-repeat"
    img.style.height = '37px';
    img.style.width = '100px';
    img.style.position = 'absolute';
    img.style.left = '-3px';
    img.style.bottom = '-3px';
    img.style.right = "0px"
    el.appendChild(img);

    let panel = document.createElement("div");
    panel.style.height = '50px';
    panel.style.width = '350px';
    panel.style.position = 'absolute';
    panel.style.left = '97px';
    panel.style.top = '-60px';
    panel.style.border = "solid 1px #8E0EFF";
    panel.style.background = 'linear-gradient(#00ffff, #00ffff) left top,  linear-gradient(#00ffff, #00ffff) left top,     linear-gradient(#00ffff, #00ffff) right bottom,    linear-gradient(#00ffff, #00ffff) right bottom';
    panel.style.backgroundRepeat = 'no-repeat';
    panel.style.backgroundColor ='rgba(87,255,255, 0.3)'
    panel.style.backgroundSize = '1px 6px, 6px 1px';
    panel.style.fontSize = '18px';
    panel.style.color = '#ffffff';
    panel.innerHTML =  `<div style='margin: 15px 5px 15px 5px'>${content}</div>`;
    el.appendChild(panel);

    // Add markers to the map.
    let marker = new vjmap.Marker({
        element: el,
        anchor: "bottom-left"
    })
    marker.setLngLat(lnglat)
        .addTo(map);
    return marker
}


```
