Atlaslog
Atlaslog / Examples / Leaflet / Draw Polygon
Leaflet · Runnable Example

Draw Polygon

Draw and edit polygons with the drawing plugin and GeoJSON output.

Open standalone → Chinese notes All Leaflet examples
/demos/leaflet-draw-polygon.htmlLIVE
single HTML file · CDN dependencies onlyinterface may contain Chinese labels

Source

demos/leaflet-draw-polygon.html
<!--
  示例:绘制多边形
  要点:点击加点、双击收尾去重、顶点拖拽实时改形、球面面积、多面共存与逐层删除
  依赖: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.

More Leaflet examples

Leaflet · EXAMPLEEvent Debug Leaflet · EXAMPLEExport Image Leaflet · EXAMPLEFeature Edit