在地理信息系统 (GIS) 开发中,多边形绘制是一个核心功能,广泛应用于区域标注、面积测量、地块划分等场景。本文将详细介绍如何使用 Leaflet.js 结合 Leaflet.Draw 插件,开发一个功能完善的多边形绘制工具,并重点解决开发过程中遇到的双击地图导致无限加载瓦片的问题。

技术选型与优势

核心技术栈

  • Leaflet.js:轻量级开源地图库,仅 38KB 大小,提供完整的地图交互功能
  • Leaflet.Draw:Leaflet 的绘图扩展插件,提供直观的图形绘制界面
  • 高德地图:作为底图服务,提供高精度的国内地图数据

方案优势

  • 纯前端实现,无需后端支持即可完成所有功能
  • 采用球面算法计算面积,考虑地球曲率影响,结果更精准
  • 实时在多边形中心显示面积标签,直观展示测量结果
  • 支持多边形的绘制、编辑、删除全流程操作
  • 响应式设计,适配各种屏幕尺寸

功能实现详解

1. 开发环境搭建

首先需要引入必要的 CSS 和 JavaScript 资源:

<!-- Leaflet核心样式 -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<!-- Leaflet.Draw样式 -->
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />

<!-- Leaflet核心库 -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Leaflet.Draw插件 -->
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>

创建基础 HTML 结构,包括地图容器和操作提示:

<div class="container">
    <h1>多边形绘制工具</h1>
    <div id="info">状态: 就绪</div>
    <div class="instruction">
        操作说明:<br>
        1. 点击左上角"绘制多边形"按钮开始绘制<br>
        2. 在地图上点击添加顶点(至少需要3个顶点)<br>
        3. 双击或点击"完成"按钮闭合多边形<br>
        4. 按Enter键可快速完成绘制<br>
        5. 使用"编辑"工具可调整多边形形状<br>
        6. 使用"删除"工具可移除多边形
    </div>
    <div id="map"></div>
</div>

2. 基础样式设计

为地图和交互元素设计基础样式,重点是面积标签的样式:

#map {
    width: 100%;
    height: 600px;
    border: 1px solid #ccc;
}

.container {
    max-width: 1200px;
    margin: 20px auto;
    padding: 0 20px;
}

#info {
    color: #666;
    margin: 10px 0;
    padding: 8px 12px;
    border-radius: 4px;
    background-color: #f5f5f5;
}

.drawing-mode {
    color: #e53e3e;
    font-weight: bold;
    background-color: #fff8f8;
    border: 1px solid #ffe3e3;
}

.instruction {
    font-size: 14px;
    color: #666;
    margin: 10px 0;
    padding: 10px;
    background-color: #f9f9f9;
    border-radius: 4px;
}

/* 面积标签样式 - 小字体设计 */
.area-label {
    font-size: 12px;
    color: #333;
    background-color: rgba(255, 255, 255, 0.7);
    padding: 2px 6px;
    border-radius: 3px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
    white-space: nowrap;
}

3. 地图初始化与图层管理

初始化地图并配置图层系统,分离多边形图层和标签图层便于管理:

// 初始化地图,中心点设为北京
const map = L.map('map').setView([39.9042, 116.4074], 13);

// 添加高德地图图层
L.tileLayer('https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', {
    subdomains: ['1', '2', '3', '4'],
    attribution: '© 高德地图',
    maxZoom: 18, // 添加最大缩放级别限制
    minZoom: 1   // 添加最小缩放级别限制
}).addTo(map);

// 创建图层组存储所有多边形和面积标签
const polygonsLayer = new L.FeatureGroup();
const labelsLayer = new L.FeatureGroup(); // 用于存储面积标签
map.addLayer(polygonsLayer);
map.addLayer(labelsLayer);

// 跟踪绘图状态和面积标签关联
let isDrawing = false;
let currentDrawHandler = null;
const polygonLabels = new Map(); // 存储多边形与对应标签的映射关系

