使用MapLibre@5.24.0实现标尺 & 面积测量工具

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MapLibre 标尺 & 面积测量工具</title>

<script src="https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.css" rel="stylesheet" />

<style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: system-ui, -apple-system, sans-serif; }

    #map { width: 100%; height: 100vh; }

    .toolbar {
        position: absolute; top: 20px; left: 20px;
        z-index: 1000; display: flex; gap: 10px;
    }
    .toolbar button {
        padding: 10px 18px;
        background: #fff;
        border: 2px solid #555;
        border-radius: 8px;
        cursor: pointer;
        font-size: 14px;
        font-weight: 600;
        color: #333;
        box-shadow: 0 2px 8px rgba(0,0,0,.15);
        transition: all .2s;
        user-select: none;
    }
    .toolbar button:hover { background: #f5f5f5; }

    .toolbar button.measure-btn.active {
        background: #007cbf;
        border-color: #007cbf;
        color: #fff;
    }
    .toolbar button.area-btn.active {
        background: #27ae60;
        border-color: #27ae60;
        color: #fff;
    }
    .toolbar button.clear-btn {
        border-color: #e74c3c;
        color: #e74c3c;
    }
    .toolbar button.clear-btn:hover {
        background: #e74c3c;
        color: #fff;
    }

    /* ✅ 测距 Tips:宽度固定 100px,尖角朝下 */
    .tip-box {
        width: 100px;
        text-align: center;
        background: #007cbf;
        color: #fff;
        padding: 6px 0;
        border-radius: 6px;
        font-size: 14px;
        font-weight: bold;
        white-space: nowrap;
        box-shadow: 0 2px 8px rgba(0,0,0,.25);
        position: relative;
    }
    .tip-box::after {
        content: '';
        position: absolute;
        bottom: -8px;
        left: 50%;
        transform: translateX(-50%);
        width: 0; height: 0;
        border-left: 8px solid transparent;
        border-right: 8px solid transparent;
        border-top: 8px solid #007cbf;
    }

    /* ✅ 面积标签:直接显示,不用 Tips(无尖角) */
    .area-label {
        background: #27ae60;
        color: #fff;
        padding: 8px 14px;
        border-radius: 8px;
        font-size: 15px;
        font-weight: bold;
        white-space: nowrap;
        box-shadow: 0 2px 10px rgba(0,0,0,.3);
    }

    .measure-cursor { cursor: crosshair; }
</style>
</head>

<body>
<div id="map"></div>

<div class="toolbar">
    <button class="measure-btn" id="measureBtn" onclick="toggleMeasure()">📏 多点测距</button>
    <button class="area-btn" id="areaBtn" onclick="toggleArea()">📐 面积测量</button>
    <button class="clear-btn" onclick="clearAll()">🗑️ 清除</button>
</div>

<script>
// =====================================================================
//   MapLibre 标尺 & 面积测量工具(无面板 / 无文字提示版)
// =====================================================================

// ========== 地图 ==========
var map = new maplibregl.Map({
    container: 'map',
    style: 'https://demotiles.maplibre.org/style.json',
    center: [116.397, 39.908],
    zoom: 12
});

// ========== 状态 ==========
var state = {
    mode: null,       // 'measure' | 'area' | null
    finished: false,
    points: [],
    segDistances: [],
    history: { markers: [], layerIds: [], sourceIds: [] }
};

var measureBtn = document.getElementById('measureBtn');
var areaBtn    = document.getElementById('areaBtn');

// ========== 工具函数 ==========
function toRad(d) { return d * Math.PI / 180; }

function calcDistance(p1, p2) {
    var R = 6371000;
    var phi1 = toRad(p1[1]), phi2 = toRad(p2[1]);
    var dPhi = toRad(p2[1] - p1[1]);
    var dLam = toRad(p2[0] - p1[0]);
    var a = Math.sin(dPhi/2)*Math.sin(dPhi/2) +
            Math.cos(phi1)*Math.cos(phi2)*Math.sin(dLam/2)*Math.sin(dLam/2);
    return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}

function fmtDistance(m) {
    if (m < 1000) return m.toFixed(2) + ' m';
    return (m/1000).toFixed(3) + ' km';
}

function calcPlanarArea(coords) {
    if (coords.length < 3) return 0;
    var R = 6371000;
    var midLat = 0;
    for (var i = 0; i < coords.length; i++) midLat += coords[i][1];
    midLat /= coords.length;
    var cosLat = Math.cos(toRad(midLat));
    var pts = [];
    for (var i = 0; i < coords.length; i++) {
        pts.push([R * toRad(coords[i][0]) * cosLat, R * toRad(coords[i][1])]);
    }
    var area = 0;
    for (var i = 0; i < pts.length; i++) {
        var j = (i + 1) % pts.length;
        area += pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
    }
    return Math.abs(area) / 2;
}

function fmtArea(m2) {
    if (m2 < 10000) return m2.toFixed(2) + ' m\u00B2';
    if (m2 < 10000000) return (m2/10000).toFixed(4) + ' 公顷';
    return (m2/1000000).toFixed(4) + ' km\u00B2';
}

// ========== 通用控制 ==========
function clearCurrentMode() {
    state.history.markers.forEach(function(m) { m.remove(); });
    state.history.layerIds.forEach(function(id) {
        if (map.getLayer(id)) map.removeLayer(id);
    });
    state.history.sourceIds.forEach(function(id) {
        if (map.getSource(id)) map.removeSource(id);
    });
    if (map.getSource('preview-line')) {
        map.removeLayer('preview-line');
        map.removeSource('preview-line');
    }
    state.history = { markers: [], layerIds: [], sourceIds: [] };
    state.points = [];
    state.segDistances = [];
    state.finished = false;
    state.mode = null;
    measureBtn.classList.remove('active');
    areaBtn.classList.remove('active');
    map.getCanvas().classList.remove('measure-cursor');
}

function clearAll() { clearCurrentMode(); }

function stopDrawing() {
    state.finished = true;
    map.getCanvas().classList.remove('measure-cursor');

    map.off('click', onMapClick);
    map.off('mousemove', onMouseMove);
    map.off('dblclick', onDoubleClick);
    map.off('contextmenu', onRightClick);

    // 实线 -> 虚线
    state.history.layerIds.forEach(function(lid) {
        if (lid.indexOf('line-layer-') === 0 && map.getLayer(lid)) {
            map.setPaintProperty(lid, 'line-dasharray', [2, 2]);
        }
        if (lid.indexOf('polygon-layer-') === 0 && map.getLayer(lid)) {
            map.setPaintProperty(lid, 'line-dasharray', [2, 2]);
        }
    });

    if (map.getSource('preview-line')) {
        map.removeLayer('preview-line');
        map.removeSource('preview-line');
    }

    // ✅ 测距:最后一点 Tips(100px + 尖角)
    if (state.mode === 'measure' && state.points.length >= 2) {
        var lastPt = state.points[state.points.length - 1];
        var total = 0;
        for (var i = 0; i < state.segDistances.length; i++) total += state.segDistances[i];

        var el = document.createElement('div');
        el.className = 'tip-box';
        el.textContent = fmtDistance(total);
        var m = new maplibregl.Marker({ element: el, offset: [0, -35] })
            .setLngLat(lastPt).addTo(map);
        state.history.markers.push(m);
    }

    // ✅ 面积:区域中心直接显示(无 Tips)
    if (state.mode === 'area' && state.points.length >= 3) {
        var coords = state.points.slice().concat([state.points[0]]);
        var uid = Date.now();
        var sid = 'polygon-' + uid;
        var lid = 'polygon-fill-' + uid;
        var olid = 'polygon-outline-' + uid;

        map.addSource(sid, {
            type: 'geojson',
            data: { type: 'Feature', geometry: { type: 'Polygon', coordinates: [coords] } }
        });
        map.addLayer({ id: lid, type: 'fill', source: sid,
            paint: { 'fill-color': '#27ae60', 'fill-opacity': 0.2 } });
        map.addLayer({ id: olid, type: 'line', source: sid,
            paint: { 'line-color': '#27ae60', 'line-width': 2, 'line-dasharray': [2, 2] } });

        state.history.sourceIds.push(sid);
        state.history.layerIds.push(lid);
        state.history.layerIds.push(olid);

        var cx = 0, cy = 0;
        for (var i = 0; i < state.points.length; i++) {
            cx += state.points[i][0];
            cy += state.points[i][1];
        }
        cx /= state.points.length;
        cy /= state.points.length;

        var area = calcPlanarArea(state.points);
        var el = document.createElement('div');
        el.className = 'area-label';
        el.textContent = fmtArea(area);
        var m = new maplibregl.Marker({ element: el })
            .setLngLat([cx, cy]).addTo(map);
        state.history.markers.push(m);
    }
}

// ========== 多点测距 ==========
function toggleMeasure() {
    if (state.mode === 'measure' && state.finished) {
        clearCurrentMode();
        startMeasure();
    } else if (state.mode === 'measure') {
        if (state.points.length >= 2) stopDrawing();
    } else {
        if (state.mode) clearCurrentMode();
        startMeasure();
    }
}

function startMeasure() {
    state.mode = 'measure';
    state.finished = false;
    state.points = [];
    state.segDistances = [];

    measureBtn.classList.add('active');
    map.getCanvas().classList.add('measure-cursor');

    map.on('click', onMapClick);
    map.on('mousemove', onMouseMove);
    map.on('dblclick', onDoubleClick);
    map.on('contextmenu', onRightClick);
}

// ========== 面积测量 ==========
function toggleArea() {
    if (state.mode === 'area' && state.finished) {
        clearCurrentMode();
        startArea();
    } else if (state.mode === 'area') {
        if (state.points.length >= 3) stopDrawing();
    } else {
        if (state.mode) clearCurrentMode();
        startArea();
    }
}

function startArea() {
    state.mode = 'area';
    state.finished = false;
    state.points = [];
    state.segDistances = [];

    areaBtn.classList.add('active');
    map.getCanvas().classList.add('measure-cursor');

    map.on('click', onMapClick);
    map.on('mousemove', onMouseMove);
    map.on('dblclick', onDoubleClick);
    map.on('contextmenu', onRightClick);
}

// ========== 地图事件 ==========
function onMapClick(e) {
    if (state.finished) return;

    var p = [e.lngLat.lng, e.lngLat.lat];
    state.points.push(p);
    addPoint(p, state.points.length);

    if (state.mode === 'measure' && state.points.length >= 2) {
        var d = calcDistance(
            state.points[state.points.length - 2],
            state.points[state.points.length - 1]
        );
        state.segDistances.push(d);
        drawSegment(
            state.points[state.points.length - 2],
            state.points[state.points.length - 1]
        );
    }

    if (state.mode === 'area' && state.points.length >= 3) {
        drawPolygonPreview();
    }
}

function onMouseMove(e) {
    if (state.points.length === 0) return;
    updatePreview([e.lngLat.lng, e.lngLat.lat]);
}

function onDoubleClick(e) {
    e.preventDefault();
    setTimeout(function() {
        if (state.mode === 'measure' && state.points.length >= 2) stopDrawing();
        if (state.mode === 'area' && state.points.length >= 3) stopDrawing();
    }, 50);
}

function onRightClick(e) {
    e.preventDefault();
    if (state.mode === 'measure' && state.points.length >= 2) stopDrawing();
    if (state.mode === 'area' && state.points.length >= 3) stopDrawing();
}

// ========== 点 ==========
function addPoint(coord, idx) {
    var uid = Date.now() + Math.random();
    var sid = 'point-' + uid;
    var lid = 'point-layer-' + uid;

    map.addSource(sid, {
        type: 'geojson',
        data: { type: 'Feature', geometry: { type: 'Point', coordinates: coord } }
    });
    map.addLayer({
        id: lid, type: 'circle', source: sid,
        paint: {
            'circle-radius': idx === 1 ? 3 : 3,
            'circle-color': idx === 1 ? '#2ecc71' : '#e74c3c',
            'circle-stroke-color': '#fff',
            'circle-stroke-width': 2
        }
    });

    state.history.sourceIds.push(sid);
    state.history.layerIds.push(lid);
}

// ========== 测距线段 ==========
function drawSegment(p1, p2) {
    var uid = Date.now();
    var lsid = 'line-' + uid;
    var llid = 'line-layer-' + uid;

    map.addSource(lsid, {
        type: 'geojson',
        data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [p1, p2] } }
    });
    map.addLayer({
        id: llid, type: 'line', source: lsid,
        paint: { 'line-color': '#e74c3c', 'line-width': 2, 'line-dasharray': [2, 0] }
    });
    state.history.sourceIds.push(lsid);
    state.history.layerIds.push(llid);
}

