Measure
Runnable Leaflet example: Measure. 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 + 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 #total{color:#16181d;font-weight:600;font-variant-numeric:tabular-nums}
#map{flex:1 1 auto;min-height:0;cursor:crosshair}
.vtx{width:8px;height:8px;border-radius:50%;background:#fff;border:2px solid #0e7490;
box-sizing:border-box}
.dist-tip{background:rgba(15,23,42,.86);border:0;border-radius:3px;color:#fff;
font-size:11px;padding:1px 5px;box-shadow:none}
.dist-tip::before{border-top-color:rgba(15,23,42,.86)}
</style>
</head>
<body>
<div class="bar">
<button id="reset" type="button">重新测量</button>
<button id="undo" type="button" disabled>撤销</button>
<button id="finish" type="button" disabled>结束</button>
<span class="spacer"></span>
<span>总长 <span id="total">0 m</span></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.907, 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: [8, 8],
iconAnchor: [4, 4]
});
var points = [];
var markers = [];
var labels = [];
var line = L.polyline([], {
color: '#0e7490',
weight: 3,
dashArray: '7 5',
interactive: false
}).addTo(map);
var finished = false;
var totalEl = document.getElementById('total');
var undoBtn = document.getElementById('undo');
var finishBtn = document.getElementById('finish');
function fmt(m) {
return m >= 1000 ? (m / 1000).toFixed(2) + ' km' : Math.round(m) + ' m';
}
function segments() {
var sum = 0;
for (var i = 1; i < points.length; i++) {
sum += points[i - 1].distanceTo(points[i]);
}
return sum;
}
function clearLabels() {
labels.forEach(function (t) { map.removeLayer(t); });
labels = [];
}
function redraw() {
line.setLatLngs(points);
clearLabels();
for (var i = 1; i < points.length; i++) {
var a = points[i - 1];
var b = points[i];
var mid = L.latLng((a.lat + b.lat) / 2, (a.lng + b.lng) / 2);
var tip = L.tooltip({
permanent: true,
direction: 'center',
className: 'dist-tip',
opacity: 1
}).setLatLng(mid).setContent(fmt(a.distanceTo(b))).addTo(map);
labels.push(tip);
}
totalEl.textContent = fmt(segments());
undoBtn.disabled = points.length === 0 || finished;
finishBtn.disabled = points.length < 2 || finished;
}
function addPoint(latlng) {
if (finished) return;
points.push(latlng);
markers.push(L.marker(latlng, { icon: vertexIcon, interactive: false }).addTo(map));
redraw();
}
function popPoint() {
if (finished || !points.length) return;
points.pop();
var m = markers.pop();
if (m) map.removeLayer(m);
redraw();
}
function finish() {
if (points.length < 2) return;
finished = true;
markers.forEach(function (m) { map.removeLayer(m); });
markers = [];
redraw();
}
function reset() {
points = [];
markers.forEach(function (m) { map.removeLayer(m); });
markers = [];
clearLabels();
line.setLatLngs([]);
finished = false;
totalEl.textContent = '0 m';
undoBtn.disabled = true;
finishBtn.disabled = true;
}
map.on('click', function (e) {
addPoint(e.latlng);
});
map.on('dblclick', function () {
if (finished || !points.length) return;
/* 双击会先产生两次 click,去掉重复的末端顶点 */
if (points.length >= 2) {
var last = points[points.length - 1];
var prev = points[points.length - 2];
if (last.distanceTo(prev) < 1) popPoint();
}
if (points.length >= 2) finish();
});
document.getElementById('reset').addEventListener('click', reset);
document.getElementById('undo').addEventListener('click', popPoint);
document.getElementById('finish').addEventListener('click', finish);
redraw();
})();
</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.