Draw Polygon
Draw and edit polygons with the drawing plugin and GeoJSON output.
single HTML file · CDN dependencies onlyinterface may contain Chinese labels
Source
<!--
示例:绘制多边形
要点:点击加点、双击收尾去重、顶点拖拽实时改形、球面面积、多面共存与逐层删除
依赖:Leaflet 1.9.4 · jsDelivr CDN + Esri 瓦片,无绘图插件
-->
<!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;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:disabled{opacity:.5;cursor:default}
.bar .spacer{flex:1 1 auto}
.bar #status{color:#16181d;font-variant-numeric:tabular-nums}
#map{flex:1 1 auto;min-height:0;cursor:default}
#map.drawing{cursor:crosshair}
.vtx{width:10px;height:10px;border-radius:50%;background:#fff;
border:2px solid #1f5fd0;box-sizing:border-box;cursor:move}
</style>
</head>
<body>
<div class="bar">
<button id="start" type="button">开始绘制</button>
<button id="finish" type="button" disabled>结束</button>
<button id="undo" type="button" disabled>撤销顶点</button>
<button id="clear" type="button">清空</button>
<span class="spacer"></span>
<span id="status">点击「开始绘制」,在地图上逐点落位</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 R = 6378137;
var RAD = Math.PI / 180;
function ringArea(points) {
var sum = 0;
for (var i = 0, n = points.length; i < n; i++) {
var a = points[i];
var b = points[(i + 1) % n];
sum += (b.lng - a.lng) * RAD *
(2 + Math.sin(a.lat * RAD) + Math.sin(b.lat * RAD));
}
return Math.abs(sum * R * R / 2);
}
function formatArea(m2) {
if (m2 >= 1e6) return (m2 / 1e6).toFixed(2) + ' km²';
return Math.round(m2) + ' m²';
}
var map = L.map('map', {
center: [39.917, 116.391],
zoom: 13,
doubleClickZoom: false
});
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 vertexIcon = L.divIcon({
className: '',
html: '<div class="vtx"></div>',
iconSize: [10, 10],
iconAnchor: [5, 5]
});
var style = {
color: '#1f5fd0',
weight: 2,
fillColor: '#1f5fd0',
fillOpacity: 0.18,
interactive: false
};
var mapEl = document.getElementById('map');
var statusEl = document.getElementById('status');
var startBtn = document.getElementById('start');
var finishBtn = document.getElementById('finish');
var undoBtn = document.getElementById('undo');
var clearBtn = document.getElementById('clear');
var drawing = false;
var shape = null; /* 当前形状 */
var shapes = []; /* 已完成的形状 */
var palette = ['#1f5fd0', '#17694a', '#c2410c', '#7c3aed', '#0e7490'];
function newShape() {
return { points: [], markers: [], poly: null };
}
function totalArea() {
var sum = 0;
shapes.forEach(function (s) { sum += ringArea(s.points); });
if (shape && shape.points.length >= 3) sum += ringArea(shape.points);
return sum;
}
function updateStatus() {
var n = shape ? shape.points.length : 0;
if (drawing) {
statusEl.textContent = '顶点 ' + n + ' · 当前 ' +
(n >= 3 ? formatArea(ringArea(shape.points)) : '—') +
' · 双击结束';
} else {
statusEl.textContent = shapes.length + ' 个面 · 合计 ' + formatArea(totalArea());
}
undoBtn.disabled = !drawing || n === 0;
finishBtn.disabled = !drawing;
}
function addVertex(latlng) {
var index = shape.points.length;
shape.points.push(latlng);
if (!shape.poly) {
shape.poly = L.polygon(shape.points, style).addTo(map);
} else {
shape.poly.setLatLngs(shape.points);
}
var marker = L.marker(latlng, {
icon: vertexIcon,
draggable: true,
bubblingMouseEvents: false
});
/* 闭包捕获当前形状:绘制完成后 shape 会被置空,已完成的顶点仍要能拖动 */
var owner = shape;
marker.on('drag', function (e) {
owner.points[index] = e.target.getLatLng();
owner.poly.setLatLngs(owner.points);
updateStatus();
});
marker.addTo(map);
shape.markers.push(marker);
updateStatus();
}
function popVertex() {
if (!shape || !shape.points.length) return;
shape.points.pop();
var marker = shape.markers.pop();
if (marker) map.removeLayer(marker);
if (shape.poly) {
if (shape.points.length) shape.poly.setLatLngs(shape.points);
else { map.removeLayer(shape.poly); shape.poly = null; }
}
updateStatus();
}
function startDrawing() {
if (shape && shape.points.length >= 3) finish();
else if (shape) discard();
shape = newShape();
drawing = true;
mapEl.classList.add('drawing');
startBtn.textContent = '重新开始';
updateStatus();
}
function finish() {
if (!drawing || !shape) return;
if (shape.points.length < 3) {
statusEl.textContent = '至少需要 3 个顶点才能闭合';
return;
}
/* 换一种描边颜色,区分相邻的面 */
shape.poly.setStyle({
color: palette[shapes.length % palette.length],
fillColor: palette[shapes.length % palette.length]
});
shapes.push(shape);
shape = null;
drawing = false;
mapEl.classList.remove('drawing');
startBtn.textContent = '开始绘制';
updateStatus();
}
function discard() {
if (!shape) return;
shape.markers.forEach(function (m) { map.removeLayer(m); });
if (shape.poly) map.removeLayer(shape.poly);
shape = null;
drawing = false;
mapEl.classList.remove('drawing');
startBtn.textContent = '开始绘制';
updateStatus();
}
map.on('click', function (e) {
if (!drawing) return;
if (!shape) shape = newShape();
addVertex(e.latlng);
});
map.on('dblclick', function () {
if (!drawing || !shape) return;
/* 双击前会补两次 click,最后一点重复,删掉一个再收尾 */
if (shape.points.length >= 2) {
var last = shape.points[shape.points.length - 1];
var prev = shape.points[shape.points.length - 2];
if (last.distanceTo(prev) < 1) popVertex();
}
if (shape && shape.points.length >= 3) finish();
else updateStatus();
});
startBtn.addEventListener('click', startDrawing);
finishBtn.addEventListener('click', finish);
undoBtn.addEventListener('click', function () {
if (drawing) popVertex();
});
clearBtn.addEventListener('click', function () {
shapes.forEach(function (s) {
s.markers.forEach(function (m) { map.removeLayer(m); });
if (s.poly) map.removeLayer(s.poly);
});
shapes = [];
discard();
shape = null;
drawing = false;
mapEl.classList.remove('drawing');
startBtn.textContent = '开始绘制';
updateStatus();
});
updateStatus();
})();
</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.