需求
用户可以在地图上画线测量距离。
实现过程
本质上是给地图添加一个绘制折线的LineString交互,在用户绘制过程中,实时的计算绘制出来的折线的距离。
知识点
ol.sphere.getLength(line)获取折线的长度,单位米。ol.sphere.getDistance(point1, point2)计算两个点之间的距离,点的格式是WGS84也就是EPSG:4326格式下的经纬度。overlay.getElement()获取overlay使用的DOM。evt.dragging事件回调函数中使用,用于判断是否在拖拽中。geometry().on('change', () => {})监听geometry变化,绘制折线的时候,每当折线变化的时候,都出触发。lineGeometry.getLastCoordinate()获取折线的最后一个端点的经纬度。ol.Observable.unByKey(listener)解除事件绑定,防止内存泄漏或不必要的事件触发。当不再需要某个事件监听器时,可以使用这个方法来移除它。listener是绑定事件的返回值。
代码HTML+CSS+JS
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/8.2.0/ol.min.css"
integrity="sha512-bc9nJM5uKHN+wK7rtqMnzlGicwJBWR11SIDFJlYBe5fVOwjHGtXX8KMyYZ4sMgSL0CoUjo4GYgIBucOtqX/RUQ=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<title>距离测量</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body,
#app {
height: 100%;
overflow: hidden;
}
.app-map {
width: 90vw;
height: 90vh;
margin: 5vh 5vw;
border-radius: 5px;
overflow: hidden;
}
.app-btns {
position: fixed;
right: 10px;
top: 10px;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .5);
width: 210px;
padding: 25px;
text-align: center;
border-radius: 5px;
display: flex;
flex-direction: column;
z-index: 2;
}
.app-btns button {
font-size: 18px;
border: none;
padding: 12px 20px;
border-radius: 4px;
color: #fff;
background-color: #409eff;
border-color: none;
cursor: pointer;
border: 1px solid #dcdfe6;
margin-bottom: 5px;
}
.app-btns button:hover {
background: #66b1ff;
}
.app-btns button.end,
.app-btns button.end:hover {
background-color: #ff0000;
}
#helpTxt,
.ol-tooltip {
position: relative;
background: rgba(0, 0, 0, 0.5);
border-radius: 4px;
color: white;
padding: 4px 8px;
opacity: 0.7;
white-space: nowrap;
font-size: 12px;
cursor: default;
user-select: none;
}
.ol-tooltip-measure {
opacity: 1;
font-weight: bold;
}
.ol-tooltip-static {
background-color: #20ba11;
color: #fff;
opacity: 1;
border: 1px solid white;
}
.ol-tooltip-measure:before,
.ol-tooltip-static:before {
border-top: 6px solid rgba(0, 0, 0, 0.5);
border-right: 6px solid transparent;
border-left: 6px solid transparent;
content: "";
position: absolute;
bottom: -6px;
margin-left: -7px;
left: 50%;
}
.ol-tooltip-static:before {
border-top-color: #20ba11;
}
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div id="app" v-cloak>
<div class="app-map" id="app-map"></div>
<div class="app-btns">
<button type='button' :class="{end: calcIng}" @click='handleClickCalcDistance'>{{calcIng ? '结束测量' : '开始测量'}}</button>
</div>
<div id="helpTxt">{{helpMsg}}</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/openlayers/8.2.0/dist/ol.min.js"
integrity="sha512-+nvfloZUX7awRy1yslYBsicmHKh/qFW5w79+AiGiNcbewg0nBy7AS4G3+aK/Rm+eGPOKlO3tLuVphMxFXeKeOQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.14/vue.global.prod.min.js"
integrity="sha512-huEQFMCpBzGkSDSPVAeQFMfvWuQJWs09DslYxQ1xHeaCGQlBiky9KKZuXX7zfb0ytmgvfpTIKKAmlCZT94TAlQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
const { createApp } = Vue;
// 绘制过程中的折线样式
const style1 = new ol.style.Style({
stroke: new ol.style.Stroke({
color: 'rgba(32, 177, 170, 0.5)',
width: 3
})
});
// 绘制结束后的折线样式
const style2 = new ol.style.Style({
stroke: new ol.style.Stroke({
color: 'rgba(32, 177, 170, 1)',
width: 3
})
});
// 计算折线长度的函数
const formatLength = function (line) {
const length = ol.sphere.getLength(line);
let output;
if (length > 100) {
output = Math.round((length / 1000) * 100) / 100 + ' ' + 'km';
} else {
output = Math.round(length * 100) / 100 + ' ' + 'm';
}
return output;
};
// 格式化距离
function formatDistance (dis) {
if (dis > 100) {
return Math.round((dis / 1000) * 100) / 100 + ' ' + 'km';
} else {
return Math.round(dis * 100) / 100 + ' ' + 'm';
}
};
const vm = createApp({
data() {
return {
map: {}, // 地图实例
drawSource: {}, // 绘制图形的图层资源
draw: null, // 绘制实例
calcIng: false, // 测量中标识
helpMsg: '', // 提示文本
measureTooltip: null, // 每段折线用于实时和最后显示总长的overlay
sketch: null // 绘制过程中的折线
}
},
methods: {
// 初始化地图
initMap() {
// 创建放置用户绘制的feature的图层
this.drawSource = new ol.source.Vector();
const layer = new ol.layer.Vector({
source: this.drawSource
});
// 高德地图瓦片地址
const mianLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'http://wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}'
})
});
// 初始化地图
this.map = new ol.Map({
target: 'app-map',
layers: [mianLayer, layer],
view: new ol.View({
projection: 'EPSG:3857',
//设定中心点,因为默认坐标系为 3587,所以要将我们常用的经纬度坐标系4326 转换为 3587坐标系
center: ol.proj.transform([111.8453154, 32.7383500], 'EPSG:4326', 'EPSG:3857'),
zoom: 12
})
});
// 初始化绑定事件
this.bindEvt();
// 初始化提示文本的overlay
this.initHelpOverlay();
},
// 初始化绑定事件
bindEvt() {
// 鼠标离开地图范围,隐藏提示信息
this.map.getViewport().addEventListener('mouseleave', () => {
this.helpTooltip.getElement().style.display = 'none';
});
// 鼠标进入地图范围,如果正在测量距离,显示提示信息
this.map.getViewport().addEventListener('mouseenter', () => {
if (this.calcIng) {
this.helpTooltip.getElement().style.display = 'block';
}
});
// 鼠标移动,动态设置提示信息的位置
this.map.on('pointermove', (evt) => {
// 在拖拽过程中或者未开启测量,不处理
if (evt.dragging || this.calcIng === false) {
return;
}
// 绘制第一个点前的提示信息
this.helpMsg = '点击鼠标左键,确定起始点位';
if (this.sketch) {
// 确定了第一个点之后的提示信息
this.helpMsg = '移动鼠标,点击左键确定下一点位,鼠标右键结束测量';
}
// 设置提示信息的位置
this.helpTooltip.setPosition(evt.coordinate);
});
},
// 初始化提示文本的overlay
initHelpOverlay() {
this.helpTooltip = new ol.Overlay({
element: document.querySelector('#helpTxt'),
offset: [15, 0],
positioning: 'center-left'
});
this.map.addOverlay(this.helpTooltip);
},
// 点击开始测量
handleClickCalcDistance() {
if (this.calcIng) {
// 结束测量
this.calcIng = false;
this.map.removeInteraction(this.draw);
this.helpTooltip.getElement().style.display = 'none';
} else {
// 开始测量,初始化交互
this.calcIng = true;
this.initInteraction();
}
},
// 创建显示距离的信息,每开始一个折线,就需要一个显示距离的overlay
createMeasureTooltip() {
const measureTooltipElement = document.createElement('div');
measureTooltipElement.className = 'ol-tooltip ol-tooltip-measure';
this.measureTooltip = new ol.Overlay({
element: measureTooltipElement,
offset: [0, -15],
positioning: 'bottom-center',
stopEvent: false,
// insertFirst属性控制Overlay是否应该被插入到地图元素列表的开头
insertFirst: false
});
this.map.addOverlay(this.measureTooltip);
},
// 向折线线段上添加距离overlay
putOverlayToLine(coordinate, text) {
const div = document.createElement('div');
div.className = 'ol-tooltip ol-tooltip-measure';
div.innerHTML = text;
// 创建Overlay
const overlay = new ol.Overlay({
element: div,
offset: [0, -15],
positioning: 'bottom-center',
stopEvent: false
});
this.map.addOverlay(overlay); // 将Overlay添加到地图
overlay.setPosition(coordinate); // 设置Overlay的位置
},
// 初始化绘制直线,开始测量
initInteraction() {
// 创建绘制折线的交互控件
this.draw = new ol.interaction.Draw({
source: this.drawSource,
type: 'LineString',
style: style1
});
// 将交互添加到地图
this.map.addInteraction(this.draw);
let listener; // 监听geomerty的change事件,开始时候绑定,结束时候解绑
this.draw.on('drawstart', (evt) => {
let lines = {}; // 每段折线的线集合
this.createMeasureTooltip(); // 创建一个overlay,用于实时显示距离,测量结束后停留在折线末端
this.helpTooltip.getElement().style.display = 'block'; // 展示提示信息
this.sketch = evt.feature;
let tooltipCoord = evt.coordinate;
// 绑定Geometry发生变化事件,实时显示距离和设置位置
listener = this.sketch.getGeometry().on('change', (evt) => {
// 展示总距离
const geom = evt.target;
let output = formatLength(geom); // 计算距离
tooltipCoord = geom.getLastCoordinate(); // 折线的最后一个点的坐标
this.measureTooltip.getElement().innerHTML = '总长' + output; // 显示计算后的距离
this.measureTooltip.setPosition(tooltipCoord); // 设置overlay位置显示在折线的末端
// 展示分段距离
const coordinates = geom.getCoordinates().slice(0, -1);
// 获取折线的每段线段 给除了最后一段线段增加距离显示
for (let i = 0; i < coordinates.length; i++) {
const start = coordinates[i]; // 折线起点
const end = coordinates[i + 1]; // 折线终点
if (!end) {
continue;
}
// 使用开始可结束的经纬度组成唯一标识
const key = 'l_' + start.join('_').replace(/[.]/g, '') + '-' + end.join('_').replace(/[.]/g, '');
if (!lines[key] && tooltipCoord.join('') != end.join('')) {
lines[key] = 1;
const start4326 = ol.proj.transform(start, 'EPSG:3857', 'EPSG:4326');
const end4326 = ol.proj.transform(end, 'EPSG:3857', 'EPSG:4326')
const distance = ol.sphere.getDistance(start4326, end4326); // 计算距离
this.putOverlayToLine(end, formatDistance(distance)); // 添加overlay显示折线距离
}
}
});
});
this.draw.on('drawend', (evt) => {
// 显示距离的div设置类名
this.measureTooltip.getElement().className = 'ol-tooltip ol-tooltip-static';
this.measureTooltip.setOffset([0, -7]);
evt.feature.setStyle(style2); // 设置折线样式
ol.Observable.unByKey(listener); // 解绑change事件
this.sketch = null;
this.measureTooltip = null;
});
}
},
mounted() {
this.initMap();
}
}).mount('#app')
</script>
</body>
</html>
备注
本文代码部分参考官方栗子所写。去掉了面积的测量,增加了每段线段距离的显示。一些css样式和函数直接复制过来的。写完后发现官方的另一个栗子实现了同样的效果,这个栗子使用Point来显示每段的距离,并且将Point放置在线段的中间位置。