// ========== 面积预览(不闭合) ==========
function drawPolygonPreview() {
    var coords = state.points.slice().concat([state.points[0]]);
    if (!map.getSource('preview-line')) {
        map.addSource('preview-line', {
            type: 'geojson',
            data: { type: 'Feature', geometry: { type: 'LineString', coordinates: coords } }
        });
        map.addLayer({
            id: 'preview-line', type: 'line', source: 'preview-line',
            paint: { 'line-color': '#27ae60', 'line-width': 2, 'line-dasharray': [4, 4] }
        });
    } else {
        map.getSource('preview-line').setData({
            type: 'Feature',
            geometry: { type: 'LineString', coordinates: coords }
        });
    }
}

// ========== 实时预览 ==========
function updatePreview(p) {
    var id = 'preview-line';
    var coords;

    if (state.mode === 'measure') {
        coords = state.points.slice().concat([p]);
    } else if (state.mode === 'area') {
        coords = state.points.slice().concat([p, state.points[0]]);
    } else {
        return;
    }

    if (!map.getSource(id)) {
        map.addSource(id, {
            type: 'geojson',
            data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [] } }
        });
        map.addLayer({
            id: id, type: 'line', source: id,
            paint: {
                'line-color': state.mode === 'area' ? '#27ae60' : '#007cbf',
                'line-width': 2,
                'line-dasharray': [2, 2]
            }
        });
    }

    map.getSource(id).setData({
        type: 'Feature',
        geometry: { type: 'LineString', coordinates: coords }
    });
}
</script>
</body>
</html>

