高德+React实现H5版高德打车-司机接驾-行程中 汽车行驶功能

2,484 阅读4分钟

先看效果图:

1626768140129823.gif

学习高德经纬度实时定位-精确地址 👈 戳这里

首先说一下大体思路:

  1. pathArr为空时表示没有规划路线,根据起点和终点位置开始规划路线;
  2. 接口5秒轮询不断拿到司机的实时位置,然后计算司机的位置是否在这条路线上;
  3. 在这条路线则计算小车的行驶路线,开始行驶等操作;不在这条路线则重新规划路线。
    /**
     * @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

  1. distance(p1:LngLat, p2:LngLat) 计算两个经纬度点之间的实际距离。单位:米
  2. closestOnLine(p:LngLat, line:[LngLat]) 计算line上距离P最近的点
  3. isPointOnLine(p:LngLat, line:[LngLat],tolerance:Number) 判断P是否在line上,tolerance为误差范围
  4. 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移动事件、和移动结束后;

移动中监听:

    1. 行驶后的路线是白色的,在小车移动过程中设置小车行驶过路线Polyline的path;
    1. 为了用户友好体验,避免出现一些问题,设置司机位置距离我的位置小于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. 行驶路线

  1. 每次轮询得到的司机位置‘startLngLat’与总路线‘pathArr’进行计算拿到距离路线最近的一个点;
  2. 拿到这个点以后,通过循环计算当前这个点与路线所有点的距离,得到一个距离集合;
  3. 通过遍历,找到距离最短的点的下标;(也就是司机位置距离这条线哪个点最近,把最近的点的下标找到)
    /**
     * @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
    }
  1. 距离最短的点的下标,也就是汽车位置的下标;比如:小车默认下标为0,当下次轮询得到司机位置与距离下标为5的位置很近,那我们就截取0到5的数据,这就是小车需要行驶的路线‘carRunPath’。

  2. 小车从下标0的位置行驶到下标为5的位置以后,将小车位置保存起来,一定要用全局变量去保存。

  3. 距离最近的下标大于小车当前位置,说明小车向前行驶;相反,小车不动。

  4. 计算行驶路线的长度‘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实现,服务端返回司机经纬度误差不是很大的话,应该是没有问题的,这里还需要后续不断的测试;没有服务端支持轮询接口,可以写一个定时器来模拟轮询,然后在地图中选择一些扎点模拟司机位置来测试行驶。还有一些计算这里没有详细介绍,感兴趣的大家可以手动试一试。

制作不易,顺便点个赞吧~😄 👍