先看效果图:
学习高德经纬度实时定位-精确地址 👈 戳这里
首先说一下大体思路:
- pathArr为空时表示没有规划路线,根据起点和终点位置开始规划路线;
- 接口5秒轮询不断拿到司机的实时位置,然后计算司机的位置是否在这条路线上;
- 在这条路线则计算小车的行驶路线,开始行驶等操作;不在这条路线则重新规划路线。
/**
* @description: 轮询函数
* @param {*} slnglat 起点(司机位置)
* @param {*} elnglat 终点(接驾中:起始地作为终点 / 行程中:目的地作为终点)
*/
drawDistance(slnglat, elnglat) {
let startLngLat = new AMap.LngLat(slnglat.lng, slnglat.lat)
let endLngLat = new AMap.LngLat(elnglat.lng, elnglat.lat)
if (this.pathArr.length === 0) { '表示没有路线,开始绘制'
this.drawPath(startLngLat, endLngLat)
return
}
let isPointInRing = AMap.GeometryUtil.isPointOnLine(startLngLat, this.pathArr, 80)
if (!isPointInRing) { '小车位置不在这条轨迹上'
this.drawPath(startLngLat, endLngLat)
} else {
this.setPassByPath() // 每次执行前 将上一次行驶的路线添加到总行驶路线
this.setMoveLine(startLngLat) // 设置小车5秒行驶的路线
this.moveAlongFn() // 小车开始行驶
}
}
跟着我一起来实现吧!!👊
下面用到的高德数学计算库:AMap.GeometryUtil
- distance(p1:LngLat, p2:LngLat) 计算两个经纬度点之间的实际距离。单位:米
- closestOnLine(p:LngLat, line:[LngLat]) 计算line上距离P最近的点
- isPointOnLine(p:LngLat, line:[LngLat],tolerance:Number) 判断P是否在line上,tolerance为误差范围
- distanceOfLine(ring:[LngLat]) 计算一个经纬度路径的实际长度。单位:米
1. 路径规划
首先我们需要实现的是路径规划,通过起点和终点位置规划一条小车需要行驶的路线,且路线分为最快捷、最经济、最短距离、考虑实时路况;这里大家可以选择一个,暂时没有具体对比这几种策略哪个更好,我选的最快捷:AMap.DrivingPolicy.LEAST_TIME
/**
* @description: 规划路线
* @param {*} startLngLat 起点
* @param {*} endLngLat 终点
*/
drawPath(startLngLat, endLngLat) {
this.clearCarState() // 绘制之前要清除所有覆盖物并重置数据(省略详细)
this.map.plugin('AMap.Driving', () => { // 加载插件-插件为地图功能的扩展
let driving = new AMap.Driving({ // 构造路线导航类
hideMarkers: true, // 隐藏路径起始点图标
autoFitView: true, // 自动调整地图视野
policy: AMap.DrivingPolicy.LEAST_TIME, // 驾车路线规划策略
extensions: 'all', // 详细信息
})
driving.search(startLngLat, endLngLat, (status, result) => {
console.log(result, '规划的路线信息')
if (status === 'complete') {
this.setLineData(result) // * 路线数据处理
this.createCover(endLngLat) // * 根据路线开始绘制
}
})
})
}
对路线信息进行数据处理,得到一维对象数组
setLineData(result) {
let paths = []
let routes = result.routes && result.routes.length > 0 ? result.routes[0] : null
if (routes) {
this.titDistance = routes.distance // 路径总长度
this.titTimes = routes.time // 路径总时长
let steps = routes.steps || []
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps[i].path.length; j++) {
paths.push(steps[i].path[j])
}
}
}
this.pathArr = paths
}
2. 路径绘制
根据路线pathArr集合开始绘制:总路线Polyline、 单次行驶路线Polyline、 总行驶路线Polyline;时间距离Marker、汽车Marker;Marker和Polyline如何创建,具体可以查看高德官网API,这里就不做详细介绍了;
createCover(endLngLat) {
let paths = this.pathArr
this.carTitMarker = new AMap.Marker({ '距离和时间marker'
map: this.map,
position: paths[0], // 初始位置
content: '',
zIndex: 102,
})
this.setCarTime() '为carTitMarker添加内容'
// this.getAngle() 下面会做详细介绍-------
let angle = this.getAngle(paths[0], paths[1]) - 90 // Marker的初始角度0度=90度
this.carMarker = new AMap.Marker({ '小车marker'
map: this.map,
position: paths[0],
content: `<img class='caricon' src='/images/newtaxi/qiche.svg'/>`,
offset: new AMap.Pixel(-15, -10),
autoRotation: true,
angle: angle, '汽车角度'
zIndex: 102
})
this.currentLine = new AMap.Polyline({ '总路线绘制'
map: this.map,
path: paths,
strokeColor: "#45C184",
lineJoin: 'round',
lineCap: 'round',
strokeOpacity: 1, // 线条透明度
strokeWeight: 6, //线条宽度
showDir: true
});
let polyConfig = {
map: this.map,
strokeColor: "#fff", // 行驶过的路线为白色
lineJoin: 'round',
lineCap: 'round',
strokeOpacity: 1,
strokeWeight: 7,
showDir: false
}
this.passByLine = new AMap.Polyline(polyConfig) '小车总行驶后的路线polyline'
let drivingLine = new AMap.Polyline(polyConfig) '小车5秒行驶的路线polyline'
}
根据两个经纬度计算角度:
汽车默认绘制的时候,需要设置汽车行驶的角度;传入两个经纬度,通过 Math.atan2()、 Math.PI计算出汽车的角度。这里需要注意的是高德0度=-90度。
getAngle(startPoint, endPoint) {
if (!(startPoint && endPoint)) {
return 0;
}
let dRotateAngle = Math.atan2(
Math.abs(startPoint.lng - endPoint.lng),
Math.abs(startPoint.lat - endPoint.lat)
);
if (endPoint.lng >= startPoint.lng) {
if (endPoint.lat >= startPoint.lat) {
} else {
dRotateAngle = Math.PI - dRotateAngle;
}
} else {
if (endPoint.lat >= startPoint.lat) {
dRotateAngle = 2 * Math.PI - dRotateAngle;
} else {
dRotateAngle = Math.PI + dRotateAngle;
}
}
dRotateAngle = (dRotateAngle * 180) / Math.PI;
return dRotateAngle;
}
监听小车Marker移动事件、和移动结束后;
移动中监听:
-
- 行驶后的路线是白色的,在小车移动过程中设置小车行驶过路线Polyline的path;
-
- 为了用户友好体验,避免出现一些问题,设置司机位置距离我的位置小于150米,轨迹隐藏;距离我的位置小于100米,距离时间提示隐藏。
移动结束后:
- 1.移动结束后,将移动结束的位置滑动到地图中心点。
this.carMarker.on('moving', (e) => {
drivingLine.setPath(e.passedPath)
this.movingFn(e.passedPath, endLngLat)
})
this.carMarker.on('movealong', (e) => {
this.setCarTime() // 行驶完更新时间
if (this.carIndex > 0 && this.pathArr.length > this.carIndex) {
let carLoc = this.pathArr[this.carIndex]
this.setCenter(carLoc)
}
})
movingFn(path, endLngLat) {
let distance = AMap.GeometryUtil.distance(path[path.length - 1], endLngLat)
if (distance < 150) { // 司机位置距离我的位置小于150米,轨迹隐藏
this.currentLine && this.currentLine.hide()
}
if (distance < 100) { // 距离我的位置小于100米,距离时间提示隐藏
this.carTitMarker && this.carTitMarker.hide()
}
}
3. 行驶路线
- 每次轮询得到的司机位置‘startLngLat’与总路线‘pathArr’进行计算拿到距离路线最近的一个点;
- 拿到这个点以后,通过循环计算当前这个点与路线所有点的距离,得到一个距离集合;
- 通过遍历,找到距离最短的点的下标;(也就是司机位置距离这条线哪个点最近,把最近的点的下标找到)
/**
* @description: 设置汽车行驶的路线
* @param {*} startLngLat 司机位置
*/
setMoveLine(startLngLat) {
let line_near = AMap.GeometryUtil.closestOnLine(startLngLat, this.pathArr, 50); // 司机位置直线距离轨迹最近的点
let distanceList = [] //距离集合
for (let i = this.pathArr.length; i--;) {
let distances = AMap.GeometryUtil.distance(line_near, this.pathArr[i]) // 计算距离
distanceList[i] = distances
}
let index = this.arrayMin(distanceList) // 距离最短的点的下标
//.....
}
处理距离集合,循环遍历拿到距离最短的下标值
arrayMin(arr) {
let len = arr.length
let min = Infinity
let minIndex = 0
while (len--) {
if (arr[len] < min) {
min = arr[len];
minIndex = len
}
}
return minIndex
}
-
距离最短的点的下标,也就是汽车位置的下标;比如:小车默认下标为0,当下次轮询得到司机位置与距离下标为5的位置很近,那我们就截取0到5的数据,这就是小车需要行驶的路线‘carRunPath’。
-
小车从下标0的位置行驶到下标为5的位置以后,将小车位置保存起来,一定要用全局变量去保存。
-
距离最近的下标大于小车当前位置,说明小车向前行驶;相反,小车不动。
-
计算行驶路线的长度‘moveRice’并保存,为汽车行驶计算速度提供。
if (index > this.carIndex) { // * 司机位置小车当前位置前面,小车行驶
this.carRunPath = this.pathArr.slice(this.carIndex, index + 1)
this.moveRice = Math.round(AMap.GeometryUtil.distanceOfLine(this.carRunPath)) '计算行驶路线长度'
// * 行驶速度进行匀速处理---通过路径长度去控制,路径过短则等待下一次
let overlen = distanceList.length - 5 // 结尾的路程根据小车真实移动去改变位置
if (index < overlen && this.moveRice < 20) { // 路径小于20米,小车不动--解决距离过短汽车行驶不平滑问题
this.carRunPath = []
return
}
this.carIndex = index
} else { // * 司机位置没动或者在小车位置后面,小车不动(避免小车行驶后重复行驶)
console.log('位置没动');
this.carRunPath = []
}
4. 汽车行驶
当设置好小车需要行驶的路线‘carRunPath’时,同时也知道了这段路线的长度‘moveRice’,因为我们设置的5秒轮询,所有行驶的路程需要在5秒内行驶完,不然会出现小车闪跳现象;
- 计算速度:【长度/5秒】计算出每秒行驶的长度,【长度*3.6】然后转换成 千米/小时
moveAlongFn() {
if (this.carRunPath.length > 0) {
let speed = (this.moveRice / 5) * 3.6 // 计算5秒内行驶的速度
this.carMarker.moveAlong(this.carRunPath, speed) '小车行驶'
this.carTitMarker.moveAlong(this.carRunPath, speed) '距离时间跟随小车一起行驶'
}
}
5. 总行驶路线
最后需要做的是 在小车每次行驶之前,将小车当次行驶的路线记录到总行驶路线中,不然每次会将前面的路线会重新绘制。总行驶路线的Polyline setPath()
setPassByPath() {
if (this.carRunPath.length > 0) { // 小车位置可能没动,就不会执行以下操作
this.carPassByPath = this.carPassByPath.concat(this.carRunPath)
this.passByLine.setPath(this.carPassByPath)
}
}
6. 结束
很多还是需要依赖高德的api实现,服务端返回司机经纬度误差不是很大的话,应该是没有问题的,这里还需要后续不断的测试;没有服务端支持轮询接口,可以写一个定时器来模拟轮询,然后在地图中选择一些扎点模拟司机位置来测试行驶。还有一些计算这里没有详细介绍,感兴趣的大家可以手动试一试。