封装成一个类作为工具使用:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MapLibre 封装版标尺 & 面积测量控件</title>
<script src="https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.css" rel="stylesheet" />
<style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: system-ui, -apple-system, sans-serif; }
    #map { width: 100%; height: 100vh; }
</style>
</head>
<body>
<div id="map"></div>

<script>
// ====================== 封装测量控件类 ======================
class MaplibreMeasureControl {
    constructor() {
        // 内部状态,全部挂载实例,不再全局变量
        this.state = {
            mode: null,       // 'measure' | 'area' | null
            finished: false,
            points: [],
            segDistances: [],
            history: { markers: [], layerIds: [], sourceIds: [] }
        };
        this.map = null;
        this.container = null;
        this.measureBtn = null;
        this.areaBtn = null;
        // 绑定事件上下文
        this.toggleMeasure = this.toggleMeasure.bind(this);
        this.toggleArea = this.toggleArea.bind(this);
        this.clearAll = this.clearAll.bind(this);
        this.onMapClick = this.onMapClick.bind(this);
        this.onMouseMove = this.onMouseMove.bind(this);
        this.onDoubleClick = this.onDoubleClick.bind(this);
        this.onRightClick = this.onRightClick.bind(this);
    }

    // 控件标准接口:添加到地图时执行
    onAdd(map) {
        this.map = map;
        // 创建外层容器
        this.container = document.createElement('div');
        this.container.className = 'maplibregl-ctrl maplibregl-ctrl-group';
        this.container.style.display = 'flex';
        this.container.style.flexDirection = 'column';
        this.container.style.gap = '5px';
        this.container.style.padding = '5px';

        // 注入内部样式(隔离不污染全局)
        const style = document.createElement('style');
        style.textContent = `
            .maplibregl-ctrl-group button {
                display: flex;
                align-items: center;
                justify-content: center;
                width: 25px;
                height: 25px;
            }
            .maplibregl-ctrl-group button+button {
                border-top: none;
            }
            .measure-control-btn {
                background: #fff;
                cursor: pointer;
                transition: all .2s;
                user-select: none;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .measure-control-btn svg {
                display: block;
            }
            .measure-control-btn:hover { background: #f5f5f5; }

            /* 测距 Tips:宽度固定 100px,尖角朝下 */
            .tip-box {
                width: 100px;
                text-align: center;
                background: #007cbf;
                color: #fff;
                padding: 6px 0;
                border-radius: 6px;
                font-size: 14px;
                font-weight: bold;
                white-space: nowrap;
                box-shadow: 0 2px 8px rgba(0,0,0,.25);
                position: relative;
            }
            .tip-box::after {
                content: '';
                position: absolute;
                bottom: -8px;
                left: 50%;
                transform: translateX(-50%);
                width: 0; height: 0;
                border-left: 8px solid transparent;
                border-right: 8px solid transparent;
                border-top: 8px solid #007cbf;
            }
            /* 面积标签:直接显示,不用 Tips(无尖角) */
            .area-label {
                background: #27ae60;
                color: #fff;
                padding: 8px 14px;
                border-radius: 8px;
                font-size: 15px;
                font-weight: bold;
                white-space: nowrap;
                box-shadow: 0 2px 10px rgba(0,0,0,.3);
            }
            .measure-cursor { cursor: crosshair !important; }
        `;
        document.head.appendChild(style);

        // 按钮1:多点测距 - 替换为尺子SVG
        this.measureBtn = document.createElement('button');
        this.measureBtn.className = 'measure-control-btn measure';
        this.measureBtn.title = "测量长度";
        this.measureBtn.innerHTML = `<svg t="1575453922172" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5140" width="20" height="20"><path d="M64 335.8v352c0 8.8 6.9 16 15.4 16h865.1c8.5 0 15.4-7.2 15.4-16v-352c0-8.8-6.9-16-15.4-16h-865c-8.6 0-15.5 7.2-15.5 16z m833.2 304H128.8v-256h768.4v256z" p-id="5141"></path><path d="M202.5 577.6h30v62h-30zM320.3 485.3h30v154h-30zM438.1 577.6h30v62h-30zM555.9 485.3h30v154h-30zM673.7 577.6h30v62h-30zM791.5 485.3h30v154h-30z" p-id="5142"></path></svg>`;
        this.measureBtn.addEventListener('click', this.toggleMeasure);

        // 按钮2:面积测量 - 替换为多边形SVG
        this.areaBtn = document.createElement('button');
        this.areaBtn.className = 'measure-control-btn area';
        this.areaBtn.title = "测量面积";
        this.areaBtn.innerHTML = `<svg t="1575453274789" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4964" width="20" height="20"><path d="M947.93351 255.639285c16.063496 0 29.06104-12.998035 29.06104-29.059849L976.99455 73.015967c0-16.062837-12.997545-29.060873-29.06104-29.060873L794.334073 43.955094c-16.034842 0-29.060017 12.998035-29.060017 29.060873L765.274056 120.750131l-94.33837 0c-2.937009-0.451278-5.928256-0.451278-8.864241 0L258.72799 120.750131 258.72799 73.015967c0-16.062837-13.027222-29.060873-29.062063-29.060873L76.067513 43.955094c-16.063496 0-29.062063 12.998035-29.062063 29.060873l0 153.563468c0 16.062837 12.998568 29.059849 29.062063 29.059849l47.737143 0 0 506.581594L76.067513 762.220878c-16.063496 0-29.062063 12.997012-29.062063 29.059849l0 153.563468c0 16.062837 12.998568 29.059849 29.062063 29.059849l153.598414 0c16.034842 0 29.062063-12.997012 29.062063-29.059849l0-47.735188 506.545043 0 0 47.735188c0 16.062837 13.026198 29.059849 29.060017 29.059849l153.599437 0c16.063496 0 29.06104-12.997012 29.06104-29.059849L976.993527 791.281751c0-16.062837-12.997545-29.059849-29.06104-29.059849l-47.737143 0L900.195344 354.582761c0.050144-0.984421 0.050144-1.970888 0-2.955308l0-95.988168L947.93351 255.639285zM823.39716 102.07684l95.473264 0 0 95.441723-95.473264 0L823.39716 102.07684zM181.928783 459.920878c1.707968-1.102101 3.328951-2.394537 4.825086-3.89061l277.168724-277.158391 132.079449 0L181.928783 592.929194 181.928783 459.920878zM105.129576 102.07684l95.473264 0 0 46.62797c-0.014327 0.367367-0.02763 0.734734-0.02763 1.106194s0.014327 0.738827 0.02763 1.106194l0 46.601364-95.473264 0L105.129576 102.07684zM229.665927 255.639285c16.034842 0 29.062063-12.998035 29.062063-29.059849l0-47.707558 123.003375 0L181.928783 378.666272 181.928783 255.639285 229.665927 255.639285zM181.928783 675.116031l496.265511-496.244154 87.079762 0 0 35.606962L217.509574 762.220878l-35.580791 0L181.928783 675.116031zM105.129576 915.784346l0-95.441723 95.473264 0 0 46.600341c-0.014327 0.367367-0.02763 0.735757-0.02763 1.106194s0.014327 0.738827 0.02763 1.106194l0 46.62797L105.129576 915.783323zM842.071217 553.748846 563.784997 832.022641c-2.10912 2.109034-3.823229 4.460592-5.148464 6.965645L426.365717 838.988286l415.7055-415.688467L842.071217 553.748846zM918.870424 915.784346l-95.473264 0 0-95.441723 95.473264 0L918.870424 915.784346zM794.334073 762.220878c-16.034842 0-29.060017 12.997012-29.060017 29.059849l0 47.706535L639.011318 838.987263l203.060922-203.051579 0 126.284171L794.334073 762.219855zM842.071217 341.111958 344.174489 838.988286l-85.446499 0 0-35.795251 547.576186-547.552727 35.76704 0L842.071217 341.111958z" p-id="4965"></path></svg>`;
        this.areaBtn.addEventListener('click', this.toggleArea);

        // 按钮3:清除
        const clearBtn = document.createElement('button');
        clearBtn.className = 'measure-control-btn clear';
        clearBtn.title = "清空";
        clearBtn.innerHTML = `<svg t="1782891788089" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6639" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20"><path d="M899.9936 258.56h-211.5584v-48.9472c0-59.392-48.3328-107.7248-107.7248-107.7248H436.992c-59.392 0-107.7248 48.3328-107.7248 107.7248v48.9472h-198.144c-19.8144 0-35.84 16.0256-35.84 35.84s16.0256 35.84 35.84 35.84h58.6752v434.2272c0 80.6912 65.6384 146.2784 146.2784 146.2784h346.2656c80.6912 0 146.2784-65.6384 146.2784-146.2784V330.24h71.3216c19.8144 0 35.84-16.0256 35.84-35.84s-15.9744-35.84-35.7888-35.84z m-499.0464-48.9472c0-19.8656 16.1792-36.0448 36.0448-36.0448h143.7696c19.8656 0 36.0448 16.1792 36.0448 36.0448v48.9472H400.9472v-48.9472z m356.0448 554.8032c0 41.1648-33.4848 74.5984-74.5984 74.5984H336.128c-41.1648 0-74.5984-33.4848-74.5984-74.5984V330.24h495.5136v434.176z" fill="#34332E" p-id="6640"></path><path d="M412.928 439.6032c-19.8144 0-35.84 16.0256-35.84 35.84v219.2384c0 19.8144 16.0256 35.84 35.84 35.84s35.84-16.0256 35.84-35.84V475.4432c0-19.8144-16.0256-35.84-35.84-35.84zM601.4464 439.6032c-19.8144 0-35.84 16.0256-35.84 35.84v219.2384c0 19.8144 16.0256 35.84 35.84 35.84s35.84-16.0256 35.84-35.84V475.4432c0-19.8144-16.0768-35.84-35.84-35.84z" fill="#34332E" p-id="6641"></path></svg>`;
        clearBtn.addEventListener('click', this.clearAll);

        this.container.appendChild(this.measureBtn);
        this.container.appendChild(this.areaBtn);
        this.container.appendChild(clearBtn);
        return this.container;
    }