// 保存原始的双击事件处理函数 - 解决双击报错的关键
const originalDblClick = map._onDblClick;

4. 核心算法实现

4.1 多边形中心坐标计算

实现多边形中心坐标计算函数,为面积标签定位:

// 计算多边形中心坐标
function calculatePolygonCenter(layer) {
    try {
        if (layer.getLatLngs) {
            const points = layer.getLatLngs()[0]; // 获取外环顶点
            if (points && points.length > 2) {
                let latSum = 0, lngSum = 0;
                const numPoints = points.length;
                
                // 计算所有顶点的经纬度平均值
                points.forEach(point => {
                    latSum += point.lat;
                    lngSum += point.lng;
                });
                
                // 返回中心坐标
                return L.latLng(latSum / numPoints, lngSum / numPoints);
            }
        }
        return null;
    } catch (error) {
        console.error('计算中心坐标时出错:', error);
        return null;
    }
}
4.2 多边形面积计算

采用球面鞋带公式计算多边形面积,考虑地球曲率影响:

// 地球半径(米)
const EARTH_RADIUS = 6371000;

// 将角度转换为弧度
function toRadians(degrees) {
    return degrees * Math.PI / 180;
}

// 计算多边形面积(平方米) - 使用球面鞋带公式
function calculatePolygonArea(layer) {
    try {
        if (layer.getLatLngs) {
            const points = layer.getLatLngs()[0];
            if (points && points.length > 2) {
                let area = 0;
                const numPoints = points.length;
                
                for (let i = 0; i < numPoints; i++) {
                    const j = (i + 1) % numPoints;
                    const lat1 = toRadians(points[i].lat);
                    const lon1 = toRadians(points[i].lng);
                    const lat2 = toRadians(points[j].lat);
                    const lon2 = toRadians(points[j].lng);
                    
                    const factor = Math.sin(lat1) * Math.sin(lat2);
                    const term = Math.cos(lat1) * Math.cos(lat2) * Math.cos(lon1 - lon2);
                    area += Math.atan2(Math.sin(lon2 - lon1) * (factor + term), 1 - factor - term);
                }
                
                return Math.abs(area) * EARTH_RADIUS * EARTH_RADIUS;
            }
        }
        return 0;
    } catch (error) {
        console.error('计算面积时出错:', error);
        return 0;
    }
}
4.3 面积单位格式化

根据面积大小自动转换合适的显示单位:

// 格式化面积显示
function formatArea(areaMeters) {
    if (areaMeters < 1000) {
        return `${areaMeters.toFixed(0)} 平方米`;
    } else if (areaMeters < 1000000) {
        return `${(areaMeters / 10000).toFixed(2)} 公顷`;
    } else {
        return `${(areaMeters / 1000000).toFixed(2)} 平方千米`;
    }
}

5. 面积标签管理

实现多边形中心面积标签的创建、更新和删除管理:

// 在多边形中心添加面积标签
function addAreaLabelToCenter(layer) {
    // 移除该多边形已有的标签
    if (polygonLabels.has(layer._leaflet_id)) {
        const oldLabel = polygonLabels.get(layer._leaflet_id);
        labelsLayer.removeLayer(oldLabel);
    }
    
    // 计算中心坐标和面积
    const center = calculatePolygonCenter(layer);
    const areaMeters = calculatePolygonArea(layer);
    const areaText = formatArea(areaMeters);
    
    if (center) {
        // 创建自定义HTML标签
        const label = L.marker(center, {
            icon: L.divIcon({
                className: 'area-label',
                html: `面积: ${areaText}`,
                iconSize: [120, 20], // 自适应大小
                iconAnchor: [60, 10] // 中心点对齐
            }),
            zIndexOffset: 100 // 确保标签显示在多边形上方
        }).addTo(labelsLayer);
        
        // 存储标签与多边形的关联
        polygonLabels.set(layer._leaflet_id, label);
    }
}

