基于OpenLayers+rbush实现高德轨迹样式

一 前言

  近期翻阅博客,看到社区大神一休哥的一篇《canvas 奇巧淫技(二)绘制箭头路径效果》文章,同样,该大神还展示过一个使用rbush库如何在前端快速从海量数据进行空间检索的案例:https://alex2wong.github.io/mapbox-plugins/examples/rbush/,很有分享精神的前端GIS专家,更多关于前端GIS检索数据的技术可参考搜狐的干货专访:《深入理解空间搜索算法 ——数百万数据中的瞬时搜索》。关于轨迹样式带导航箭头这种常见问题,笔者基于兴趣和朋友们的总结,也试着用熟悉的OpenLayers的StyleFunction去实现一个这样的玩具,在此分享给大家。

高德轨迹箭头.png

  基于已知的一条轨迹,实现这样的一个导航轨迹箭头,需要解决三个问题:

  • 在轨迹上根据固定像素间隔,计算当前地图分辨率下箭头总数量。
  • 计算当前地图分辨率下,每个箭头的绘制位置。
  • 计算好箭头的数量和位置后,要确定箭头的方向。

一 箭头数量

  由高德轨迹箭头图可知,每隔固定像素,打上一个箭头。假设当前的线LineString地理长度为length,当前固定像素间隔stpes=n像素,在当前地图比例尺res已知的情况下,n像素地理距离是resn,那么箭头总数count=length/(resn):

let length=line_geom.getLength();//线图形的地理长度
const steps=40;//每隔40像素打一个箭头点
let geo_steps=map_res*steps;//40像素长度在当前地图比例尺下地理长度。
let arrow_count=length*1.0/geo_steps;

多么浅显易懂的道理啊,第一个问题很顺利的解决了。

二 箭头位置

  第一步得到了箭头的总数,在获取箭头位置时,一个重要的API是线条LineString的getCoordinateAt,利用它我们在轨迹线上获取箭头点的位置。

/*
fraction:参考点的百分比,如0就是LineString的起点,1就是LineString的终点,0.5就是LineString的中点。
*/
linestring.getCoordinateAt(fraction, opt_dest)

  假如箭头总数为arrowsNum,那么arrowsNum个箭头的数量分别是

 for(let i=1;i<arrowsNum;i++){
       let arraw_coor=geometry.getCoordinateAt(i*1.0/arrowsNum);
       console.log(arraw_coor);//输出每个箭头的坐标
 }

  得到每个箭头的位置后,我们先可视化下吧,OpenLayers的地图样式完全由StyleFunction实现的,完整样式代码如下:

/*
feature:地图上的要素对象,既有属性,也有坐标图形。
res:当前地图分辨率参数。
return:返回一个定制的渲染样式
*/
var styleFunction = function(feature,res){
        //轨迹线图形
       var trackLine= feature.getGeometry();
       var styles = [
          new ol.style.Style({
            stroke: new ol.style.Stroke({
              color: '#2E8B57',
              width: 10
            })
          })
        ];
        //轨迹地理长度
        let length=trackLine.getLength();
        //像素间隔步长
        let stpes=40;//像素步长间隔
        //将像素步长转实际地理距离步长
        let geo_steps=stpes*res;
        //箭头总数
        let arrowsNum=parseInt(length/geo_steps);
        for(let i=1;i<arrowsNum;i++){
            let arraw_coor=trackLine.getCoordinateAt(i*1.0/arrowsNum);
            styles.push(new ol.style.Style({
                geometry: new ol.geom.Point(arraw_coor),
                image: new ol.style.Circle({
                    radius: 7,
                    fill: new ol.style.Fill({
                        color: '#ffcc33'
                    })
                })
            }));
        }
        return styles;
}
箭头位置计算与可视化结果.png

三 箭头方向

  之前的逻辑,我们已经计算了一个轨迹样式的雏形了,把地图上箭头位置的黄点改成一个箭头图标,做下方向旋转就可以了。在说明此前,需要说明下轨迹,segment线段,箭头点之间的关系,如下图:


轨迹,segment,箭头位置之间的关系.png

  观察示意图,总结如下:

  • 一条完整的轨迹由多个连续的segment组成。
  • 通过getCoordinateAt方法计算得到的箭头点,一定是在轨迹线上的某个点。
  • 每个箭头点的方向是由箭头点落在的segment的方向决定的。
    很显然,计算箭头方向其实就是计算每个箭头点到底落在了哪个segment上,将segme方向赋予箭头点。这里我们引入了rbush库构建空间索引,计算轨迹点与segment对应关系。
      之所以我要引入rbush库,是解决循环计算问题,想象下如果不引入rbush库,只能使用如下的伪代码暴力计算了:
for(let i=0;i<arrows.length;i++){
      for(let j=0;j<segments.length;j++){
              if(instersects(arrows[i],segments[j])===true){
                       // arrows[i]对应的segments是segments[j]
                      break;
              }
      }
}

感觉逻辑很简单啊,这样做难道不可以吗?想象下,箭头数量,segment的数量其实都是不可控的,一个复杂的轨迹线可能由成百上千的近万的segments,这样一个个循环去匹配,效率是不是就有问题了?所以引入了空间索引。这里查询,使用了rbush进行btree查询,查询的结果后再详细比对是否和箭头相交,累了,直接贴代码了,不详述了:

 var styleFunction = function(feature,res){
        //轨迹线图形
       var trackLine= feature.getGeometry();
       var styles = [
          new ol.style.Style({
            stroke: new ol.style.Stroke({
              color: '#2E8B57',
              width: 10
            })
          })
        ];
        //对segments建立btree索引
        let tree= rbush();//路段数
        trackLine.forEachSegment(function(start, end) {
            var dx = end[0] - start[0];
            var dy = end[1] - start[1];
            //计算每个segment的方向,即箭头旋转方向
            let rotation = Math.atan2(dy, dx);
            let geom=new ol.geom.LineString([start,end]);
            let extent=geom.getExtent();
            var item = {
              minX: extent[0],
              minY: extent[1],
              maxX: extent[2],
              maxY: extent[3],
              geom: geom,
              rotation:rotation
            };
            tree.insert(item);
        });
        //轨迹地理长度
        let length=trackLine.getLength();
        //像素间隔步长
        let stpes=40;//像素步长间隔
        //将像素步长转实际地理距离步长
        let geo_steps=stpes*res;
        //箭头总数
        let arrowsNum=parseInt(length/geo_steps);
        for(let i=1;i<arrowsNum;i++){
            let arraw_coor=trackLine.getCoordinateAt(i*1.0/arrowsNum);
            let tol=10;//查询设置的点的容差,测试地图单位是米。如果是4326坐标系单位为度的话,改成0.0001.
            let arraw_coor_buffer=[arraw_coor[0]-tol,arraw_coor[1]-tol,arraw_coor[0]+tol,arraw_coor[1]+tol];
            //进行btree查询
            var treeSearch = tree.search({
              minX: arraw_coor_buffer[0],
              minY: arraw_coor_buffer[1],
              maxX: arraw_coor_buffer[2],
              maxY: arraw_coor_buffer[3]
            });
            let arrow_rotation;
            //只查询一个,那么肯定是它了,直接返回
            if(treeSearch.length==1)
              arrow_rotation=treeSearch[0].rotation;
            else if(treeSearch.length>1){
                let results=treeSearch.filter(function(item){
                  //箭头点与segment相交,返回结果。该方法实测不是很准,可能是计算中间结果
                  //保存到小数精度导致查询有点问题
                  // if(item.geom.intersectsCoordinate(arraw_coor))
                  //   return true;

                  //换一种方案,设置一个稍小的容差,消除精度问题
                  let _tol=1;//消除精度误差的容差
                  if(item.geom.intersectsExtent([arraw_coor[0]-_tol,arraw_coor[1]-_tol,arraw_coor[0]+_tol,arraw_coor[1]+_tol]))
                    return true;
                })
                if(results.length>0)
                  arrow_rotation=results[0].rotation;
            }
            styles.push(new ol.style.Style({
                geometry: new ol.geom.Point(arraw_coor),
                image: new ol.style.Icon({
                  src: '../static/content/images/arrowright.png',
                  anchor: [0.75, 0.5],
                  rotateWithView: true,
                  rotation: -arrow_rotation
                })
            }));
        }
        return styles;
      }
轨迹箭头效果图.png

看着还凑合吧,但其实要做到高德那个精细的样式,才万里第一步,祝诸君继续研究,期待更好的效果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容