    // 控件标准接口:从地图移除时销毁资源
    onRemove() {
        this.clearCurrentMode();
        if (this.container.parentNode) {
            this.container.parentNode.removeChild(this.container);
        }
        this.map = null;
    }

    // ====================== 内部工具函数 ======================
    toRad(d) { return d * Math.PI / 180; }
    calcDistance(p1, p2) {
        const R = 6371000;
        const phi1 = this.toRad(p1[1]), phi2 = this.toRad(p2[1]);
        const dPhi = this.toRad(p2[1] - p1[1]);
        const dLam = this.toRad(p2[0] - p1[0]);
        const a = Math.sin(dPhi/2)*Math.sin(dPhi/2) +
                Math.cos(phi1)*Math.cos(phi2)*Math.sin(dLam/2)*Math.sin(dLam/2);
        return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    }
    fmtDistance(m) {
        if (m < 1000) return m.toFixed(2) + ' m';
        return (m/1000).toFixed(3) + ' km';
    }
    calcPlanarArea(coords) {
        if (coords.length < 3) return 0;
        const R = 6371000;
        let midLat = 0;
        for (let i = 0; i < coords.length; i++) midLat += coords[i][1];
        midLat /= coords.length;
        const cosLat = Math.cos(this.toRad(midLat));
        const pts = [];
        for (let i = 0; i < coords.length; i++) {
            pts.push([R * this.toRad(coords[i][0]) * cosLat, R * this.toRad(coords[i][1])]);
        }
        let area = 0;
        for (let i = 0; i < pts.length; i++) {
            const j = (i + 1) % pts.length;
            area += pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
        }
        return Math.abs(area) / 2;
    }
    fmtArea(m2) {
        if (m2 < 10000) return m2.toFixed(2) + ' m²';
        if (m2 < 10000000) return (m2/10000).toFixed(4) + ' 公顷';
        return (m2/1000000).toFixed(4) + ' km²';
    }