6. 绘图控件配置

配置 Leaflet.Draw 控件,仅启用多边形绘制功能:

// 配置绘图控件
const drawControl = new L.Control.Draw({
    position: 'topleft',
    draw: {
        polygon: {
            allowIntersection: false,
            showArea: true,
            shapeOptions: {
                color: '#34a853',
                weight: 2,
                fillColor: '#81c784',
                fillOpacity: 0.3
            },
            metric: true,
            repeatMode: false
        },
        marker: false,
        polyline: false,
        rectangle: false,
        circle: false,
        circlemarker: false,
    },
    edit: {
        featureGroup: polygonsLayer,
        remove: true,
        edit: true,
        selectedPathOptions: {
            color: '#ff5722',
            weight: 3
        }
    }
});
map.addControl(drawControl);

7. 事件处理系统与双击报错解决方案

实现完整的交互事件处理,并重点解决双击地图导致的无限加载瓦片问题:

// 更新状态信息
function updateInfo(text, isDrawingState = false) {
    const infoEl = document.getElementById('info');
    infoEl.textContent = `状态: ${text}`;
    infoEl.className = isDrawingState ? 'drawing-mode' : '';
}

// 绘图开始事件
map.on(L.Draw.Event.DRAWSTART, function (e) {
    isDrawing = true;
    currentDrawHandler = e.handler;
    updateInfo('正在绘制多边形 - 点击添加顶点,双击或闭合起点完成', true);
    
    // 临时替换双击事件处理函数 - 解决双击冲突的关键
    map._onDblClick = function(e) {
        // 完成绘制而不是缩放
        if (currentDrawHandler) {
            const pointsCount = currentDrawHandler._path?._parts[0]?.length || 0;
            if (pointsCount >= 3) {
                currentDrawHandler.completeShape();
                isDrawing = false;
                // 恢复原始双击事件
                map._onDblClick = originalDblClick;
            } else {
                updateInfo('至少需要3个顶点才能形成多边形', true);
            }
        }
    };
});

// 绘图完成事件
map.on(L.Draw.Event.CREATED, function (e) {
    const layer = e.layer;
    const areaMeters = calculatePolygonArea(layer);
    const areaText = formatArea(areaMeters);

    // 添加弹窗显示面积信息
    layer.bindPopup(`面积: ${areaText}`);
    polygonsLayer.addLayer(layer);
    
    // 在中心添加面积标签
    addAreaLabelToCenter(layer);
    
    updateInfo(`多边形绘制完成 (${areaText})`);

    isDrawing = false;
    currentDrawHandler = null;
    
    // 恢复原始的双击事件处理
    map._onDblClick = originalDblClick;
});

// 编辑完成事件 - 更新面积和标签位置
map.on(L.Draw.Event.EDITED, function (e) {
    e.layers.eachLayer(function (layer) {
        const areaMeters = calculatePolygonArea(layer);
        const areaText = formatArea(areaMeters);
        layer.bindPopup(`面积: ${areaText}`);
        
        // 更新中心标签
        addAreaLabelToCenter(layer);
    });
    updateInfo(`已编辑 ${e.layers.getLayers().length} 个多边形`);
});

// 删除事件 - 同时删除对应的标签
map.on(L.Draw.Event.DELETED, function (e) {
    e.layers.eachLayer(function (layer) {
        if (polygonLabels.has(layer._leaflet_id)) {
            const label = polygonLabels.get(layer._leaflet_id);
            labelsLayer.removeLayer(label);
            polygonLabels.delete(layer._leaflet_id);
        }
    });
    updateInfo(`已删除 ${e.layers.getLayers().length} 个多边形`);
});

// 键盘Enter键结束绘制
document.addEventListener('keydown', function (e) {
    if (e.key === 'Enter' && isDrawing && currentDrawHandler) {
        const pointsCount = currentDrawHandler._path?._parts[0]?.length || 0;
        if (pointsCount >= 3) {
            currentDrawHandler.completeShape();
            isDrawing = false;
            // 恢复原始双击事件
            map._onDblClick = originalDblClick;
        } else {
            updateInfo('至少需要3个顶点才能形成多边形', true);
        }
    }
});

