使用openlayers绘制类似高德地图轨迹线并实现箭头动画效果

前言

        在高德地图中,如果使用过导航就可以看到它的轨迹线做的非常漂亮,用openlayer能实现吗?是可以的,只是稍微复杂了一点点。

1. 轨迹线构成分析

        我们来看看轨迹线是怎样组成的呢。

  1. 起点、终点可以用两个图标。然后使用openlayer的Image Style或者Overlay实现。
  2. 文字顺着线绘制,这个openlayer里已经有了,在样式类ol.style.Text里可以传入参数placement: 'line' 即可。

  3. 仔细观察,线是有边框的,openlayer的Style是无法直接实现的。这里我们可以取下巧,将其看为两个线组成,下面的线要宽一点,上面的线遮住下面的线,这里视觉上下面的线就成为了边框。
  4. 最后比较复杂的,线里面的箭头是顺着箭头的方向,并且是按照一定的间距分布的(在缩放地图不同分辨率下箭头间距也需要一致,这就需要动态去计算绘制)

2. 使用openlayer实现类高德轨迹线

        起点、终点、文字都可以使用openlayer自带的api实现,不复杂,这里就不讨论了。本次重点看下如何实现轨迹线的样式,以及在这个基础上实现箭头的动画效果。

2.1 如何实现一个高度定制的箭头

        如果我们要封装实现一个通用的轨迹绘制方法,需要考虑到箭头的问题(大小、箭头的宽度、箭头的角度、颜色可调),箭头不能使用普通的图片。在openlayer中样式类ol.style.Icon中有img属性可以传入HTMLCanvasElement,所以我们可以传入一个canvas对象,这样就简单了,我们可以根据传入的参数来定制这个箭头

        第二个问题是我们如何计算出箭头的样式的各个顶点。我们可以根据箭头的宽高,使用反三角函数来依次计算出各个顶点。 

/**
 * 获取航线右箭头
 * @param param
 * @returns {HTMLCanvasElement}
 */
function getRouteCanvasArrow(param) {
  let opt = Object.assign(
    {
      color: 'rgba(108,159,67,1)',
      lineWidth: 4,
      arrowHeight: 28,
      // 箭头夹角(度)
      angle: 110
    },
    param
  );

  if (opt.angle >= 180 || opt.angle <= 0) {
    throw new Error('箭头夹角取值范围为0-180');
  }

  const singleHeight = opt.arrowHeight / 2;
  // 计算出箭头宽度
  // 计算余切值
  const cotA = 1 / Math.tan((opt.angle * Math.PI) / 360);
  // 使用余切值计算相邻直角边, 箭头偏移宽度
  const offsetWidth = Math.ceil(cotA * singleHeight);
  const arrowWidth = offsetWidth + opt.lineWidth;

  const canvas = document.createElement('canvas');
  canvas.width = arrowWidth;
  canvas.height = opt.arrowHeight;
  let ctx = canvas.getContext('2d');
  ctx.fillStyle = opt.color;
  ctx.strokeStyle = opt.color;
  ctx.beginPath();
  ctx.lineTo(0, 0);
  ctx.lineTo(opt.lineWidth, 0);
  ctx.lineTo(arrowWidth, singleHeight);
  ctx.lineTo(opt.lineWidth, opt.arrowHeight);
  ctx.lineTo(0, opt.arrowHeight);
  ctx.lineTo(offsetWidth, singleHeight);
  ctx.closePath();
  ctx.stroke();
  ctx.fill();
  return canvas;
}

2.2 实现轨迹线样式 

2.2.1  箭头间隔如何计算

        我们可以使用ol.geom.LineString的getCoordinateAt方法来获取线上的点(参数取值范围0-1,0是起点,1是终点)。同时我们可以获取当前地图分辨率resolution,通过线的geometry的长度除以分辨率resulotion(lineGeom.getLength() / resolution ),可以得到线的总长度像素。假如我们需要箭头间隔是50个像素,我们只需要用50除以线段的总长度就知道需要几个箭头了,然后用得到的百分比调用上面提到的ol.geom.LineString的getCoordinateAt方法获取箭头的位置。

2.2.2 如何实现箭头动画

        在openlayer里可以监听图层的postcompose事件,拿到地图的framestate和vectorContext(vectorContext可以使用来绘制feature,性能比较高),如果要执行动画,只需要把framestate的animate属性设置为true即可,这样地图自己会去判断,并且使用requestAnimationFrame去执行map.render绘制下一帧,达到连续绘制形成动画的效果。

        如何让箭头像前进一样动起来呢,只需要在每次postcompose事件里,我们自己绘制时改变一下箭头的位置,让其向前移动一点点(比如说1个像素),就可以了。

2.2.3 封装绘制高德轨迹样式的函数

        按照上面的思路,封装绘制函数,参数可以输入线的宽度,颜色,箭头样式,间隔,是否开启箭头动画等参数。除了必要参数,都可以使用默认参数,就可以绘制出来,使用起来比较方便。

import * as MapUtil from './MapUtil';
import _ from 'lodash';

// 用于判断feature是否存在
let routeUniqId = 0;
/**
 * 绘制类似高德、百度的轨迹样式
 * @param { String | ol.vector.Layer } param.lineLayer              【必填】图层名或图层对象
 * @param { WKTString | ol.geom.LineString } param.lineGeom         【必填】轨迹线
 * @param { String } param.theme                                    【选填】设置样式,可选值:gray(类似于走过的灰色)
 * @param { Array<Integer> } param.lineColor                        【选填】线颜色
 * @param { number } param.lineWidth                                【选填】线宽(px)
 * @param { Array<Integer> | boolean } param.borderColor            【选填】线边框颜色,为false时不绘制边框
 * @param { number } param.borderWidth                              【选填】线边框宽度(px)
 * @param { Array<Integer> | boolean } param.arrowColor             【选填】箭头颜色,为false时不绘制箭头
 * @param { number } param.arrowWidth                               【选填】箭头线宽度(px)
 * @param { number } param.arrowGap                                 【选填】箭头间距(px)
 * @param { boolean } param.arrowAnime                              【选填】是否开启箭头动画,默认否
 * @param { (feature: ol.Feature) => void } param.cb                【选填】回调函数
 */