    // ====================== 通用清除/停止绘制 ======================
    clearCurrentMode() {
        // 移除所有标记
        this.state.history.markers.forEach(m => m.remove());
        // 移除图层
        this.state.history.layerIds.forEach(id => {
            if (this.map.getLayer(id)) this.map.removeLayer(id);
        });
        // 移除数据源
        this.state.history.sourceIds.forEach(id => {
            if (this.map.getSource(id)) this.map.removeSource(id);
        });
        // 移除预览线
        if (this.map.getSource('preview-line')) {
            this.map.removeLayer('preview-line');
            this.map.removeSource('preview-line');
        }
        // 重置状态
        this.state.history = { markers: [], layerIds: [], sourceIds: [] };
        this.state.points = [];
        this.state.segDistances = [];
        this.state.finished = false;
        this.state.mode = null;
        // 取消按钮激活
        this.measureBtn.classList.remove('active');
        this.areaBtn.classList.remove('active');
        this.map.getCanvas().classList.remove('measure-cursor');
        // 解绑所有地图事件
        this.map.off('click', this.onMapClick);
        this.map.off('mousemove', this.onMouseMove);
        this.map.off('dblclick', this.onDoubleClick);
        this.map.off('contextmenu', this.onRightClick);
    }

    clearAll() {
        this.clearCurrentMode();
    }

