• openlayers实现跨时间轨迹线(贝塞尔曲线)


    说在前面

      还是接着上回那个活~
      他们以前用百度地图实现了一个跨时间线绘制轨迹线的功能,效果如下:
    在这里插入图片描述

      之所以需要这种功能,是因为海洋上没有实际的轨迹点,只能美化处理两端点连线,否则当从小于经度180度到大于-180的两个点相连时,若不做特殊处理,会反向拉一条直线,而不是跨域太平洋拉轨迹线。处理思路主要是判断从小于经度180度到大于-180的两个点,使用二阶贝塞尔曲线进行绘制,这样可以示意从东半球跨域到西半球的轨迹(为了美观,还需要判断跨越经度超过20度时,也做贝塞尔曲线处理,这个后面说)。

    开发环境

      开发环境:vue+openlayers6,底图为百度地图,可根据实际情况进行坐标转换

    第一步:做轨迹线跨日期判断,把对应的坐标点对存放起来

      核心方法就是要将超出最大轨迹线的那部分点的经度坐标偏移系数(40075016.68也就是最大经度的两倍值),这样在绘制时,才能接着最大轨迹往右边继续绘制(实际这样在ol里会出bug,后面会说如何解决)。

    /**
         * 处理轨迹线,特别是跨日期线,目前只考虑从180~-180,后续应增加-180到180的处理
         * @param pointsArr
         */
        dealTimeLineOfPoints(pointsArr) {
            if (!pointsArr || pointsArr.length < 1) {
                return null
            }
            //返回两类数据,一类是处理后的轨迹数据,用于单点定位到轨迹上;再存一个实际绘制的轨迹点对,可能是multipolyline,一类是绘制贝塞尔曲线的起始点对(默认超过50度需要绘制曲线,看起来比较圆滑好看)
            let reObject = {
                dealedLine: [],
                multiLine: [[]],
                bezierPoints: []
            }
            let eastToWestNum = 0//计算从东半球到西半球的次数,即系数n,用后一个坐标的经度减去n*360,实际绘制还得回归180~-180的坐标范围,否则会出现超出坐标范围的线消失的问题
            let pyNum = 40075016.68//偏移系数 360//
            for (let i = 0; i < pointsArr.length - 1; i++) {
                let p = pointsArr[i]
                //lngX和latY保存原始的坐标数据,用于前端列表显示
                // p.lngX=p.lng
                // p.latY=p.lat
                let pNext = pointsArr[i + 1]
                let newSt = [p.lng, p.lat]//第一个点的坐标点对
                let newEnd = [pNext.lng, pNext.lat]//第二个点的坐标点对
                //先判断相邻两个点是否第一个在0~180,第二个一个在-180~0,若是,则这一段线不绘制,用贝塞尔单独绘制,原始坐标不做处理,这种情况下的贝塞尔第二个点存储为处理后的
                let isMuli = false //经过时间线截断;
                let isBez = false//需要贝塞尔曲线的情况:经过时间线截断;两个经度相差超过50
                if (geosUtil.isInEast(newSt[0]) && !geosUtil.isInEast(newEnd[0]) && Math.abs(newEnd[0] - newSt[0]) > 180) {//每从东半球跨到西半球,且两者只差大于180度,就次数+1
                    eastToWestNum++
                    isMuli = true
                }
                //判断两个点经度是否相差20度,是的话则需要绘制曲线
                let cutBez = 20
                if (isMuli) {//在跨时间线的情况下不用判断是否差20度,需要直接绘制贝塞尔曲线
                    isBez = true
                } else {//非跨时间线情况
                    if (Math.abs(newEnd[0] - newSt[0]) >= cutBez) {
                        isBez = true
                    }
                }
                //坐标转换,这里把地理坐标(WGS84)转换为投影坐标(BD09),详见另一篇文章中的转换方法
                //https://blog.csdn.net/weixin_42356271/article/details/126125853?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22126125853%22%2C%22source%22%3A%22weixin_42356271%22%7D
                newSt = geomUtil.zc_format_coordinate(newSt[0], newSt[1])
                newEnd = geomUtil.zc_format_coordinate(newEnd[0], newEnd[1])
    
                if (isBez) {
                    if (isMuli) {
                        let newEndLon = newEnd[0] + pyNum //eastToWestNum*pyNum 由于最后都得回归180到-180,所以不用计算跨日期线的次数了//计算实际应该绘制的经度(绘制贝塞尔用)
                        reObject.bezierPoints.push([newSt, [newEndLon, newEnd[1]]])//跨时间线且经度相差超过50度,则第二个坐标的经度保存处理后的
                    } else {
                        reObject.bezierPoints.push([newSt, newEnd])//只是普通的绘制贝塞尔曲线,则只保存正常坐标
                    }
                }
                if (i == 0) {//第一个点添加
                    reObject.dealedLine.push(newSt)
                    if (!isBez) {//只有不绘制贝塞尔时,第一个点才加入多段线
                        reObject.multiLine[0].push(newSt)
                        reObject.multiLine[0].push(newEnd)
                    } else {//绘制时,将第二个点加入多段线
                        reObject.multiLine[0].push(newEnd)
                    }
                } else {//不是第一个点
                    if (!isBez) {//不绘制贝塞尔时(正常绘制),则增加进多段线中
                        reObject.multiLine[reObject.multiLine.length - 1].push(newEnd)//上一段轨迹添加坐标
                    }
                    else {//如果因为跨日期线或者两点跨度太大需要弧线美化,不需要在多段线中增加此坐标
                        reObject.multiLine.push([newEnd])//新建下一段轨迹,且添加第二个坐标,否则会漏一个点
                    }
                }
                reObject.dealedLine.push(newEnd)
            }
            console.dir(reObject)
            return reObject
        },
        /**
         * 判断经度是否在东半球,也就是0-180度,是返回true,否返回false
         * @param lon
         */
        isInEast(lon) {
            if (lon <= 180 && lon > 0) {
                return true
            } else {
                return false
            }
        },
    

      直接绘制效果
    在这里插入图片描述

    第二步:绘制贝塞尔曲线(二阶)

      网上找了很多绘制贝塞尔曲线的例子,都不太理想,这里重新梳理了下代码,可以支持自定义绘制二阶贝塞尔曲线的角度以及绘制精细度(拆分段数)。
      这里还增加了对跨度超过20度(可自定义)的坐标点对增加贝塞尔曲线绘制,可以美化效果。
    PS:此处有个bug需要处理,即超出20037508.34的值需截断成两部分绘制(将第二个点处理,整体左移2倍经度,详见代码),否则会出现挪动时这部分轨迹(超过最大经度的那部分轨迹线)消失,这应该是openlayers的一个bug,网上有资料说将wrapX设置为false就解决了,这个不适用于当前情况,因为我们就是要左右无限拖动的

        
        maxLon : 20037508.34,//最大经度
        maxLat : 20037508.34,//最大纬度
        /** 绘制贝塞尔曲线--------------------------------------------*/
        ConstantMultiVector2(c, pos) {
            return [c * pos[0], c * pos[1]];
        },
        vector2Add(a, b) {
            return [a[0] + b[0], a[1] + b[1]];
        },
    
        /**
         * a = > [ lng,lat]
         * b = > [ lng,lat]
         * n => ratio 比例,一般为50%,即0.5,此时拿的是中点位置
         * 二维向量线性插值
         */
        linerInperpote(a, b, n) {
            let curA = this.ConstantMultiVector2(1.0 - n, a);
            let curB = this.ConstantMultiVector2(n, b);
            return this.vector2Add(curA, curB);
        },
        conHopCount: 50,//拆分段数,默认50段
        /**
         * 获取 当前贝塞尔曲线上的 点坐标
         * @param originPos1
         * @param center
         * @param originPos2
         * @param times
         * @param hopCount 拆分段数,默认50段
         * @returns {*}
         */
        getCurrentEnd(originPos1, center, originPos2, times, hopCount) {
            hopCount = hopCount ? hopCount : this.conHopCount
            let curTimes = times / hopCount;
            let a = this.ConstantMultiVector2(Math.pow(1.0 - curTimes, 2), originPos1);
            let b = this.ConstantMultiVector2(2 * curTimes * (1 - curTimes), center);
            let c = this.ConstantMultiVector2(Math.pow(curTimes, 2), originPos2);
            return this.vector2Add(this.vector2Add(a, b), c);
        },
        /**
         * 获取控控制点:,若未提供偏移量,则默认起点正向偏移10度。
         * @param originPos1 起点
         * @param originPos2 终点
         * @param offset 偏移角度
         */
        getControlP(originPos1, originPos2, offset) {
            offset = offset ? offset : 10
            offset = 2 * Math.PI / 360 * offset //计算弧度
            //偏移量需要优化,可以设置根据角度(暂时设置30度夹角计算两个点之间的控制点)
            let centerPos = this.linerInperpote(originPos1, originPos2, 0.5);//开始点 与结束点 的中点 的位置
            let st2centerLen = calculateUtil.calculateLength(originPos1[0], originPos1[1], centerPos[0], centerPos[1])//起点到中点的距离
            let st2ControlLen = st2centerLen / Math.cos(offset)//起点到控制点的距离
            let shortLen = Math.abs(centerPos[1] - originPos1[1])//中点和起点的短边
            let longLen = Math.abs(centerPos[0] - originPos1[0])//中点和起点的长边
            let otherAngle = Math.atan(shortLen / longLen)//起止点与x轴夹角(弧度)
            let sumAngle = otherAngle + offset//起点-控制点与x轴的夹角(弧度)
            let sShort = st2ControlLen * Math.cos(sumAngle)//起点到控制点的短边:x轴
            let sLong = st2ControlLen * Math.sin(sumAngle)//起点到控制点的长边:y轴
            let controlX = originPos1[0] //控制点x
            let controlY = originPos1[1]//控制点y
            //这里根据第二个点与第一个点进行比对,来决定弧度向上或向下,若统一都向上,则可不进行判断,直接+
            if (originPos2[0] >= originPos1[0]) {
                controlX += sShort
            } else {
                controlX -= sShort
            }
            if (originPos2[1] >= originPos1[1]) {
                controlY += sLong
            } else {
                controlY -= sLong
            }
            return [controlX, controlY]
        }, 
         /** --------------------------------------------- */
        /**
         * 贝塞尔曲线绘制
         * @param bezierPoints 绘制贝塞尔的点集合,支持多段绘制,[[[1,1],[2,2]],[[3,3],[4,4]]]
         * @param offsetValue 偏移角度
         */
        drawBezelLine(bezierPoints, offsetValue) {
            //第一步,遍历需要绘制贝塞尔曲线的起始点对
            for (let i in bezierPoints) {
                let startPos = bezierPoints[i][0]
                let endPos = bezierPoints[i][1]
                let centerPos = this.getControlP(startPos, endPos, offsetValue)
                let res = [startPos]
                let times = 1
                while (times % geomUtil.conHopCount != 0) {
                    res.push(this.getCurrentEnd(startPos, centerPos, endPos, times))
                    times++
                }
                res.push(endPos)
                bezierPoints[i] = res
            }
            //第二步,判断经过时间线的两个坐标点对,其他正常的进行分组
            let mergeArr = [] //正常的坐标点对
            let cutBzArr = []//跨时间线的第二段贝塞尔的坐标点对
            let outBzArr = [] //全部超出时间线的部分
            let outNum=0
            for (let j in bezierPoints) {
                let curBezierPoints = bezierPoints[j]
                outNum = 0
                for (let i = 0; i < curBezierPoints.length - 1; i++) {
                    let st = curBezierPoints[i]
                    if(i==0){
                        mergeArr.push([])//先加一个空的
                        mergeArr[mergeArr.length-1].push(st) //第一个先加进去
                    }
                    let end = curBezierPoints[i + 1]
                    let compareV = this.isCrossTimeLine(st,end)
                    if(compareV=="FAN"){//如果正向跨时间线
                        let inPoint = geosUtil.calculateIntersectionPointOfTwoLines(st,end,[this.maxLon,this.maxLat/2],[this.maxLon,-this.maxLat/2])//计算交点
                        let halfLat = inPoint[1]//(end[1] + st[1]) / 2//以前取纬度中点作为交点效果略差
                        let lastLon = end[0] - 2 * this.maxLon//将第二个点处理,整体左移2倍经度
                        let lastLat = end[1]//第二个点纬度存储用于新增第二截的
                        mergeArr[mergeArr.length-1].push([this.maxLon,halfLat])//中间点加进去
                        //这一段要截断,从新开始增加下一段的,正常情况不会出现——————————————————————
                        mergeArr.push([])
                        //第二段起止坐标放到这个arr里
                        cutBzArr.push([[-this.maxLon,halfLat],[lastLon,lastLat]])
                    }else if(compareV=="ZHENG"){//如果反向跨时间线
    
                    }else if(compareV=="OUT"){//两个点都超出最大lon
                        if(outNum==0){//第一次查到,加[]
                            outBzArr.push([])
                            outBzArr[outBzArr.length-1].push([st[0] - 2 * this.maxLon,st[1]])//加第一个点
                        }
                        outBzArr[outBzArr.length-1].push([end[0] - 2 * this.maxLon,end[1]])
                        outNum++
                    }else{//正常情况
                        mergeArr[mergeArr.length-1].push(end)
                    }
                }
            }
            //合并arr
            let newBz_1 = [...mergeArr, ...cutBzArr]
            bezierPoints = [...newBz_1, ...outBzArr]
            return bezierPoints
        },
        //判断是否经过时间线
        isCrossTimeLine(bezArrSt,bezArrEnd){
            if(bezArrSt[0] > this.maxLon){//第一个点超出最大经度范围
                if(bezArrEnd[0] <= this.maxLon){//第二个点小于等于最大经度范围
                   return "ZHENG" //表示反方向跨时间线
                }else{//第二个点超出最大经度范围
                    return "OUT"
                }
            }else if(bezArrSt[0] <= this.maxLon) {//第一个点在正常经度范围内(小于等于)
                if (bezArrEnd[0] > this.maxLon) {//第二个点超出最大经度范围
                    return "FAN" //表示正方向跨时间线
                }
            }
            return "IN" //表示不跨时间线
        },
    

      增加贝塞尔曲线效果
    在这里插入图片描述

    第三步:优化:计算两条线的交点

      由于开始使用跨日期线的坐标点对与[180,90],[180,-90]的中点进行截断,效果不是很好,所以考虑还是使用两条线的交点进行截断,这样绘制出来效果就比较完美了。

    /**
         * 计算两条线的交点
         * @param x1
         * @param y1
         * @param x2
         * @param y2
         * @param x3
         * @param y3
         * @param x4
         * @param y4
         * @returns {*[]}
         */
        calculateIntersectionPointOfTwoLines([x1, y1], [x2, y2], [x3, y3], [x4, y4]) {
            debugger
            let a = {
                x: x1,
                y: y1
            }
            let b = {
                x: x2,
                y: y2
            }
            let c = {
                x: x3,
                y: y3
            }
            let d = {
                x: x4,
                y: y4
            }
            let rePs = this.segmentsIntr(a, b, c, d)
            return [rePs.x, rePs.y]
        },
        /**
         * 计算两条线的交点
         * @param a
         * @param b
         * @param c
         * @param d
         * @returns {*}
         */
        segmentsIntr(a, b, c, d) {
            // 三角形abc 面积的2倍
            var area_abc = (a.x - c.x) * (b.y - c.y) - (a.y - c.y) * (b.x - c.x);
    
            // 三角形abd 面积的2倍
            var area_abd = (a.x - d.x) * (b.y - d.y) - (a.y - d.y) * (b.x - d.x);
    
            // 面积符号相同则两点在线段同侧,不相交 (对点在线段上的情况,本例当作不相交处理);
            if (area_abc * area_abd >= 0) {
                return false;
            }
    
            // 三角形cda 面积的2倍
            var area_cda = (c.x - a.x) * (d.y - a.y) - (c.y - a.y) * (d.x - a.x);
            // 三角形cdb 面积的2倍
            // 注意: 这里有一个小优化.不需要再用公式计算面积,而是通过已知的三个面积加减得出.
            var area_cdb = area_cda + area_abc - area_abd;
            if (area_cda * area_cdb >= 0) {
                return false;
            }
    
            //计算交点坐标
            var t = area_cda / (area_abd - area_abc);
            var dx = t * (b.x - a.x),
                dy = t * (b.y - a.y);
            return {x: a.x + dx, y: a.y + dy};
    
        }
    

      放大后看180度位置,完美跨越~
    在这里插入图片描述

  • 相关阅读:
    中国智能网联汽车信息安全分析2022案例征集
    VINS系统框架--原理剖析
    图解SOC中的Timer(一):系统里有哪些Timer?
    CMMI和SPCA是一样的吗?有什么区别
    [云原生] Prometheus理论知识及系统搭建
    “一键合并剪辑,轻松添加片头——全新的视频编辑工具让你成为视频制作达人“
    计算机底层的秘密 摘抄笔记
    抵押品管理服务市场现状及未来发展趋势分析
    lv5 嵌入式开发-2 exec函数族
    【Kubernetes系列】Kubernetes相关概念介绍
  • 原文地址:https://blog.csdn.net/weixin_42356271/article/details/127089948