✅ 每次绘制 → 保留在地图上
✅ 新绘制不会影响旧图形
✅ 【清除】→ 清除 所有 历史绘制
✅ 【标尺工具】→ 开启新一轮绘制
<!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, 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 #007cbf;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: #007cbf;
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
}
.toolbar button.active {
background: #007cbf;
color: #fff;
}
.toolbar button.clear-btn {
border-color: #e74c3c;
color: #e74c3c;
}
.toolbar button.clear-btn:hover {
background: #e74c3c;
color: #fff;
}
.hint {
position: absolute;
top: 80px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: rgba(0, 0, 0, .7);
color: #fff;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
display: none;
}
.hint.show {
display: block;
}
.coord-info {
position: absolute;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: rgba(255, 255, 255, .95);
padding: 8px 14px;
border-radius: 8px;
font-size: 12px;
display: none;
}
.result-panel {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: #fff;
padding: 15px 25px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, .2);
display: none;
}
.result-panel.show {
display: block;
}
.result-panel .distance {
font-size: 24px;
font-weight: bold;
color: #007cbf;
}
.ruler-cursor {
cursor: crosshair;
}
</style>
</head>
<body>
<div id="map"></div>
<div class="toolbar">
<button id="rulerBtn" onclick="toggleRuler()">📏 标尺工具</button>
<button class="clear-btn" onclick="clearAll()">🗑️ 清除</button>
</div>
<div class="hint" id="hint"></div>
<div class="coord-info" id="coordInfo"></div>
<div class="result-panel" id="resultPanel">
<div style="text-align:center">
<div class="distance" id="distanceText">0 m</div>
<div class="unit">直线距离</div>
</div>
</div>
<script>
/* ========= 地图 ========= */
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [116.397, 39.908],
zoom: 12
});
/* ========= 状态 ========= */
const state = {
active: false,
finished: false,
points: [],
// ✅ 历史记录(用于清除)
history: {
markers: [],
layerIds: [],
sourceIds: []
}
};
const rulerBtn = document.getElementById('rulerBtn');
const hint = document.getElementById('hint');
const coordInfo = document.getElementById('coordInfo');
const resultPanel = document.getElementById('resultPanel');
const distanceText = document.getElementById('distanceText');
/* ========= 工具函数 ========= */
function toRad(d) {
return d * Math.PI / 180;
}
function calcDistance([lng1, lat1], [lng2, lat2]) {
const R = 6371000;
const φ1 = toRad(lat1);
const φ2 = toRad(lat2);
const Δφ = toRad(lat2 - lat1);
const Δλ = toRad(lng2 - lng1);
const a =
Math.sin(Δφ / 2) ** 2 +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
function fmtDistance(m) {
return m < 1000
? `${m.toFixed(2)} m`
: `${(m / 1000).toFixed(3)} km`;
}
/* ========= 标尺控制 ========= */
function toggleRuler() {
if (!state.active) {
startNewRuler();
} else if (state.finished) {
startNewRuler();
} else {
cancelCurrent();
}
}
function startNewRuler() {
state.active = true;
state.finished = false;
state.points = [];
rulerBtn.classList.add('active');
map.getCanvas().classList.add('ruler-cursor');
hint.textContent = '点击地图选择第一个点';
hint.classList.add('show');
map.on('click', onClick);
map.on('mousemove', onMouseMove);
}
function cancelCurrent() {
state.active = false;
state.finished = false;
rulerBtn.classList.remove('active');
map.getCanvas().classList.remove('ruler-cursor');
hint.classList.remove('show');
coordInfo.classList.remove('show');
resultPanel.classList.remove('show');
map.off('click', onClick);
map.off('mousemove', onMouseMove);
}
/* ========= 清除(全部) ========= */
function clearAll() {
// 移除所有 marker
state.history.markers.forEach(m => m.remove());
// 移除所有 layer / source
state.history.layerIds.forEach(id => {
if (map.getLayer(id)) map.removeLayer(id);
});
state.history.sourceIds.forEach(id => {
if (map.getSource(id)) map.removeSource(id);
});
// 重置
state.history = { markers: [], layerIds: [], sourceIds: [] };
state.active = false;
state.finished = false;
state.points = [];
rulerBtn.classList.remove('active');
map.getCanvas().classList.remove('ruler-cursor');
hint.classList.remove('show');
coordInfo.classList.remove('show');
resultPanel.classList.remove('show');
}
/* ========= 事件 ========= */
function onClick(e) {
if (state.finished || state.points.length >= 2) return;
const p = [e.lngLat.lng, e.lngLat.lat];
state.points.push(p);
addMarker(p, state.points.length);
if (state.points.length === 1) {
hint.textContent = '点击地图选择第二个点';
coordInfo.textContent = `起点: ${p[0].toFixed(6)}, ${p[1].toFixed(6)}`;
coordInfo.classList.add('show');
}
if (state.points.length === 2) {
drawLine();
showResult();
finishDrawing();
}
}
function onMouseMove(e) {
if (state.points.length !== 1) return;
const p = [e.lngLat.lng, e.lngLat.lat];
coordInfo.textContent =
`起点: ${state.points[0][0].toFixed(6)}, ${state.points[0][1].toFixed(6)} | 鼠标: ${p[0].toFixed(6)}, ${p[1].toFixed(6)}`;
state.tempMarker
? state.tempMarker.setLngLat(p)
: state.tempMarker = new maplibregl.Marker({ color: '#007cbf', scale: 0.8 })
.setLngLat(p).addTo(map);
updatePreview(p);
}
/* ========= 绘制 ========= */
let tempMarker = null;
function addMarker(coord, idx) {
const el = document.createElement('div');
el.style.cssText = `
width:18px;height:18px;border-radius:50%;
background:${idx === 1 ? '#2ecc71' : '#e74c3c'};
border:3px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,.3);
`;
const m = new maplibregl.Marker(el).setLngLat(coord).addTo(map);
state.history.markers.push(m);
}
function updatePreview(p) {
const id = 'preview-line';
if (!map.getSource(id)) {
map.addSource(id, {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [] } }
});
map.addLayer({
id, type: 'line', source: id,
paint: { 'line-color': '#007cbf', 'line-width': 3, 'line-dasharray': [2, 2] }
});
}
map.getSource(id).setData({
type: 'Feature',
geometry: { type: 'LineString', coordinates: [state.points[0], p] }
});
}
function drawLine() {
const uid = Date.now();
const sourceId = `ruler-source-${uid}`;
const layerId = `ruler-layer-${uid}`;
map.addSource(sourceId, {
type: 'geojson',
data: {
type: 'Feature',
geometry: { type: 'LineString', coordinates: state.points }
}
});
map.addLayer({
id: layerId,
type: 'line',
source: sourceId,
paint: {
'line-color': '#e74c3c',
'line-width': 4
}
});
state.history.sourceIds.push(sourceId);
state.history.layerIds.push(layerId);
// 清除临时预览线
if (map.getSource('preview-line')) {
map.removeLayer('preview-line');
map.removeSource('preview-line');
}
}
function showResult() {
const d = calcDistance(state.points[0], state.points[1]);
distanceText.textContent = fmtDistance(d);
resultPanel.classList.add('show');
const mid = [
(state.points[0][0] + state.points[1][0]) / 2,
(state.points[0][1] + state.points[1][1]) / 2
];
const el = document.createElement('div');
el.innerHTML = `
<div style="
background:#007cbf;color:#fff;
padding:6px 12px;border-radius:6px;
font-size:14px;font-weight:bold;
box-shadow:0 2px 8px rgba(0,0,0,.2);
">${fmtDistance(d)}</div>
`;
const m = new maplibregl.Marker({ element: el }).setLngLat(mid).addTo(map);
state.history.markers.push(m);
}
/* ========= 完成绘制 ========= */
function finishDrawing() {
state.finished = true;
map.getCanvas().classList.remove('ruler-cursor');
hint.textContent = '点击“标尺工具”继续绘制,或点击“清除”清除所有图形';
map.off('click', onClick);
map.off('mousemove', onMouseMove);
tempMarker?.remove();
tempMarker = null;
}
</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, 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 #007cbf;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: #007cbf;
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
}
.toolbar button.active {
background: #007cbf;
color: #fff;
}
.toolbar button.clear-btn {
border-color: #e74c3c;
color: #e74c3c;
}
.toolbar button.clear-btn:hover {
background: #e74c3c;
color: #fff;
}
.hint {
position: absolute;
top: 80px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: rgba(0, 0, 0, .7);
color: #fff;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
display: none;
}
.hint.show {
display: block;
}
.coord-info {
position: absolute;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: rgba(255, 255, 255, .95);
padding: 8px 14px;
border-radius: 8px;
font-size: 12px;
display: none;
}
.result-panel {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: #fff;
padding: 15px 25px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, .2);
display: none;
}
.result-panel.show {
display: block;
}
.result-panel .distance {
font-size: 24px;
font-weight: bold;
color: #007cbf;
}
.ruler-cursor {
cursor: crosshair;
}
</style>
</head>
<body>
<div id="map"></div>
<div class="toolbar">
<button id="rulerBtn" onclick="toggleRuler()">📏 标尺工具</button>
<button class="clear-btn" onclick="clearAll()">🗑️ 清除</button>
</div>
<div class="hint" id="hint"></div>
<div class="coord-info" id="coordInfo"></div>
<div class="result-panel" id="resultPanel">
<div style="text-align:center">
<div class="distance" id="distanceText">0 m</div>
<div class="unit">直线距离</div>
</div>
</div>
<script>
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [116.397, 39.908],
zoom: 12
});
const state = {
active: false,
finished: false,
points: [],
history: {
markers: [],
layerIds: [],
sourceIds: []
}
};
const rulerBtn = document.getElementById('rulerBtn');
const hint = document.getElementById('hint');
const coordInfo = document.getElementById('coordInfo');
const resultPanel = document.getElementById('resultPanel');
const distanceText = document.getElementById('distanceText');
function toRad(d) {
return d * Math.PI / 180;
}
function calcDistance([lng1, lat1], [lng2, lat2]) {
const R = 6371000;
const φ1 = toRad(lat1);
const φ2 = toRad(lat2);
const Δφ = toRad(lat2 - lat1);
const Δλ = toRad(lng2 - lng1);
const a =
Math.sin(Δφ / 2) ** 2 +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
function fmtDistance(m) {
return m < 1000
? `${m.toFixed(2)} m`
: `${(m / 1000).toFixed(3)} km`;
}
function toggleRuler() {
if (!state.active) {
startNewRuler();
} else if (state.finished) {
startNewRuler();
} else {
cancelCurrent();
}
}
function startNewRuler() {
state.active = true;
state.finished = false;
state.points = [];
rulerBtn.classList.add('active');
map.getCanvas().classList.add('ruler-cursor');
hint.textContent = '点击地图选择第一个点';
hint.classList.add('show');
map.on('click', onClick);
map.on('mousemove', onMouseMove);
}
function cancelCurrent() {
state.active = false;
state.finished = false;
rulerBtn.classList.remove('active');
map.getCanvas().classList.remove('ruler-cursor');
hint.classList.remove('show');
coordInfo.classList.remove('show');
resultPanel.classList.remove('show');
map.off('click', onClick);
map.off('mousemove', onMouseMove);
}
function clearAll() {
state.history.markers.forEach(m => m.remove());
state.history.layerIds.forEach(id => {
if (map.getLayer(id)) map.removeLayer(id);
});
state.history.sourceIds.forEach(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.active = false;
state.finished = false;
state.points = [];
rulerBtn.classList.remove('active');
map.getCanvas().classList.remove('ruler-cursor');
hint.classList.remove('show');
coordInfo.classList.remove('show');
resultPanel.classList.remove('show');
}
function onClick(e) {
if (state.finished || state.points.length >= 2) return;
const p = [e.lngLat.lng, e.lngLat.lat];
state.points.push(p);
addMarker(p, state.points.length);
if (state.points.length === 1) {
hint.textContent = '点击地图选择第二个点';
coordInfo.textContent = `起点: ${p[0].toFixed(6)}, ${p[1].toFixed(6)}`;
coordInfo.classList.add('show');
}
if (state.points.length === 2) {
drawLine();
showResult();
finishDrawing();
}
}
function onMouseMove(e) {
if (state.points.length !== 1) return;
const p = [e.lngLat.lng, e.lngLat.lat];
coordInfo.textContent =
`起点: ${state.points[0][0].toFixed(6)}, ${state.points[0][1].toFixed(6)} | 鼠标: ${p[0].toFixed(6)}, ${p[1].toFixed(6)}`;
updatePreview(p);
}
function addMarker(coord, idx) {
const uid = Date.now() + Math.random();
const sourceId = `point-${uid}`;
const layerId = `point-layer-${uid}`;
map.addSource(sourceId, {
type: 'geojson',
data: {
type: 'Feature',
geometry: { type: 'Point', coordinates: coord }
}
});
map.addLayer({
id: layerId,
type: 'circle',
source: sourceId,
paint: {
'circle-radius': idx === 1 ? 4 : 3,
'circle-color': idx === 1 ? '#2ecc71' : '#e74c3c',
'circle-stroke-color': '#fff',
'circle-stroke-width': 2
}
});
state.history.sourceIds.push(sourceId);
state.history.layerIds.push(layerId);
}
function updatePreview(p) {
const id = 'preview-line';
if (!map.getSource(id)) {
map.addSource(id, {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [] } }
});
map.addLayer({
id,
type: 'line',
source: id,
paint: {
'line-color': '#007cbf',
'line-width': 2,
'line-dasharray': [2, 2]
}
});
}
map.getSource(id).setData({
type: 'Feature',
geometry: { type: 'LineString', coordinates: [state.points[0], p] }
});
}
function drawLine() {
const uid = Date.now();
const sourceId = `ruler-source-${uid}`;
const layerId = `ruler-layer-${uid}`;
map.addSource(sourceId, {
type: 'geojson',
data: {
type: 'Feature',
geometry: { type: 'LineString', coordinates: state.points }
}
});
map.addLayer({
id: layerId,
type: 'line',
source: sourceId,
paint: {
'line-color': '#e74c3c',
'line-width': 2,
'line-dasharray': [2, 2]
}
});
state.history.sourceIds.push(sourceId);
state.history.layerIds.push(layerId);
if (map.getSource('preview-line')) {
map.removeLayer('preview-line');
map.removeSource('preview-line');
}
}
function showResult() {
const d = calcDistance(state.points[0], state.points[1]);
distanceText.textContent = fmtDistance(d);
resultPanel.classList.add('show');
const mid = [
(state.points[0][0] + state.points[1][0]) / 2,
(state.points[0][1] + state.points[1][1]) / 2
];
const el = document.createElement('div');
el.innerHTML = `
<div style="
background:#007cbf;color:#fff;
padding:6px 12px;border-radius:6px;
font-size:14px;font-weight:bold;
box-shadow:0 2px 8px rgba(0,0,0,.2);
">${fmtDistance(d)}</div>
`;
const m = new maplibregl.Marker({ element: el }).setLngLat(mid).addTo(map);
state.history.markers.push(m);
}
function finishDrawing() {
state.finished = true;
map.getCanvas().classList.remove('ruler-cursor');
hint.textContent = '点击“标尺工具”继续绘制,或点击“清除”清除所有图形';
map.off('click', onClick);
map.off('mousemove', onMouseMove);
// ✅ 只把“线”从实线改成虚线,不碰点
state.history.layerIds.forEach(layerId => {
if (layerId.startsWith('ruler-layer-') && map.getLayer(layerId)) {
map.setPaintProperty(layerId, 'line-dasharray', [6, 4]);
}
});
}
</script>
</body>
</html>
230

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