    stopDrawing() {
        this.state.finished = true;
        this.map.getCanvas().classList.remove('measure-cursor');
        // 解绑绘图事件
        this.map.off('click', this.onMapClick);
        this.map.off('mousemove', this.onMouseMove);
        this.map.off('dblclick', this.onDoubleClick);
        this.map.off('contextmenu', this.onRightClick);

        // 已绘线段改为虚线
        this.state.history.layerIds.forEach(lid => {
            if (lid.indexOf('line-layer-') === 0 && this.map.getLayer(lid)) {
                this.map.setPaintProperty(lid, 'line-dasharray', [2, 2]);
            }
            if (lid.indexOf('polygon-layer-') === 0 && this.map.getLayer(lid)) {
                this.map.setPaintProperty(lid, 'line-dasharray', [2, 2]);
            }
        });
        // 删除临时预览线
        if (this.map.getSource('preview-line')) {
            this.map.removeLayer('preview-line');
            this.map.removeSource('preview-line');
        }

        // 测距总长度弹窗
        if (this.state.mode === 'measure' && this.state.points.length >= 2) {
            const lastPt = this.state.points[this.state.points.length - 1];
            let total = 0;
            for (let i = 0; i < this.state.segDistances.length; i++) total += this.state.segDistances[i];
            const el = document.createElement('div');
            el.className = 'tip-box';
            el.textContent = this.fmtDistance(total);
            const m = new maplibregl.Marker({ element: el, offset: [0, -35] })
                .setLngLat(lastPt).addTo(this.map);
            this.state.history.markers.push(m);
        }

        // 面积闭合多边形+中心标签
        if (this.state.mode === 'area' && this.state.points.length >= 3) {
            const coords = this.state.points.slice().concat([this.state.points[0]]);
            const uid = Date.now();
            const sid = 'polygon-' + uid;
            const lid = 'polygon-fill-' + uid;
            const olid = 'polygon-outline-' + uid;
            this.map.addSource(sid, {
                type: 'geojson',
                data: { type: 'Feature', geometry: { type: 'Polygon', coordinates: [coords] } }
            });
            this.map.addLayer({ id: lid, type: 'fill', source: sid,
                paint: { 'fill-color': '#27ae60', 'fill-opacity': 0.2 } });
            this.map.addLayer({ id: olid, type: 'line', source: sid,
                paint: { 'line-color': '#27ae60', 'line-width': 2, 'line-dasharray': [2, 2] } });
            this.state.history.sourceIds.push(sid);
            this.state.history.layerIds.push(lid);
            this.state.history.layerIds.push(olid);

            // 计算中心点
            let cx = 0, cy = 0;
            for (let i = 0; i < this.state.points.length; i++) {
                cx += this.state.points[i][0];
                cy += this.state.points[i][1];
            }
            cx /= this.state.points.length;
            cy /= this.state.points.length;
            const area = this.calcPlanarArea(this.state.points);
            const el = document.createElement('div');
            el.className = 'area-label';
            el.textContent = this.fmtArea(area);
            const m = new maplibregl.Marker({ element: el })
                .setLngLat([cx, cy]).addTo(this.map);
            this.state.history.markers.push(m);
        }
    }

