openlayers 绘制动态迁徙线、曲线

前言:本来懒得写这个博客,实在深感无聊,没啥事情做,出篇博客让大家看看。文章会尽可能简短。

简单效果

掉帧属录屏效果,尚未测试过性能,因为这个可以看自己调节。以下为一条贝塞尔曲线分了180段的效果描述。
颜色属于瞎编乱造,只为示例,不为效果负责。
在这里插入图片描述
在这里插入图片描述

来自新增2022/7/26

发了个包,应该说如果你想学习实现的思路,尽管往下看,如果你想要使用这个效果
ol-dynamic-curves地址
直接根据npm的内容使用即可。
里面装了一些比较常见的可能会处理到的API,新增删除,等等,以及配置的选项。
如果你想学习代码实现的核心内容
b站视频

准备步骤

1、先生成起点与终点的表示点。这个很重要,原因在于:openlayers 会智能的检测图层中的数据源(source)是否有需要更新的features,如果你没有设置features,或者不在视图内,是不会触发渲染、因此,也就不会触发我们需要的prerender事件。
2、监听图层的prerender 事件,顾名思义,prerender 意味着这是 openlayers 暴露出来的一个图层的渲染事件,prerender意味着在渲染前执行的一个函数,他会传入一个renderEvent对象。
3、获取到renderEvent之后,我们可以通过getVectorContext()此API,获取一个有关于openlayers底层对当前图层绘制的canvas内容,里头主要封装了两个操作:绘制geometry, 设置样式

核心实现

贝塞尔曲线的实现

上图中可以看出,线是动态画出来的,其实是由一个个密集的线表示而绘制出来的一条曲线。为此,我们分为三点一个个去说。
1、动态增加的线
需要一个数组去记录当前应该渲染的线的集合。以及一个用于表示上一个结束点坐标的位置 去 在下一个阶段 作为 开始位置。

let lineCoords = []
let startPos = ...
layer.on('prerender',function(evt){
    
    
	let endPos = []
	lineCoords.push([startPos,endPos])
	let geometry = new MultiLineString(lineCoords);
})

2、曲线的绘制

本例使用二阶贝塞尔曲线绘制。不懂请去别处找资料。

主要在于控制点的寻找。以及如何获取当前贝塞尔曲线上的点坐标。
getCurrentEnd 函数 里面的计算 取自 百度百科上的贝塞尔曲线 二次方公式

function ConstantMultiVector2(c, pos) {
    
    
        return [c * pos[0], c * pos[1]];
      }
      function vector2Add(a, b) {
    
    
        return [a[0] + b[0], a[1] + b[1]];
      }

      /**
       * a = > [ lng,lat]
       * b = > [ lng,lat]
       * n => ratio
       * 二维向量线性插值
       */
      function linerInperpote(a, b, n) {
    
    
        let curA = ConstantMultiVector2(1.0 - n, a);
        let curB = ConstantMultiVector2(n, b);
        return vector2Add(curA, curB);
      }
      //  获取 当前贝塞尔曲线上的 点坐标
      function getCurrenetEnd(originPos1, center, originPos2, times) {
    
    
        let curTimes = times / 180;
        let a = ConstantMultiVector2(Math.pow(1.0 - curTimes, 2), originPos1);
        let b = ConstantMultiVector2(2 * curTimes * (1 - curTimes), center);
        let c = ConstantMultiVector2(Math.pow(curTimes, 2), originPos2);
        return vector2Add(vector2Add(a, b), c);
      }

以下表示控制点为: 开始点 与结束点 的中点 的经纬度位置 ,经度减10作为控制点。

let controlPos = vector2Add(
        linerInperpote(originPos1, originPos2, 0.5),
        [-10.0, 0]
      )

3、段数的处理(时间的增加)
涉及到动画必然跟时间会有联系。这里我们选择以线段的处理到达终点作为一个周期。
将整个线位置的输入过程分为180段。并非180段最好,这个看个人的设置。理论上来说段数越高,表现越明显,当然,运动会越慢,所以合适就好。

