Polygon Edit
Runnable Leaflet example: Polygon Edit. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.
single HTML file · CDN dependencies onlyinterface may contain Chinese labels
Source
<!--
示例:多边形编辑
要点:顶点拖拽改形状、点击地图追加顶点、球面面积计算、顶点图层重建
依赖:Leaflet 1.9.4 · jsDelivr CDN
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="noindex,follow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>多边形编辑</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css">
<style>
html,body{height:100%;margin:0}
body{display:flex;flex-direction:column;overflow:hidden;
font:13px/1.6 -apple-system,"Segoe UI","PingFang SC",sans-serif}
.bar{flex:0 0 auto;padding:8px 12px;background:#f6f7f9;border-bottom:1px solid #e3e6ea;
display:flex;gap:8px;align-items:center;flex-wrap:wrap;font-size:12px;color:#58606e}
.bar button{font:inherit;font-size:12px;padding:4px 10px;border-radius:4px;cursor:pointer;
border:1px solid #ccd1d9;background:#fff;color:#16181d}
.bar button:hover{background:#eef0f3}
.bar button.on{background:#1f5fd0;border-color:#1f5fd0;color:#fff}
.bar b{color:#16181d;font-weight:600;font-variant-numeric:tabular-nums}
#map{flex:1 1 auto;min-height:0}
.vertex{width:11px;height:11px;background:#fff;border:2px solid #1f5fd0;border-radius:2px;
box-shadow:0 0 0 1px rgba(0,0,0,.18)}
</style>
</head>
<body>
<div class="bar">
<button id="add">加点:关</button>
<button id="undo">撤销顶点</button>
<button id="reset">重置</button>
<span>顶点 <b id="n">4</b> · 面积 <b id="area">—</b></span>
<span id="hint">拖动白色方块改形状,点击方块删除该顶点</span>
</div>
<div id="map"></div>
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
(function () {
'use strict';
var map = L.map('map', { center: [39.910, 116.392], zoom: 13 });
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 19,
attribution: 'Tiles © Esri'
}).addTo(map);
var INITIAL = [
[39.900, 116.370],
[39.900, 116.408],
[39.918, 116.412],
[39.921, 116.380]
];
var pts = INITIAL.map(function (p) { return L.latLng(p[0], p[1]); });
var polygon = L.polygon(pts, {
color: '#1f5fd0',
weight: 2,
fillColor: '#1f5fd0',
fillOpacity: 0.12
}).addTo(map);
var vertexLayer = L.layerGroup().addTo(map);
var adding = false;
var nEl = document.getElementById('n');
var areaEl = document.getElementById('area');
/* 球面多边形面积:对每条边做一次带状积分 */
function areaOf(latlngs) {
var R = 6378137;
var rad = Math.PI / 180;
var sum = 0;
for (var i = 0, len = latlngs.length; i < len; i++) {
var p1 = latlngs[i];
var p2 = latlngs[(i + 1) % len];
sum += (p2.lng - p1.lng) * rad * (2 + Math.sin(p1.lat * rad) + Math.sin(p2.lat * rad));
}
return Math.abs(sum * R * R / 2);
}
function fmtArea(m2) {
return m2 > 1e6 ? (m2 / 1e6).toFixed(2) + ' km²' : Math.round(m2) + ' m²';
}
function refresh() {
polygon.setLatLngs(pts);
nEl.textContent = pts.length;
areaEl.textContent = fmtArea(areaOf(pts));
}
/* 顶点图层每次重建:数量少,重建比增量维护简单可靠 */
function buildVertices() {
vertexLayer.clearLayers();
pts.forEach(function (latlng, idx) {
var icon = L.divIcon({
className: '',
html: '<div class="vertex"></div>',
iconSize: [11, 11],
iconAnchor: [5.5, 5.5]
});
var vm = L.marker(latlng, { icon: icon, draggable: true }).addTo(vertexLayer);
vm.on('drag', function (e) {
pts[idx] = e.target.getLatLng();
polygon.setLatLngs(pts);
areaEl.textContent = fmtArea(areaOf(pts));
});
vm.on('dragend', function () {
pts[idx] = vm.getLatLng();
refresh();
});
vm.on('click', function () {
if (pts.length <= 3) {
document.getElementById('hint').textContent = '三角形是下限,再删就没有面了';
return;
}
pts.splice(idx, 1);
refresh();
buildVertices();
document.getElementById('hint').textContent = '已删除顶点,剩余 ' + pts.length + ' 个';
});
});
}
document.getElementById('add').addEventListener('click', function () {
adding = !adding;
this.textContent = '加点:' + (adding ? '开' : '关');
this.classList.toggle('on', adding);
map.getContainer().style.cursor = adding ? 'crosshair' : '';
});
map.on('click', function (e) {
if (!adding) return;
pts.push(e.latlng);
refresh();
buildVertices();
document.getElementById('hint').textContent = '已追加顶点,共 ' + pts.length + ' 个';
});
document.getElementById('undo').addEventListener('click', function () {
if (pts.length <= 3) return;
pts.pop();
refresh();
buildVertices();
});
document.getElementById('reset').addEventListener('click', function () {
pts = INITIAL.map(function (p) { return L.latLng(p[0], p[1]); });
refresh();
buildVertices();
map.fitBounds(polygon.getBounds(), { padding: [40, 40] });
document.getElementById('hint').textContent = '已恢复初始四边形';
});
refresh();
buildVertices();
})();
</script>
</body>
</html>
Language: the code and this page are in English; the full tutorial and API notes for this example are in Chinese. The demo itself is the authoritative version — it runs exactly as shown.