    // ====================== 测距切换逻辑 ======================
    toggleMeasure() {
        if (this.state.mode === 'measure' && this.state.finished) {
            this.clearCurrentMode();
            this.startMeasure();
        } else if (this.state.mode === 'measure') {
            if (this.state.points.length >= 2) this.stopDrawing();
        } else {
            if (this.state.mode) this.clearCurrentMode();
            this.startMeasure();
        }
    }
    startMeasure() {
        this.state.mode = 'measure';
        this.state.finished = false;
        this.state.points = [];
        this.state.segDistances = [];
        this.measureBtn.classList.add('active');
        this.map.getCanvas().classList.add('measure-cursor');
        // 绑定绘图事件
        this.map.on('click', this.onMapClick);
        this.map.on('mousemove', this.onMouseMove);
        this.map.on('dblclick', this.onDoubleClick);
        this.map.on('contextmenu', this.onRightClick);
    }

    // ====================== 面积切换逻辑 ======================
    toggleArea() {
        if (this.state.mode === 'area' && this.state.finished) {
            this.clearCurrentMode();
            this.startArea();
        } else if (this.state.mode === 'area') {
            if (this.state.points.length >= 3) this.stopDrawing();
        } else {
            if (this.state.mode) this.clearCurrentMode();
            this.startArea();
        }
    }
    startArea() {
        this.state.mode = 'area';
        this.state.finished = false;
        this.state.points = [];
        this.state.segDistances = [];
        this.areaBtn.classList.add('active');
        this.map.getCanvas().classList.add('measure-cursor');
        this.map.on('click', this.onMapClick);
        this.map.on('mousemove', this.onMouseMove);
        this.map.on('dblclick', this.onDoubleClick);
        this.map.on('contextmenu', this.onRightClick);
    }