// 取消绘制事件
map.on(L.Draw.Event.DRAWSTOP, function () {
    if (isDrawing) {
        updateInfo('绘制已取消');
        isDrawing = false;
        currentDrawHandler = null;
        // 恢复原始的双击事件处理
        map._onDblClick = originalDblClick;
    }
});

// 修复默认完成按钮
setTimeout(() => {
    const finishBtn = document.querySelector('.leaflet-draw-actions a:first-child');
    if (finishBtn) {
        finishBtn.addEventListener('click', function (e) {
            e.stopPropagation();
            if (isDrawing && currentDrawHandler) {
                const pointsCount = currentDrawHandler._path?._parts[0]?.length || 0;
                if (pointsCount >= 3) {
                    currentDrawHandler.completeShape();
                    isDrawing = false;
                    // 恢复原始双击事件
                    map._onDblClick = originalDblClick;
                } else {
                    updateInfo('至少需要3个顶点才能形成多边形', true);
                }
            }
        });
    }
}, 500);

双击报错问题深度解析与解决方案

问题根源

在开发过程中发现,绘制完成后双击地图会出现Uncaught Error: Attempted to load an infinite number of tiles错误,这是由于:

  1. Leaflet 默认的双击事件用于地图缩放
  2. 绘制过程中我们需要将双击事件改为完成绘制功能
  3. 如果事件处理函数没有正确恢复,会导致地图尝试加载超出范围的瓦片

解决方案

  1. 保存原始事件处理函数:

    const originalDblClick = map._onDblClick;
    
  2. 绘图时替换事件处理:
    在绘制开始时,临时替换双击事件用于完成绘制操作

  3. 完整的事件恢复机制:
    在所有可能结束绘制的场景(完成、取消、编辑、按键等)都确保恢复原始双击事件

  4. 添加缩放级别限制:
    在瓦片图层配置中添加最大和最小缩放级别,作为额外的安全保障

完整代码实现

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>多边形绘制工具(中心显示面积)</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
    <link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />

    <style>
        #map {
            width: 100%;
            height: 600px;
            border: 1px solid #ccc;
        }

        .container {
            max-width: 1200px;
            margin: 20px auto;
            padding: 0 20px;
        }

        #info {
            color: #666;
            margin: 10px 0;
            padding: 8px 12px;
            border-radius: 4px;
            background-color: #f5f5f5;
        }

        .drawing-mode {
            color: #e53e3e;
            font-weight: bold;
            background-color: #fff8f8;
            border: 1px solid #ffe3e3;
        }

        .instruction {
            font-size: 14px;
            color: #666;
            margin: 10px 0;
            padding: 10px;
            background-color: #f9f9f9;
            border-radius: 4px;
        }
        
        /* 面积标签样式 */
        .area-label {
            font-size: 12px;
            color: #333;
            background-color: rgba(255, 255, 255, 0.7);
            padding: 2px 6px;
            border-radius: 3px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            white-space: nowrap;
        }
    </style>
</head>

