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.youkuaiyun.com/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度位置,完美跨越~
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值