    // ====================== 地图绘图事件 ======================
    onMapClick(e) {
        if (this.state.finished) return;
        const p = [e.lngLat.lng, e.lngLat.lat];
        this.state.points.push(p);
        this.addPoint(p, this.state.points.length);

        // 测距新增线段
        if (this.state.mode === 'measure' && this.state.points.length >= 2) {
            const d = this.calcDistance(
                this.state.points[this.state.points.length - 2],
                this.state.points[this.state.points.length - 1]
            );
            this.state.segDistances.push(d);
            this.drawSegment(
                this.state.points[this.state.points.length - 2],
                this.state.points[this.state.points.length - 1]
            );
        }
        // 面积预览闭合线
        if (this.state.mode === 'area' && this.state.points.length >= 3) {
            this.drawPolygonPreview();
        }
    }

    onMouseMove(e) {
        if (this.state.points.length === 0) return;
        this.updatePreview([e.lngLat.lng, e.lngLat.lat]);
    }

    onDoubleClick(e) {
        e.preventDefault();
        setTimeout(() => {
            if (this.state.mode === 'measure' && this.state.points.length >= 2) this.stopDrawing();
            if (this.state.mode === 'area' && this.state.points.length >= 3) this.stopDrawing();
        }, 50);
    }

    onRightClick(e) {
        e.preventDefault();
        if (this.state.mode === 'measure' && this.state.points.length >= 2) this.stopDrawing();
        if (this.state.mode === 'area' && this.state.points.length >= 3) this.stopDrawing();
    }

    // ====================== 绘制元素工具 ======================
    addPoint(coord, idx) {
        const uid = Date.now() + Math.random();
        const sid = 'point-' + uid;
        const lid = 'point-layer-' + uid;
        this.map.addSource(sid, {
            type: 'geojson',
            data: { type: 'Feature', geometry: { type: 'Point', coordinates: coord } }
        });
        this.map.addLayer({
            id: lid, type: 'circle', source: sid,
            paint: {
                'circle-radius': 3,
                'circle-color': idx === 1 ? '#2ecc71' : '#e74c3c',
                'circle-stroke-color': '#fff',
                'circle-stroke-width': 2
            }
        });
        this.state.history.sourceIds.push(sid);
        this.state.history.layerIds.push(lid);
    }

    drawSegment(p1, p2) {
        const uid = Date.now();
        const lsid = 'line-' + uid;
        const llid = 'line-layer-' + uid;
        this.map.addSource(lsid, {
            type: 'geojson',
            data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [p1, p2] } }
        });
        this.map.addLayer({
            id: llid, type: 'line', source: lsid,
            paint: { 'line-color': '#e74c3c', 'line-width': 2, 'line-dasharray': [2, 0] }
        });
        this.state.history.sourceIds.push(lsid);
        this.state.history.layerIds.push(llid);
    }

    drawPolygonPreview() {
        const coords = this.state.points.slice().concat([this.state.points[0]]);
        if (!this.map.getSource('preview-line')) {
            this.map.addSource('preview-line', {
                type: 'geojson',
                data: { type: 'Feature', geometry: { type: 'LineString', coordinates: coords } }
            });
            this.map.addLayer({
                id: 'preview-line', type: 'line', source: 'preview-line',
                paint: { 'line-color': '#27ae60', 'line-width': 2, 'line-dasharray': [4, 4] }
            });
        } else {
            this.map.getSource('preview-line').setData({
                type: 'Feature',
                geometry: { type: 'LineString', coordinates: coords }
            });
        }
    }

    updatePreview(p) {
        const id = 'preview-line';
        let coords;
        if (this.state.mode === 'measure') {
            coords = this.state.points.slice().concat([p]);
        } else if (this.state.mode === 'area') {
            coords = this.state.points.slice().concat([p, this.state.points[0]]);
        } else {
            return;
        }
        if (!this.map.getSource(id)) {
            this.map.addSource(id, {
                type: 'geojson',
                data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [] } }
            });
            this.map.addLayer({
                id: id, type: 'line', source: id,
                paint: {
                    'line-color': this.state.mode === 'area' ? '#27ae60' : '#007cbf',
                    'line-width': 2,
                    'line-dasharray': [2, 2]
                }
            });
        }
        this.map.getSource(id).setData({
            type: 'Feature',
            geometry: { type: 'LineString', coordinates: coords }
        });
    }
}

// ====================== 地图初始化 & 使用控件 ======================
var map = new maplibregl.Map({
    container: 'map',
    style: 'https://demotiles.maplibre.org/style.json',
    center: [116.397, 39.908],
    zoom: 12
});

// 按需求示例方式使用控件
const measureControl = new MaplibreMeasureControl();
map.addControl(measureControl, 'top-right');
</script>
</body>
</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值