<body>
    <div class="container">
        <h1>多边形绘制工具</h1>
        <div id="info">状态: 就绪</div>
        <div class="instruction">
            操作说明:<br>
            1. 点击左上角"绘制多边形"按钮开始绘制<br>
            2. 在地图上点击添加顶点(至少需要3个顶点)<br>
            3. 双击或点击"完成"按钮闭合多边形<br>
            4. 按Enter键可快速完成绘制<br>
            5. 使用"编辑"工具可调整多边形形状<br>
            6. 使用"删除"工具可移除多边形
        </div>
        <div id="map"></div>
    </div>

    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>

    <script>
        // 初始化地图,中心点设为北京
        const map = L.map('map').setView([39.9042, 116.4074], 13);

        // 添加高德地图图层
        L.tileLayer('https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', {
            subdomains: ['1', '2', '3', '4'],
            attribution: '© 高德地图',
            maxZoom: 18, // 添加最大缩放级别限制
            minZoom: 1   // 添加最小缩放级别限制
        }).addTo(map);

        // 创建图层组存储所有多边形和面积标签
        const polygonsLayer = new L.FeatureGroup();
        const labelsLayer = new L.FeatureGroup(); // 用于存储面积标签
        map.addLayer(polygonsLayer);
        map.addLayer(labelsLayer);

        // 跟踪绘图状态和面积标签关联
        let isDrawing = false;
        let currentDrawHandler = null;
        const polygonLabels = new Map(); // 存储多边形与对应标签的映射关系
        
        // 保存原始的双击事件处理函数
        const originalDblClick = map._onDblClick;

        // 更新状态信息
        function updateInfo(text, isDrawingState = false) {
            const infoEl = document.getElementById('info');
            infoEl.textContent = `状态: ${text}`;
            infoEl.className = isDrawingState ? 'drawing-mode' : '';
        }

        // 地球半径(米)
        const EARTH_RADIUS = 6371000;

        // 将角度转换为弧度
        function toRadians(degrees) {
            return degrees * Math.PI / 180;
        }

        // 计算多边形中心坐标
        function calculatePolygonCenter(layer) {
            try {
                if (layer.getLatLngs) {
                    const points = layer.getLatLngs()[0]; // 获取外环顶点
                    if (points && points.length > 2) {
                        let latSum = 0, lngSum = 0;
                        const numPoints = points.length;
                        
                        // 计算所有顶点的经纬度平均值
                        points.forEach(point => {
                            latSum += point.lat;
                            lngSum += point.lng;
                        });
                        
                        // 返回中心坐标
                        return L.latLng(latSum / numPoints, lngSum / numPoints);
                    }
                }
                return null;
            } catch (error) {
                console.error('计算中心坐标时出错:', error);
                return null;
            }
        }

        // 计算多边形面积(平方米) - 使用球面鞋带公式
        function calculatePolygonArea(layer) {
            try {
                if (layer.getLatLngs) {
                    const points = layer.getLatLngs()[0];
                    if (points && points.length > 2) {
                        let area = 0;
                        const numPoints = points.length;
                        
                        for (let i = 0; i < numPoints; i++) {
                            const j = (i + 1) % numPoints;
                            const lat1 = toRadians(points[i].lat);
                            const lon1 = toRadians(points[i].lng);
                            const lat2 = toRadians(points[j].lat);
                            const lon2 = toRadians(points[j].lng);
                            
                            const factor = Math.sin(lat1) * Math.sin(lat2);
                            const term = Math.cos(lat1) * Math.cos(lat2) * Math.cos(lon1 - lon2);
                            area += Math.atan2(Math.sin(lon2 - lon1) * (factor + term), 1 - factor - term);
                        }
                        
                        return Math.abs(area) * EARTH_RADIUS * EARTH_RADIUS;
                    }
                }
                return 0;
            } catch (error) {
                console.error('计算面积时出错:', error);
                return 0;
            }
        }

        // 格式化面积显示
        function formatArea(areaMeters) {
            if (areaMeters < 1000) {
                return `${areaMeters.toFixed(0)} 平方米`;
            } else if (areaMeters < 1000000) {
                return `${(areaMeters / 10000).toFixed(2)} 公顷`;
            } else {
                return `${(areaMeters / 1000000).toFixed(2)} 平方千米`;
            }
        }

        // 在多边形中心添加面积标签
        function addAreaLabelToCenter(layer) {
            // 移除该多边形已有的标签
            if (polygonLabels.has(layer._leaflet_id)) {
                const oldLabel = polygonLabels.get(layer._leaflet_id);
                labelsLayer.removeLayer(oldLabel);
            }
            
            // 计算中心坐标和面积
            const center = calculatePolygonCenter(layer);
            const areaMeters = calculatePolygonArea(layer);
            const areaText = formatArea(areaMeters);
            
            if (center) {
                // 创建自定义HTML标签
                const label = L.marker(center, {
                    icon: L.divIcon({
                        className: 'area-label',
                        html: `面积: ${areaText}`,
                        iconSize: [120, 20], // 自适应大小
                        iconAnchor: [60, 10] // 中心点对齐
                    }),
                    zIndexOffset: 100 // 确保标签显示在多边形上方
                }).addTo(labelsLayer);
                
                // 存储标签与多边形的关联
                polygonLabels.set(layer._leaflet_id, label);
            }
        }

        // 配置绘图控件
        const drawControl = new L.Control.Draw({
            position: 'topleft',
            draw: {
                polygon: {
                    allowIntersection: false,
                    showArea: true,
                    shapeOptions: {
                        color: '#34a853',
                        weight: 2,
                        fillColor: '#81c784',
                        fillOpacity: 0.3
                    },
                    metric: true,
                    repeatMode: false
                },
                marker: false,
                polyline: false,
                rectangle: false,
                circle: false,
                circlemarker: false,
            },
            edit: {
                featureGroup: polygonsLayer,
                remove: true,
                edit: true,
                selectedPathOptions: {
                    color: '#ff5722',
                    weight: 3
                }
            }
        });
        map.addControl(drawControl);

        // 绘图开始事件
        map.on(L.Draw.Event.DRAWSTART, function (e) {
            isDrawing = true;
            currentDrawHandler = e.handler;
            updateInfo('正在绘制多边形 - 点击添加顶点,双击或闭合起点完成', true);
            
            // 临时替换双击事件处理函数
            map._onDblClick = function(e) {
                // 完成绘制而不是缩放
                if (currentDrawHandler) {
                    const pointsCount = currentDrawHandler._path?._parts[0]?.length || 0;
                    if (pointsCount >= 3) {
                        currentDrawHandler.completeShape();
                        isDrawing = false;
                        // 恢复原始双击事件
                        map._onDblClick = originalDblClick;
                    } else {
                        updateInfo('至少需要3个顶点才能形成多边形', true);
                    }
                }
            };
        });

        // 绘图完成事件
        map.on(L.Draw.Event.CREATED, function (e) {
            const layer = e.layer;
            const areaMeters = calculatePolygonArea(layer);
            const areaText = formatArea(areaMeters);

            // 添加弹窗显示面积信息
            layer.bindPopup(`面积: ${areaText}`);
            polygonsLayer.addLayer(layer);
            
            // 在中心添加面积标签
            addAreaLabelToCenter(layer);
            
            updateInfo(`多边形绘制完成 (${areaText})`);

            isDrawing = false;
            currentDrawHandler = null;
            
            // 恢复原始的双击事件处理
            map._onDblClick = originalDblClick;
        });

        // 编辑完成事件 - 更新面积和标签位置
        map.on(L.Draw.Event.EDITED, function (e) {
            e.layers.eachLayer(function (layer) {
                const areaMeters = calculatePolygonArea(layer);
                const areaText = formatArea(areaMeters);
                layer.bindPopup(`面积: ${areaText}`);
                
                // 更新中心标签
                addAreaLabelToCenter(layer);
            });
            updateInfo(`已编辑 ${e.layers.getLayers().length} 个多边形`);
        });

        // 删除事件 - 同时删除对应的标签
        map.on(L.Draw.Event.DELETED, function (e) {
            e.layers.eachLayer(function (layer) {
                if (polygonLabels.has(layer._leaflet_id)) {
                    const label = polygonLabels.get(layer._leaflet_id);
                    labelsLayer.removeLayer(label);
                    polygonLabels.delete(layer._leaflet_id);
                }
            });
            updateInfo(`已删除 ${e.layers.getLayers().length} 个多边形`);
        });

        // 键盘Enter键结束绘制
        document.addEventListener('keydown', function (e) {
            if (e.key === 'Enter' && isDrawing && currentDrawHandler) {
                const pointsCount = currentDrawHandler._path?._parts[0]?.length || 0;
                if (pointsCount >= 3) {
                    currentDrawHandler.completeShape();
                    isDrawing = false;
                    // 恢复原始双击事件
                    map._onDblClick = originalDblClick;
                } else {
                    updateInfo('至少需要3个顶点才能形成多边形', true);
                }
            }
        });

        // 取消绘制事件
        map.on(L.Draw.Event.DRAWSTOP, function () {
            if (isDrawing) {
                updateInfo('绘制已取消');
                isDrawing = false;
                currentDrawHandler = null;
                // 恢复原始的双击事件处理
                map._onDblClick = originalDblClick;
            }
        });

        // 修复默认完成按钮
        setTimeout(() => {
            const finishBtn = document.querySelector('.leaflet-draw-actions a:first-child');
            if (finishBtn) {
                finishBtn.addEventListener('click', function (e) {
                    e.stopPropagation();
                    if (isDrawing && currentDrawHandler) {
                        const pointsCount = currentDrawHandler._path?._parts[0]?.length || 0;
                        if (pointsCount >= 3) {
                            currentDrawHandler.completeShape();
                            isDrawing = false;
                            // 恢复原始双击事件
                            map._onDblClick = originalDblClick;
                        } else {
                            updateInfo('至少需要3个顶点才能形成多边形', true);
                        }
                    }
                });
            }
        }, 500);
    </script>
