<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MapLibre 标尺 & 面积测量工具</title>
<script src="https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.css" rel="stylesheet" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, -apple-system, sans-serif; }
#map { width: 100%; height: 100vh; }
.toolbar {
position: absolute; top: 20px; left: 20px;
z-index: 1000; display: flex; gap: 10px;
}
.toolbar button {
padding: 10px 18px;
background: #fff;
border: 2px solid #555;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: #333;
box-shadow: 0 2px 8px rgba(0,0,0,.15);
transition: all .2s;
user-select: none;
}
.toolbar button:hover { background: #f5f5f5; }
.toolbar button.measure-btn.active {
background: #007cbf;
border-color: #007cbf;
color: #fff;
}
.toolbar button.area-btn.active {
background: #27ae60;
border-color: #27ae60;
color: #fff;
}
.toolbar button.clear-btn {
border-color: #e74c3c;
color: #e74c3c;
}
.toolbar button.clear-btn:hover {
background: #e74c3c;
color: #fff;
}
/* ✅ 测距 Tips:宽度固定 100px,尖角朝下 */
.tip-box {
width: 100px;
text-align: center;
background: #007cbf;
color: #fff;
padding: 6px 0;
border-radius: 6px;
font-size: 14px;
font-weight: bold;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0,0,0,.25);
position: relative;
}
.tip-box::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 0; height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid #007cbf;
}
/* ✅ 面积标签:直接显示,不用 Tips(无尖角) */
.area-label {
background: #27ae60;
color: #fff;
padding: 8px 14px;
border-radius: 8px;
font-size: 15px;
font-weight: bold;
white-space: nowrap;
box-shadow: 0 2px 10px rgba(0,0,0,.3);
}
.measure-cursor { cursor: crosshair; }
</style>
</head>
<body>
<div id="map"></div>
<div class="toolbar">
<button class="measure-btn" id="measureBtn" onclick="toggleMeasure()">📏 多点测距</button>
<button class="area-btn" id="areaBtn" onclick="toggleArea()">📐 面积测量</button>
<button class="clear-btn" onclick="clearAll()">🗑️ 清除</button>
</div>
<script>
// =====================================================================
// MapLibre 标尺 & 面积测量工具(无面板 / 无文字提示版)
// =====================================================================
// ========== 地图 ==========
var map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [116.397, 39.908],
zoom: 12
});
// ========== 状态 ==========
var state = {
mode: null, // 'measure' | 'area' | null
finished: false,
points: [],
segDistances: [],
history: { markers: [], layerIds: [], sourceIds: [] }
};
var measureBtn = document.getElementById('measureBtn');
var areaBtn = document.getElementById('areaBtn');
// ========== 工具函数 ==========
function toRad(d) { return d * Math.PI / 180; }
function calcDistance(p1, p2) {
var R = 6371000;
var phi1 = toRad(p1[1]), phi2 = toRad(p2[1]);
var dPhi = toRad(p2[1] - p1[1]);
var dLam = toRad(p2[0] - p1[0]);
var a = Math.sin(dPhi/2)*Math.sin(dPhi/2) +
Math.cos(phi1)*Math.cos(phi2)*Math.sin(dLam/2)*Math.sin(dLam/2);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
function fmtDistance(m) {
if (m < 1000) return m.toFixed(2) + ' m';
return (m/1000).toFixed(3) + ' km';
}
function calcPlanarArea(coords) {
if (coords.length < 3) return 0;
var R = 6371000;
var midLat = 0;
for (var i = 0; i < coords.length; i++) midLat += coords[i][1];
midLat /= coords.length;
var cosLat = Math.cos(toRad(midLat));
var pts = [];
for (var i = 0; i < coords.length; i++) {
pts.push([R * toRad(coords[i][0]) * cosLat, R * toRad(coords[i][1])]);
}
var area = 0;
for (var i = 0; i < pts.length; i++) {
var j = (i + 1) % pts.length;
area += pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
}
return Math.abs(area) / 2;
}
function fmtArea(m2) {
if (m2 < 10000) return m2.toFixed(2) + ' m\u00B2';
if (m2 < 10000000) return (m2/10000).toFixed(4) + ' 公顷';
return (m2/1000000).toFixed(4) + ' km\u00B2';
}
// ========== 通用控制 ==========
function clearCurrentMode() {
state.history.markers.forEach(function(m) { m.remove(); });
state.history.layerIds.forEach(function(id) {
if (map.getLayer(id)) map.removeLayer(id);
});
state.history.sourceIds.forEach(function(id) {
if (map.getSource(id)) map.removeSource(id);
});
if (map.getSource('preview-line')) {
map.removeLayer('preview-line');
map.removeSource('preview-line');
}
state.history = { markers: [], layerIds: [], sourceIds: [] };
state.points = [];
state.segDistances = [];
state.finished = false;
state.mode = null;
measureBtn.classList.remove('active');
areaBtn.classList.remove('active');
map.getCanvas().classList.remove('measure-cursor');
}
function clearAll() { clearCurrentMode(); }
function stopDrawing() {
state.finished = true;
map.getCanvas().classList.remove('measure-cursor');
map.off('click', onMapClick);
map.off('mousemove', onMouseMove);
map.off('dblclick', onDoubleClick);
map.off('contextmenu', onRightClick);
// 实线 -> 虚线
state.history.layerIds.forEach(function(lid) {
if (lid.indexOf('line-layer-') === 0 && map.getLayer(lid)) {
map.setPaintProperty(lid, 'line-dasharray', [2, 2]);
}
if (lid.indexOf('polygon-layer-') === 0 && map.getLayer(lid)) {
map.setPaintProperty(lid, 'line-dasharray', [2, 2]);
}
});
if (map.getSource('preview-line')) {
map.removeLayer('preview-line');
map.removeSource('preview-line');
}
// ✅ 测距:最后一点 Tips(100px + 尖角)
if (state.mode === 'measure' && state.points.length >= 2) {
var lastPt = state.points[state.points.length - 1];
var total = 0;
for (var i = 0; i < state.segDistances.length; i++) total += state.segDistances[i];
var el = document.createElement('div');
el.className = 'tip-box';
el.textContent = fmtDistance(total);
var m = new maplibregl.Marker({ element: el, offset: [0, -35] })
.setLngLat(lastPt).addTo(map);
state.history.markers.push(m);
}
// ✅ 面积:区域中心直接显示(无 Tips)
if (state.mode === 'area' && state.points.length >= 3) {
var coords = state.points.slice().concat([state.points[0]]);
var uid = Date.now();
var sid = 'polygon-' + uid;
var lid = 'polygon-fill-' + uid;
var olid = 'polygon-outline-' + uid;
map.addSource(sid, {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'Polygon', coordinates: [coords] } }
});
map.addLayer({ id: lid, type: 'fill', source: sid,
paint: { 'fill-color': '#27ae60', 'fill-opacity': 0.2 } });
map.addLayer({ id: olid, type: 'line', source: sid,
paint: { 'line-color': '#27ae60', 'line-width': 2, 'line-dasharray': [2, 2] } });
state.history.sourceIds.push(sid);
state.history.layerIds.push(lid);
state.history.layerIds.push(olid);
var cx = 0, cy = 0;
for (var i = 0; i < state.points.length; i++) {
cx += state.points[i][0];
cy += state.points[i][1];
}
cx /= state.points.length;
cy /= state.points.length;
var area = calcPlanarArea(state.points);
var el = document.createElement('div');
el.className = 'area-label';
el.textContent = fmtArea(area);
var m = new maplibregl.Marker({ element: el })
.setLngLat([cx, cy]).addTo(map);
state.history.markers.push(m);
}
}
// ========== 多点测距 ==========
function toggleMeasure() {
if (state.mode === 'measure' && state.finished) {
clearCurrentMode();
startMeasure();
} else if (state.mode === 'measure') {
if (state.points.length >= 2) stopDrawing();
} else {
if (state.mode) clearCurrentMode();
startMeasure();
}
}
function startMeasure() {
state.mode = 'measure';
state.finished = false;
state.points = [];
state.segDistances = [];
measureBtn.classList.add('active');
map.getCanvas().classList.add('measure-cursor');
map.on('click', onMapClick);
map.on('mousemove', onMouseMove);
map.on('dblclick', onDoubleClick);
map.on('contextmenu', onRightClick);
}
// ========== 面积测量 ==========
function toggleArea() {
if (state.mode === 'area' && state.finished) {
clearCurrentMode();
startArea();
} else if (state.mode === 'area') {
if (state.points.length >= 3) stopDrawing();
} else {
if (state.mode) clearCurrentMode();
startArea();
}
}
function startArea() {
state.mode = 'area';
state.finished = false;
state.points = [];
state.segDistances = [];
areaBtn.classList.add('active');
map.getCanvas().classList.add('measure-cursor');
map.on('click', onMapClick);
map.on('mousemove', onMouseMove);
map.on('dblclick', onDoubleClick);
map.on('contextmenu', onRightClick);
}
// ========== 地图事件 ==========
function onMapClick(e) {
if (state.finished) return;
var p = [e.lngLat.lng, e.lngLat.lat];
state.points.push(p);
addPoint(p, state.points.length);
if (state.mode === 'measure' && state.points.length >= 2) {
var d = calcDistance(
state.points[state.points.length - 2],
state.points[state.points.length - 1]
);
state.segDistances.push(d);
drawSegment(
state.points[state.points.length - 2],
state.points[state.points.length - 1]
);
}
if (state.mode === 'area' && state.points.length >= 3) {
drawPolygonPreview();
}
}
function onMouseMove(e) {
if (state.points.length === 0) return;
updatePreview([e.lngLat.lng, e.lngLat.lat]);
}
function onDoubleClick(e) {
e.preventDefault();
setTimeout(function() {
if (state.mode === 'measure' && state.points.length >= 2) stopDrawing();
if (state.mode === 'area' && state.points.length >= 3) stopDrawing();
}, 50);
}
function onRightClick(e) {
e.preventDefault();
if (state.mode === 'measure' && state.points.length >= 2) stopDrawing();
if (state.mode === 'area' && state.points.length >= 3) stopDrawing();
}
// ========== 点 ==========
function addPoint(coord, idx) {
var uid = Date.now() + Math.random();
var sid = 'point-' + uid;
var lid = 'point-layer-' + uid;
map.addSource(sid, {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'Point', coordinates: coord } }
});
map.addLayer({
id: lid, type: 'circle', source: sid,
paint: {
'circle-radius': idx === 1 ? 3 : 3,
'circle-color': idx === 1 ? '#2ecc71' : '#e74c3c',
'circle-stroke-color': '#fff',
'circle-stroke-width': 2
}
});
state.history.sourceIds.push(sid);
state.history.layerIds.push(lid);
}
// ========== 测距线段 ==========
function drawSegment(p1, p2) {
var uid = Date.now();
var lsid = 'line-' + uid;
var llid = 'line-layer-' + uid;
map.addSource(lsid, {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [p1, p2] } }
});
map.addLayer({
id: llid, type: 'line', source: lsid,
paint: { 'line-color': '#e74c3c', 'line-width': 2, 'line-dasharray': [2, 0] }
});
state.history.sourceIds.push(lsid);
state.history.layerIds.push(llid);
}
// ========== 面积预览(不闭合) ==========
function drawPolygonPreview() {
var coords = state.points.slice().concat([state.points[0]]);
if (!map.getSource('preview-line')) {
map.addSource('preview-line', {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: coords } }
});
map.addLayer({
id: 'preview-line', type: 'line', source: 'preview-line',
paint: { 'line-color': '#27ae60', 'line-width': 2, 'line-dasharray': [4, 4] }
});
} else {
map.getSource('preview-line').setData({
type: 'Feature',
geometry: { type: 'LineString', coordinates: coords }
});
}
}
// ========== 实时预览 ==========
function updatePreview(p) {
var id = 'preview-line';
var coords;
if (state.mode === 'measure') {
coords = state.points.slice().concat([p]);
} else if (state.mode === 'area') {
coords = state.points.slice().concat([p, state.points[0]]);
} else {
return;
}
if (!map.getSource(id)) {
map.addSource(id, {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [] } }
});
map.addLayer({
id: id, type: 'line', source: id,
paint: {
'line-color': state.mode === 'area' ? '#27ae60' : '#007cbf',
'line-width': 2,
'line-dasharray': [2, 2]
}
});
}
map.getSource(id).setData({
type: 'Feature',
geometry: { type: 'LineString', coordinates: coords }
});
}
</script>
</body>
</html>
封装成一个类作为工具使用:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MapLibre 封装版标尺 & 面积测量控件</title>
<script src="https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.css" rel="stylesheet" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, -apple-system, sans-serif; }
#map { width: 100%; height: 100vh; }
</style>
</head>
<body>
<div id="map"></div>
<script>
// ====================== 封装测量控件类 ======================
class MaplibreMeasureControl {
constructor() {
// 内部状态,全部挂载实例,不再全局变量
this.state = {
mode: null, // 'measure' | 'area' | null
finished: false,
points: [],
segDistances: [],
history: { markers: [], layerIds: [], sourceIds: [] }
};
this.map = null;
this.container = null;
this.measureBtn = null;
this.areaBtn = null;
// 绑定事件上下文
this.toggleMeasure = this.toggleMeasure.bind(this);
this.toggleArea = this.toggleArea.bind(this);
this.clearAll = this.clearAll.bind(this);
this.onMapClick = this.onMapClick.bind(this);
this.onMouseMove = this.onMouseMove.bind(this);
this.onDoubleClick = this.onDoubleClick.bind(this);
this.onRightClick = this.onRightClick.bind(this);
}
// 控件标准接口:添加到地图时执行
onAdd(map) {
this.map = map;
// 创建外层容器
this.container = document.createElement('div');
this.container.className = 'maplibregl-ctrl maplibregl-ctrl-group';
this.container.style.display = 'flex';
this.container.style.flexDirection = 'column';
this.container.style.gap = '5px';
this.container.style.padding = '5px';
// 注入内部样式(隔离不污染全局)
const style = document.createElement('style');
style.textContent = `
.maplibregl-ctrl-group button {
display: flex;
align-items: center;
justify-content: center;
width: 25px;
height: 25px;
}
.maplibregl-ctrl-group button+button {
border-top: none;
}
.measure-control-btn {
background: #fff;
cursor: pointer;
transition: all .2s;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
}
.measure-control-btn svg {
display: block;
}
.measure-control-btn:hover { background: #f5f5f5; }
/* 测距 Tips:宽度固定 100px,尖角朝下 */
.tip-box {
width: 100px;
text-align: center;
background: #007cbf;
color: #fff;
padding: 6px 0;
border-radius: 6px;
font-size: 14px;
font-weight: bold;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0,0,0,.25);
position: relative;
}
.tip-box::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 0; height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid #007cbf;
}
/* 面积标签:直接显示,不用 Tips(无尖角) */
.area-label {
background: #27ae60;
color: #fff;
padding: 8px 14px;
border-radius: 8px;
font-size: 15px;
font-weight: bold;
white-space: nowrap;
box-shadow: 0 2px 10px rgba(0,0,0,.3);
}
.measure-cursor { cursor: crosshair !important; }
`;
document.head.appendChild(style);
// 按钮1:多点测距 - 替换为尺子SVG
this.measureBtn = document.createElement('button');
this.measureBtn.className = 'measure-control-btn measure';
this.measureBtn.title = "测量长度";
this.measureBtn.innerHTML = `<svg t="1575453922172" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5140" width="20" height="20"><path d="M64 335.8v352c0 8.8 6.9 16 15.4 16h865.1c8.5 0 15.4-7.2 15.4-16v-352c0-8.8-6.9-16-15.4-16h-865c-8.6 0-15.5 7.2-15.5 16z m833.2 304H128.8v-256h768.4v256z" p-id="5141"></path><path d="M202.5 577.6h30v62h-30zM320.3 485.3h30v154h-30zM438.1 577.6h30v62h-30zM555.9 485.3h30v154h-30zM673.7 577.6h30v62h-30zM791.5 485.3h30v154h-30z" p-id="5142"></path></svg>`;
this.measureBtn.addEventListener('click', this.toggleMeasure);
// 按钮2:面积测量 - 替换为多边形SVG
this.areaBtn = document.createElement('button');
this.areaBtn.className = 'measure-control-btn area';
this.areaBtn.title = "测量面积";
this.areaBtn.innerHTML = `<svg t="1575453274789" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4964" width="20" height="20"><path d="M947.93351 255.639285c16.063496 0 29.06104-12.998035 29.06104-29.059849L976.99455 73.015967c0-16.062837-12.997545-29.060873-29.06104-29.060873L794.334073 43.955094c-16.034842 0-29.060017 12.998035-29.060017 29.060873L765.274056 120.750131l-94.33837 0c-2.937009-0.451278-5.928256-0.451278-8.864241 0L258.72799 120.750131 258.72799 73.015967c0-16.062837-13.027222-29.060873-29.062063-29.060873L76.067513 43.955094c-16.063496 0-29.062063 12.998035-29.062063 29.060873l0 153.563468c0 16.062837 12.998568 29.059849 29.062063 29.059849l47.737143 0 0 506.581594L76.067513 762.220878c-16.063496 0-29.062063 12.997012-29.062063 29.059849l0 153.563468c0 16.062837 12.998568 29.059849 29.062063 29.059849l153.598414 0c16.034842 0 29.062063-12.997012 29.062063-29.059849l0-47.735188 506.545043 0 0 47.735188c0 16.062837 13.026198 29.059849 29.060017 29.059849l153.599437 0c16.063496 0 29.06104-12.997012 29.06104-29.059849L976.993527 791.281751c0-16.062837-12.997545-29.059849-29.06104-29.059849l-47.737143 0L900.195344 354.582761c0.050144-0.984421 0.050144-1.970888 0-2.955308l0-95.988168L947.93351 255.639285zM823.39716 102.07684l95.473264 0 0 95.441723-95.473264 0L823.39716 102.07684zM181.928783 459.920878c1.707968-1.102101 3.328951-2.394537 4.825086-3.89061l277.168724-277.158391 132.079449 0L181.928783 592.929194 181.928783 459.920878zM105.129576 102.07684l95.473264 0 0 46.62797c-0.014327 0.367367-0.02763 0.734734-0.02763 1.106194s0.014327 0.738827 0.02763 1.106194l0 46.601364-95.473264 0L105.129576 102.07684zM229.665927 255.639285c16.034842 0 29.062063-12.998035 29.062063-29.059849l0-47.707558 123.003375 0L181.928783 378.666272 181.928783 255.639285 229.665927 255.639285zM181.928783 675.116031l496.265511-496.244154 87.079762 0 0 35.606962L217.509574 762.220878l-35.580791 0L181.928783 675.116031zM105.129576 915.784346l0-95.441723 95.473264 0 0 46.600341c-0.014327 0.367367-0.02763 0.735757-0.02763 1.106194s0.014327 0.738827 0.02763 1.106194l0 46.62797L105.129576 915.783323zM842.071217 553.748846 563.784997 832.022641c-2.10912 2.109034-3.823229 4.460592-5.148464 6.965645L426.365717 838.988286l415.7055-415.688467L842.071217 553.748846zM918.870424 915.784346l-95.473264 0 0-95.441723 95.473264 0L918.870424 915.784346zM794.334073 762.220878c-16.034842 0-29.060017 12.997012-29.060017 29.059849l0 47.706535L639.011318 838.987263l203.060922-203.051579 0 126.284171L794.334073 762.219855zM842.071217 341.111958 344.174489 838.988286l-85.446499 0 0-35.795251 547.576186-547.552727 35.76704 0L842.071217 341.111958z" p-id="4965"></path></svg>`;
this.areaBtn.addEventListener('click', this.toggleArea);
// 按钮3:清除
const clearBtn = document.createElement('button');
clearBtn.className = 'measure-control-btn clear';
clearBtn.title = "清空";
clearBtn.innerHTML = `<svg t="1782891788089" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6639" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20"><path d="M899.9936 258.56h-211.5584v-48.9472c0-59.392-48.3328-107.7248-107.7248-107.7248H436.992c-59.392 0-107.7248 48.3328-107.7248 107.7248v48.9472h-198.144c-19.8144 0-35.84 16.0256-35.84 35.84s16.0256 35.84 35.84 35.84h58.6752v434.2272c0 80.6912 65.6384 146.2784 146.2784 146.2784h346.2656c80.6912 0 146.2784-65.6384 146.2784-146.2784V330.24h71.3216c19.8144 0 35.84-16.0256 35.84-35.84s-15.9744-35.84-35.7888-35.84z m-499.0464-48.9472c0-19.8656 16.1792-36.0448 36.0448-36.0448h143.7696c19.8656 0 36.0448 16.1792 36.0448 36.0448v48.9472H400.9472v-48.9472z m356.0448 554.8032c0 41.1648-33.4848 74.5984-74.5984 74.5984H336.128c-41.1648 0-74.5984-33.4848-74.5984-74.5984V330.24h495.5136v434.176z" fill="#34332E" p-id="6640"></path><path d="M412.928 439.6032c-19.8144 0-35.84 16.0256-35.84 35.84v219.2384c0 19.8144 16.0256 35.84 35.84 35.84s35.84-16.0256 35.84-35.84V475.4432c0-19.8144-16.0256-35.84-35.84-35.84zM601.4464 439.6032c-19.8144 0-35.84 16.0256-35.84 35.84v219.2384c0 19.8144 16.0256 35.84 35.84 35.84s35.84-16.0256 35.84-35.84V475.4432c0-19.8144-16.0768-35.84-35.84-35.84z" fill="#34332E" p-id="6641"></path></svg>`;
clearBtn.addEventListener('click', this.clearAll);
this.container.appendChild(this.measureBtn);
this.container.appendChild(this.areaBtn);
this.container.appendChild(clearBtn);
return this.container;
}
// 控件标准接口:从地图移除时销毁资源
onRemove() {
this.clearCurrentMode();
if (this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
this.map = null;
}
// ====================== 内部工具函数 ======================
toRad(d) { return d * Math.PI / 180; }
calcDistance(p1, p2) {
const R = 6371000;
const phi1 = this.toRad(p1[1]), phi2 = this.toRad(p2[1]);
const dPhi = this.toRad(p2[1] - p1[1]);
const dLam = this.toRad(p2[0] - p1[0]);
const a = Math.sin(dPhi/2)*Math.sin(dPhi/2) +
Math.cos(phi1)*Math.cos(phi2)*Math.sin(dLam/2)*Math.sin(dLam/2);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
fmtDistance(m) {
if (m < 1000) return m.toFixed(2) + ' m';
return (m/1000).toFixed(3) + ' km';
}
calcPlanarArea(coords) {
if (coords.length < 3) return 0;
const R = 6371000;
let midLat = 0;
for (let i = 0; i < coords.length; i++) midLat += coords[i][1];
midLat /= coords.length;
const cosLat = Math.cos(this.toRad(midLat));
const pts = [];
for (let i = 0; i < coords.length; i++) {
pts.push([R * this.toRad(coords[i][0]) * cosLat, R * this.toRad(coords[i][1])]);
}
let area = 0;
for (let i = 0; i < pts.length; i++) {
const j = (i + 1) % pts.length;
area += pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
}
return Math.abs(area) / 2;
}
fmtArea(m2) {
if (m2 < 10000) return m2.toFixed(2) + ' m²';
if (m2 < 10000000) return (m2/10000).toFixed(4) + ' 公顷';
return (m2/1000000).toFixed(4) + ' km²';
}
// ====================== 通用清除/停止绘制 ======================
clearCurrentMode() {
// 移除所有标记
this.state.history.markers.forEach(m => m.remove());
// 移除图层
this.state.history.layerIds.forEach(id => {
if (this.map.getLayer(id)) this.map.removeLayer(id);
});
// 移除数据源
this.state.history.sourceIds.forEach(id => {
if (this.map.getSource(id)) this.map.removeSource(id);
});
// 移除预览线
if (this.map.getSource('preview-line')) {
this.map.removeLayer('preview-line');
this.map.removeSource('preview-line');
}
// 重置状态
this.state.history = { markers: [], layerIds: [], sourceIds: [] };
this.state.points = [];
this.state.segDistances = [];
this.state.finished = false;
this.state.mode = null;
// 取消按钮激活
this.measureBtn.classList.remove('active');
this.areaBtn.classList.remove('active');
this.map.getCanvas().classList.remove('measure-cursor');
// 解绑所有地图事件
this.map.off('click', this.onMapClick);
this.map.off('mousemove', this.onMouseMove);
this.map.off('dblclick', this.onDoubleClick);
this.map.off('contextmenu', this.onRightClick);
}
clearAll() {
this.clearCurrentMode();
}
stopDrawing() {
this.state.finished = true;
this.map.getCanvas().classList.remove('measure-cursor');
// 解绑绘图事件
this.map.off('click', this.onMapClick);
this.map.off('mousemove', this.onMouseMove);
this.map.off('dblclick', this.onDoubleClick);
this.map.off('contextmenu', this.onRightClick);
// 已绘线段改为虚线
this.state.history.layerIds.forEach(lid => {
if (lid.indexOf('line-layer-') === 0 && this.map.getLayer(lid)) {
this.map.setPaintProperty(lid, 'line-dasharray', [2, 2]);
}
if (lid.indexOf('polygon-layer-') === 0 && this.map.getLayer(lid)) {
this.map.setPaintProperty(lid, 'line-dasharray', [2, 2]);
}
});
// 删除临时预览线
if (this.map.getSource('preview-line')) {
this.map.removeLayer('preview-line');
this.map.removeSource('preview-line');
}
// 测距总长度弹窗
if (this.state.mode === 'measure' && this.state.points.length >= 2) {
const lastPt = this.state.points[this.state.points.length - 1];
let total = 0;
for (let i = 0; i < this.state.segDistances.length; i++) total += this.state.segDistances[i];
const el = document.createElement('div');
el.className = 'tip-box';
el.textContent = this.fmtDistance(total);
const m = new maplibregl.Marker({ element: el, offset: [0, -35] })
.setLngLat(lastPt).addTo(this.map);
this.state.history.markers.push(m);
}
// 面积闭合多边形+中心标签
if (this.state.mode === 'area' && this.state.points.length >= 3) {
const coords = this.state.points.slice().concat([this.state.points[0]]);
const uid = Date.now();
const sid = 'polygon-' + uid;
const lid = 'polygon-fill-' + uid;
const olid = 'polygon-outline-' + uid;
this.map.addSource(sid, {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'Polygon', coordinates: [coords] } }
});
this.map.addLayer({ id: lid, type: 'fill', source: sid,
paint: { 'fill-color': '#27ae60', 'fill-opacity': 0.2 } });
this.map.addLayer({ id: olid, type: 'line', source: sid,
paint: { 'line-color': '#27ae60', 'line-width': 2, 'line-dasharray': [2, 2] } });
this.state.history.sourceIds.push(sid);
this.state.history.layerIds.push(lid);
this.state.history.layerIds.push(olid);
// 计算中心点
let cx = 0, cy = 0;
for (let i = 0; i < this.state.points.length; i++) {
cx += this.state.points[i][0];
cy += this.state.points[i][1];
}
cx /= this.state.points.length;
cy /= this.state.points.length;
const area = this.calcPlanarArea(this.state.points);
const el = document.createElement('div');
el.className = 'area-label';
el.textContent = this.fmtArea(area);
const m = new maplibregl.Marker({ element: el })
.setLngLat([cx, cy]).addTo(this.map);
this.state.history.markers.push(m);
}
}
// ====================== 测距切换逻辑 ======================
toggleMeasure() {
if (this.state.mode === 'measure' && this.state.finished) {
this.clearCurrentMode();
this.startMeasure();
} else if (this.state.mode === 'measure') {
if (this.state.points.length >= 2) this.stopDrawing();
} else {
if (this.state.mode) this.clearCurrentMode();
this.startMeasure();
}
}
startMeasure() {
this.state.mode = 'measure';
this.state.finished = false;
this.state.points = [];
this.state.segDistances = [];
this.measureBtn.classList.add('active');
this.map.getCanvas().classList.add('measure-cursor');
// 绑定绘图事件
this.map.on('click', this.onMapClick);
this.map.on('mousemove', this.onMouseMove);
this.map.on('dblclick', this.onDoubleClick);
this.map.on('contextmenu', this.onRightClick);
}
// ====================== 面积切换逻辑 ======================
toggleArea() {
if (this.state.mode === 'area' && this.state.finished) {
this.clearCurrentMode();
this.startArea();
} else if (this.state.mode === 'area') {
if (this.state.points.length >= 3) this.stopDrawing();
} else {
if (this.state.mode) this.clearCurrentMode();
this.startArea();
}
}
startArea() {
this.state.mode = 'area';
this.state.finished = false;
this.state.points = [];
this.state.segDistances = [];
this.areaBtn.classList.add('active');
this.map.getCanvas().classList.add('measure-cursor');
this.map.on('click', this.onMapClick);
this.map.on('mousemove', this.onMouseMove);
this.map.on('dblclick', this.onDoubleClick);
this.map.on('contextmenu', this.onRightClick);
}
// ====================== 地图绘图事件 ======================
onMapClick(e) {
if (this.state.finished) return;
const p = [e.lngLat.lng, e.lngLat.lat];
this.state.points.push(p);
this.addPoint(p, this.state.points.length);
// 测距新增线段
if (this.state.mode === 'measure' && this.state.points.length >= 2) {
const d = this.calcDistance(
this.state.points[this.state.points.length - 2],
this.state.points[this.state.points.length - 1]
);
this.state.segDistances.push(d);
this.drawSegment(
this.state.points[this.state.points.length - 2],
this.state.points[this.state.points.length - 1]
);
}
// 面积预览闭合线
if (this.state.mode === 'area' && this.state.points.length >= 3) {
this.drawPolygonPreview();
}
}
onMouseMove(e) {
if (this.state.points.length === 0) return;
this.updatePreview([e.lngLat.lng, e.lngLat.lat]);
}
onDoubleClick(e) {
e.preventDefault();
setTimeout(() => {
if (this.state.mode === 'measure' && this.state.points.length >= 2) this.stopDrawing();
if (this.state.mode === 'area' && this.state.points.length >= 3) this.stopDrawing();
}, 50);
}
onRightClick(e) {
e.preventDefault();
if (this.state.mode === 'measure' && this.state.points.length >= 2) this.stopDrawing();
if (this.state.mode === 'area' && this.state.points.length >= 3) this.stopDrawing();
}
// ====================== 绘制元素工具 ======================
addPoint(coord, idx) {
const uid = Date.now() + Math.random();
const sid = 'point-' + uid;
const lid = 'point-layer-' + uid;
this.map.addSource(sid, {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'Point', coordinates: coord } }
});
this.map.addLayer({
id: lid, type: 'circle', source: sid,
paint: {
'circle-radius': 3,
'circle-color': idx === 1 ? '#2ecc71' : '#e74c3c',
'circle-stroke-color': '#fff',
'circle-stroke-width': 2
}
});
this.state.history.sourceIds.push(sid);
this.state.history.layerIds.push(lid);
}
drawSegment(p1, p2) {
const uid = Date.now();
const lsid = 'line-' + uid;
const llid = 'line-layer-' + uid;
this.map.addSource(lsid, {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [p1, p2] } }
});
this.map.addLayer({
id: llid, type: 'line', source: lsid,
paint: { 'line-color': '#e74c3c', 'line-width': 2, 'line-dasharray': [2, 0] }
});
this.state.history.sourceIds.push(lsid);
this.state.history.layerIds.push(llid);
}
drawPolygonPreview() {
const coords = this.state.points.slice().concat([this.state.points[0]]);
if (!this.map.getSource('preview-line')) {
this.map.addSource('preview-line', {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: coords } }
});
this.map.addLayer({
id: 'preview-line', type: 'line', source: 'preview-line',
paint: { 'line-color': '#27ae60', 'line-width': 2, 'line-dasharray': [4, 4] }
});
} else {
this.map.getSource('preview-line').setData({
type: 'Feature',
geometry: { type: 'LineString', coordinates: coords }
});
}
}
updatePreview(p) {
const id = 'preview-line';
let coords;
if (this.state.mode === 'measure') {
coords = this.state.points.slice().concat([p]);
} else if (this.state.mode === 'area') {
coords = this.state.points.slice().concat([p, this.state.points[0]]);
} else {
return;
}
if (!this.map.getSource(id)) {
this.map.addSource(id, {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [] } }
});
this.map.addLayer({
id: id, type: 'line', source: id,
paint: {
'line-color': this.state.mode === 'area' ? '#27ae60' : '#007cbf',
'line-width': 2,
'line-dasharray': [2, 2]
}
});
}
this.map.getSource(id).setData({
type: 'Feature',
geometry: { type: 'LineString', coordinates: coords }
});
}
}
// ====================== 地图初始化 & 使用控件 ======================
var map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [116.397, 39.908],
zoom: 12
});
// 按需求示例方式使用控件
const measureControl = new MaplibreMeasureControl();
map.addControl(measureControl, 'top-right');
</script>
</body>
</html>
2305

被折叠的 条评论
为什么被折叠?