let times = 0;
layer.on("prerender", (evt) => {
    
    
	if (times % 180 === 0) {
    
    
          times = 0;
          lineCoords = [];
          lastEndPos = startPos;
  	}
  	times++;
  	layer.changed()
}

在结束一个周期时,初始化相关的变量。
调用layer.changed() 重复执行这个函数,即告诉openlayers框架:当窗口中存在该图层的features时,始终更新此图层。
4、渲染

layer.on("prerender", (evt) => {
    
    
	let geometry = new MultiLineString(multiCoords);
	let ctx = getVectorContext(evt);
	ctx.setStyle(
          new Style({
    
    
            stroke: new Stroke({
    
    
              // 模板字符串
              color: 'red',
              lineCap: "butt",
              width: 3,
            }),
          })
	);
}
ctx.drawGeometry(geometry);

样式处理

渐变色处理

理论上来说,你可以操作每一条线的颜色,但通常我们不会这么做,因为太损耗性能了。(理由跟canvas的底层设计有关,有兴趣可以去搜索下。总之fillStyle strokeStyle的设置耗时可能比绘制还长)

在这里插入图片描述

而可以看到上图,实际上就是个渐变色的应用。只不过是比较不常见的一个圆形渐变。不使用大家更常见的linear-gradient渐变 也就是线性渐变的原因如下图。
在这里插入图片描述
从表现形式上来说,我更喜欢一小段呈现出更加多变的颜色。而且只要颜色设置的较为相近,应该说线条的颜色还是会挺好看的。
圆形渐变许多人了解较少。这里特地说明一下。
在这里插入图片描述
主要分为开始圆跟结束圆的渐变色叠加,也就是说,我们大可以设置两个同样的色板,对开始圆的坐标进行偏移达到一种绚丽的效果。但更普遍的,我们一般只用一个圆就够了。
在使用之前,我们还需要计算当前两个线之间,开始点与结束点的距离以让整个圆在开始点的坐标将颜色扩散出去。同时将开始点与结束点都迁移到开始点的屏幕像素位置。

		// 通过getPixelFromCoordinate 获取当前位置对应的屏幕像素位置
      	let getPixelFromCoordinate = this.map.getPixelFromCoordinate.bind(
        	this.map
      	);
        let startGrdPixelPos = getPixelFromCoordinate(pos1);
        let endGrdPixelPos = getPixelFromCoordinate(pos2);
        let xdiff = endGrdPixelPos[0] - startGrdPixelPos[0] 
        let ydiff = endGrdPixelPos[1] - startGrdPixelPos[1]
        let radius = Math.pow(Math.pow(xdiff,2) + Math.pow(ydiff,2),0.5);
        var grd = ctx.context_.createRadialGradient(
          startGrdPixelPos[0],
          startGrdPixelPos[1],
          0,
          startGrdPixelPos[0],
          startGrdPixelPos[1],
          radius
        );
        grd.addColorStop(0, "yellow");
        grd.addColorStop(0.2, "red");
        grd.addColorStop(0.4, "pink");
        grd.addColorStop(0.6, "green");
        grd.addColorStop(0.8, "orange");
        grd.addColorStop(1, "blue");
        ctx.setStyle(
          new Style({
    
    
            stroke: new Stroke({
    
    
              // 模板字符串
              color: grd,
              lineCap: "butt",
              width: 3,
            }),
          })
        );
        ctx.drawGeometry...
箭头处理

本实例中箭头主要是通过添加Icon 的方式 对图片进行旋转达到的。所以说,对比使用逻辑去计算的箭头应该说方便许多。但是有一点在这里需要注意: 不要使用src 去 为Icon 添加图片。此处也困扰了我很久,后面我基本上确定这就是一个BUG。使用src属性在prerender函数这里调用setStyle你是创建不了图片的。至于是为什么,这里就不再赘述了。
因此,我们使用图片对象去做处理。

let arrowImage = new Image();
// 再说一次: 在vue 里面, 静态文件资源放于public目录下
// 意味着此时的请求路径,如果你的端口是8080,从本质上来说等于: http://localhost:8080/image/arrow1.png
// 你的目录结构应为 public/image/arrow1.png
// 再问我就自杀
arrowImage.src = "image/arrow1.png";
let arrowFlag = false;
arrowImage.onload = function () {
    
    
	arrowFlag = true;
};

layer.on("prerender", (evt) => {
    
    
        let arrowGeometry = new Point(endPos);
		const dx = endPos[0] - lastEndPos[0];
        const dy = endPos[1] - lastEndPos[1];
        const rotation = Math.atan2(dy, dx);
        if (arrowFlag) {
    
    
          ctx.setImageStyle(
            new Icon({
    
    
              img: arrowImage,
              rotateWithView: true,
              rotation: -rotation,
              imgSize: [16, 16],
            })
          );
        }
        ctx.drawGeometry(arrowGeometry);
}

绘制多条动态曲线线段

经过上面的核心实现后,我们离应用还差一点。首先在多个不同的开始点与结束点坐标之中。我们如何计算一个大致的控制点 以及 如何描述一个曲线线段的运动。

仔细观察上述所需的属性。 我们将他 放置在一个类里统一管理。命名为source 表示 这个动态曲线的 数据源(有点像openlayers的设计,图层=》数据源=》?)。 但由于我们的实现是完全基于geometry变化进行处理的, 为了描述这个geometry的变化,我们往推进去的这一个feature 对他的属性挂载上这个数据源。

// 一个用于描述 flyLine 属性 的数据源
export default class FlyLineSource {
    
    

    constructor(options) {
    
    
        /**
         * 开始点位置经纬度表示
         */
        this.startPos = options.startPos

        /**
         * 结束点位置经纬度表示
         */
        this.endPos = options.endPos

        /**
         * 线段中点位置
         */
        this.centerPos = this.linearInterpolation(this.startPos, this.endPos, 0.5)
        
        /**
         * 控制点位置
         */
        this.controlPos = this.getControlPoint(this.startPos,this.endPos,this.centerPos,1.0)

        /**
         * 保存上一个结束点位置 初始则为 开始点
         */
        this.lastEndPos = this.startPos

        /**
         * 计数器 用于分段
         */
        this.times = 0;

        /**
         * 曲线 数组
         */
        this.lineCoords = []

        /**
         * 箭头相关
         */
        this.arrowImage = new Image()
        this.arrowImage.src = 'arrow.png'
        this.arrowLoad = false
        this.arrowImage.onload = () => {
    
    
            this.arrowLoad = true
        }

    }

    getControlPoint(startPos,endPos,centerPos,ratio){
    
    
        let xDiff = endPos[0] - startPos[0]
        let addX = startPos[0] + xDiff
        let addY = startPos[1]
        let controlX,controlY
        controlX = addX
        controlY = addY
        let controlPos = [controlX , controlY]
        // this real control pos  
        return this.linearInterpolation(centerPos,controlPos, ratio)
    }

    getRenderState() {
    
    
        return {
    
    
            times: this.times,
            lineCoords: this.lineCoords,
            startPos: this.startPos,
            centerPos: this.centerPos,
            endPos: this.endPos,
            lastEndPos: this.lastEndPos,
            arrowImage: this.arrowImage,
            arrowLoad: this.arrowLoad,
            controlPos: this.controlPos
        }
    }

    setRenderState(options){
    
    
        for(let i in options){
    
    
            this[i] = options[i]            
        }
    }

    // 线性插值 函数 ... 此处的计算 只处理  二维  带x ,y 的 向量
    linearInterpolation(startPos, endPos, t) {
    
    
        let a = this.constantMultiVector2(1 - t, startPos)
        let b = this.constantMultiVector2(t, endPos)
        return this.vector2Add(a, b)
    }

    // 常数乘以二维向量数组 的函数
    constantMultiVector2(constant, vector2) {
    
    
        return [constant * vector2[0], constant * vector2[1]];
    }


    vector2Add(a, b) {
    
    
        return [a[0] + b[0], a[1] + b[1]]
    }

}

在外部定义 生成一个特别的点 feature

	// 根据 pointPositions 生成 point Features
    generatePointsFeatures() {
    
    
        for (let i = 0, ii = this.pointPositions.length; i < ii; i++) {
    
    
            let registeFeature = new Feature({
    
    
                geometry: new Point(fromLonLat(this.pointPositions[i][0])),
                flyLineSource: new FlyLineSource({
    
    
                    startPos: this.pointPositions[i][0],
                    endPos: this.pointPositions[i][1]
                })
            })
            this.pointsFeatures.push(registeFeature, new Feature({
    
     geometry: new Point(fromLonLat(this.pointPositions[i][1])) }))
        }
    }

猜你喜欢

转载自blog.csdn.net/q1025387665a/article/details/125429434