</body>

</html>

功能亮点与技术细节

  1. 双图层管理系统:

    • 将多边形和面积标签分离到不同图层,便于独立管理
    • 使用 Map 对象维护多边形与标签的关联关系,确保操作同步
  2. 高精度面积计算:

    • 采用球面鞋带公式,相比平面算法更适合地理空间计算
    • 考虑地球曲率因素,在大区域测量时精度更高
  3. 智能单位转换:

    • 根据面积大小自动切换显示单位(平方米→公顷→平方千米)
    • 保留合适的小数位数,兼顾精度和可读性
  4. 双击事件冲突完美解决方案:

    • 保存原始事件处理函数,实现安全的事件替换与恢复
    • 在所有可能的流程分支中确保事件处理函数正确恢复
    • 添加缩放级别限制作为额外安全保障
  5. 用户体验优化:

    • 面积标签使用小字体设计,避免遮挡地图内容
    • 半透明背景提高标签可读性,同时减少对地图的视觉干扰
    • 提供多种完成绘制的方式(双击、按钮、Enter 键)
    • 实时状态提示,引导用户完成操作

应用场景与扩展方向

本工具可直接应用于以下场景:

  • 房地产行业:地块面积测量与标注
  • 农业领域:农田区域划分与面积统计
  • 城市规划:区域规划与面积核算
  • 自然资源:林地、水域等资源面积测量

未来可扩展的功能方向:

  1. 添加多边形样式自定义(颜色、线宽、填充透明度)
  2. 实现多个多边形的面积总和计算
  3. 添加面积数据导出功能(CSV/JSON)
  4. 集成测距工具,实现多边形周长计算
  5. 添加历史记录功能,支持撤销 / 重做操作

总结

本文详细介绍了基于 Leaflet.js 和 Leaflet.Draw 插件开发多边形绘制工具的全过程,重点实现了在多边形中心显示面积标签的核心功能,并深入分析和解决了开发过程中遇到的双击地图导致无限加载瓦片的问题。

通过事件保存与恢复机制,我们完美解决了绘图与地图默认交互的冲突问题,同时保持了功能的完整性和用户体验的流畅性。该工具具有轻量、高效、易用的特点,可直接应用于各类需要区域标注和面积测量的 WebGIS 项目中。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