function drawRouteLine(param) {
  let opt = Object.assign(
    {
      lineLayer: null,
      lineGeom: null,
      theme: false,
      lineColor: [0, 186, 107],
      lineWidth: 10,
      // 为false时不绘制边框
      borderColor: [4, 110, 74],
      borderWidth: 2,
      // 为false时不绘制箭头
      arrowColor: 'rgba(255, 255, 255, 1)',
      arrowWidth: 3,
      arrowGap: 100,
      // 箭头动画
      arrowAnime: false,
      cb: null
    },
    param
  );
  let lineLayer = _.isString(opt.lineLayer) ? MapUtil.getLayer(opt.lineLayer) : opt.lineLayer;
  let lineGeom = _.isString(opt.lineGeom) ? MapUtil.WKT.readGeometry(opt.lineGeom) : opt.lineGeom;

  // 1 预处理theme
  if (opt.theme === 'gray') {
    opt.lineColor = [154, 207, 197];
    opt.borderColor = [127, 174, 159];
  }

  // 2.1 线样式
  let bottomPathStyle;
  let upperPathStyle = new ol.style.Style({
    stroke: new ol.style.Stroke({
      color: opt.lineColor,
      width: opt.lineWidth
    })
  });

  // 2.2 获取canvas箭头
  let arrowCanvas = MapUtil.getRouteCanvasArrow({
    color: opt.arrowColor,
    lineWidth: opt.arrowWidth,
    arrowHeight: opt.lineWidth
  });

  // 2.3 获取箭头样式
  const createArrowStyle = function (resolution, offset, vectorContext) {
    let resStyles = [];
    let lineLength = lineGeom.getLength() / resolution;
    if (lineLength < 50) {
      return resStyles;
    }
    let numArr = Math.ceil(lineLength / opt.arrowGap);
    let points = [];
    for (let i = 0; i <= numArr; i++) {
      let fracPos = i / numArr + offset;
      if (fracPos > 1) fracPos -= 1;
      let pg = new ol.Feature({
        geometry: new ol.geom.Point(lineGeom.getCoordinateAt(fracPos))
      });
      points.push(pg);
    }

    //确定方向并绘制
    lineGeom.forEachSegment((start, end) => {
      let line = new ol.geom.LineString([start, end]);
      _.forEach(points, point => {
        let coord = point.getGeometry().getFirstCoordinate();
        let cPoint = line.getClosestPoint(coord);
        if (Math.abs(cPoint[0] - coord[0]) < 1 && Math.abs(cPoint[1] - coord[1]) < 1) {
          let dx = end[0] - start[0];
          let dy = end[1] - start[1];
          let rotation = Math.atan2(dy, dx);
          let arrowStyle = new ol.style.Style({
            image: new ol.style.Icon({
              img: arrowCanvas,
              anchor: [1, 0.5],
              rotateWithView: true,
              rotation: -rotation,
              imgSize: [arrowCanvas.width, arrowCanvas.height]
            })
          });
          if (vectorContext) {
            vectorContext.drawFeature(point, arrowStyle);
          } else {
            arrowStyle.setGeometry(point.getGeometry());
            resStyles.push(arrowStyle);
          }
        }
      });
    });

    return resStyles;
  };

  // 3.绘制线
  if (opt.borderColor) {
    bottomPathStyle = new ol.style.Style({
      stroke: new ol.style.Stroke({
        color: opt.borderColor,
        width: opt.lineWidth + 2 * opt.borderWidth
      })
    });
    MapUtil.drawVectorFeature(lineLayer, {
      geom: lineGeom,
      style: bottomPathStyle
    });
  }

  let uid = routeUniqId++;
  let styles = upperPathStyle;
  if (!opt.arrowAnime && opt.arrowColor) {
    styles = function (feature, resolution) {
      let lineLength = lineGeom.getLength() / resolution;
      let offset = opt.arrowGap / lineLength / 2;
      let res = createArrowStyle(resolution, offset);
      res.push(upperPathStyle);
      return res;
    };
  }
  let lf = MapUtil.drawVectorFeature(lineLayer, {
    geom: lineGeom,
    style: styles,
    kv: { _uid_: uid }
  });

  // 4 执行回调
  try {
    opt.cb && opt.cb(lf);
  } catch (e) {
    console.error(e);
  }

  // 5.1 绘制箭头动画
  if (!opt.arrowAnime || !opt.arrowColor) {
    return;
  }

  // 5.2 绘制箭头动画函数
  let offset = 0.01;
  const drawArrow = function (evt) {
    let features = MapUtil.getFeatureByFilter(lineLayer, f => f.get('_uid_') == uid);
    if (!features.length) {
      lineLayer.un('postcompose', drawArrow);
      return;
    }

    let resolution = evt.frameState.viewState.resolution;
    createArrowStyle(resolution, offset, evt.vectorContext);

    let lineLength = lineGeom.getLength() / resolution;
    offset = offset + 1.5 / lineLength;
    //复位
    if (offset >= 1) offset = 0.001;
    // 告诉地图执行动画
    evt.frameState.animate = true;
  };

  lineLayer.on('postcompose', drawArrow);
}

2.3 最终效果

        使用封装的函数绘制出来的轨迹线,效果还是比较不错的。

        

  • 17
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值