百度地图历险记之LuShu路书全解

百度地图历险记之LuShu路书全解

百度地图历险记之LuShu路书全解

项目简介:

接了个双创项目的前端开发的活,主要用地图来展示一些信息,比如时间地点事件什么的。使用了Antd+react解决方案。

欢迎点个star支持下:思源siyuan

React-BMapGL文档 (baidu.com)有React中使用百度地图API的封装,可以直接使用,但很可惜事实上只封装了一小部分,很多功能并没有直接封装,但是是可以通过调用JspopularGL来实现的,这样我们的操作就更广一些了。

我们如果想要使用jspopularGL,就需要获得到map对象本身。根据文档,我们可以通过ref来实现:

获取map实例

如果你在业务中需要操作map对象,需要BMapGL.Map实例的话,可以通过组件实例的map属性访问到它。

{this.map = ref.map}} />

但是实测,因为异步加载的原因,有时候会出现undefined或者null问题,所以我们可以通过再封装一层的方式:

export default class MyMap extends React.Component {

constructor(props) {

super(props);

...

this.created = flase;

...

}

_initMap(){

...

this.map = this.mapRef.map;

...

}

componentDidMount() {

if(!this.created) {

this._initMap();

this.created = true;

}

}

render() {

...

return(

{this.mapRef = ref}}>

...

)

}

}

来解决这个问题,这样相当于把ref.map的获取放到确保组件已经生成后,就不会产生空问题。同时也可以把Map抽象出成一个React.Components,在上层直接使用,简单方便。

如果你的上层也需要map对象本身,你可以使用

render() {

const content=

className="map"

ref={(ref) => {this.map = ref}}

/>

}

来获取。

LuShu的引入

为了美观 和狂拽酷炫 ,我们使用了React-BMapGL已经封装好的Arc 2D弧线来展示路径,效果还不错:

(数据是随机mock的,所以地名和坐标都是随机的,不代表任何现实真实含义)每个点的小图标是自行实现的,不展开叙述,后面博文可能会写

完成Arc部分之后,感觉可以加些更酷的元素进去,于是在open | 百度地图API SDK (baidu.com)里面找到了这个东西: 名字叫路书,看起来很不错,是个动画,可以沿着路径走,并且可以附上HTML元素,在飞机上方展示,于是决定引入。

从示例中找到源码:

https://bj.bcebos.com/v1/mapopen/github/BMapGLLib/Lushu/src/Lushu.min.js

首先遇到的第一个问题就是,不知道怎么用。第一次遇到这样的结构:

(function (){

//function body

})()

实际上就是说定义了一个函数并且直接执行之。只需要把它下载下来放到工作文件夹中,重命名一下,然后在我们需要使用的js文件中添加:

import "./Lushu"

就可以了,然后就会发现一堆报错。

实际上报错的原因大概只有

找不到BMapGL.*

针对这类,直接改成window.BMapGL.*就可以了

缺少某个变量

这个文件由于没有直接访问到BMapGL,所以在这些变量使用前定义一下就好,例如:

这两个变量前面加上const或者var就可以了,声明一下。

defaultIcon没有

我们可以手动定义一个,比如像示例里一样用base64编码的图片:

var defaultIcon = "data:image/png;base64,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";

//如果不是默认实例,则使用默认的icon

if (!(this._opts.icon instanceof window.BMapGL.Icon)) {

this._opts.icon = defaultIcon;

}

这么长的这一段就是用base64编码的一个图片,当然也可以找个png转base64编码的工具自己魔改。

目前为止终于能用了。由于一开始比较懒并不想去分析源码,就选择去搜索了现有的关于lushu的使用方法,发现似乎lushu有很多版本。例如上文提到过的版本,和地图JS API示例 | 百度地图开放平台 (baidu.com)这里的大地线路书等等。目前为止功能较为全面的是这里的大地线路书中的版本:

api.map.baidu.com/library/LuShu/gl/src/LuShu_min.js

不知道是什么原因。

这个版本在基础功能之上实现了geodesic,autoCenter等功能,比较实用。

部署使用之后发现问题:lushu的运动轨迹和Arc的轨迹并不重合,于是决定开始自行魔改。

部分源码解读

首先明确目标:让lushu的运动轨迹和Arc的轨迹重合。所以大概思路是

明确Arc是如何绘制的明确lushu是如何运动的修改源码,使得lushu按照Arc的轨迹运动

首先来看Arc源码(node_modules/react-bmapgl/dist/Custom/Arc.js)

重点是if(this.props.data)中的部分。可以看到Arc的轨迹实际上是通过构造OdCurve,再使用OdCurve.getPoints()方法获得轨迹中点的坐标的。

OdCurve

通过传入2个或2个以上的坐标点,来依次生成od曲线坐标集。 该曲线为2D弯曲方式,且不同于大地曲线,大地曲是根据球面最短距离来计算的,距离太近的2个点基本不会弯曲,而这个Od曲线的生成算法不同,即使很短的距离也会弯曲。

OdCurve提供了两个方法:

getPoints

描述:getPoints({number}|{undefined})

解释:获取生成的Od曲线坐标集,传入的字段为曲线的分段数,默认值是20

setOptions

描述:setOptions({Object}options)

解释:修改坐标数组等属性

看到getPoints方法,我直呼牛B,这开发者是知道使用者想要什么的。

接下来再来看看lushu是如何运动的。

首先直接看构造方法:

var LuShu = (BMapGLLib.LuShu = function (map, path, opts) {

if (!path || path.length < 1) {

return;

}

this._map = map;

if (opts["geodesic"]) {

this._path = getGeodesicPath(path);

} else {

this._path = path;

}

this.i = 0;

this._setTimeoutQuene = [];

this._opts = { icon: null, speed: 400, defaultContent: "" };

if (!opts["landmarkPois"]) {

opts["landmarkPois"] = [];

}

this._setOptions(opts);

this._rotation = 0;

if (!(this._opts.icon instanceof BMapGL.Icon)) {

this._opts.icon = defaultIcon;

}

});

大致做了以下几件事:

设置好path,也就是this._path,具体用处在后面。设置了一些字段,比如this.i设置了opts

再来看我们让lushu开始时调用的lushu.start():

LuShu.prototype.start = function () {

var me = this,

len = me._path.length;

if (me.i && me.i < len - 1) {

if (!me._fromPause) {

return;

} else {

if (!me._fromStop) {

me._moveNext(++me.i);

}

}

} else {

me._addMarker();

me._timeoutFlag = setTimeout(function () {

me._addInfoWin();

if (me._opts.defaultContent == "") {

me.hideInfoWindow();

}

me._moveNext(me.i);

}, 400);

}

this._fromPause = false;

this._fromStop = false;

};

判断了一下从什么状态开始start的,我们直接看最后一个else里的内容:

首先把图标(marker)添加进来,然后设置了一个setTimeout,具体内容是把infowindow添加进来,然后执行了me._moveNext(me.i),这个函数就是所有的关键点了。我们先明确me.i是怎么来的,事实上它就是this.i,也就是在构造函数中设置的一个字段,我们进到_moveNext中来看其含义:

_moveNext: function (index) {

var me = this;

if (index < this._path.length - 1) {

me._move(me._path[index], me._path[index + 1], me._tween.linear);

}

},

到这里就很明显了,在构造函数中构造的path存放的是各个点(事实上,它是一个[{lng, lat}]类型的数组),而i则是用来标注当前已经走到第几个点。我们进到_move中去看到底是如何运动的。

_move: function (initPos, targetPos, effect) {

var me = this,

currentCount = 0,

timer = 10,

step = this._opts.speed / (1000 / timer),

init_pos = BMapGL.Projection.convertLL2MC(initPos),

target_pos = BMapGL.Projection.convertLL2MC(targetPos);

init_pos = new BMapGL.Pixel(init_pos.lng, init_pos.lat);

target_pos = new BMapGL.Pixel(target_pos.lng, target_pos.lat);

var mcDis = me._getDistance(init_pos, target_pos);

var direction = null;

if (mcDis > 30037726) {

if (target_pos.x < init_pos.x) {

target_pos.x += WORLD_SIZE_MC;

direction = "right";

} else {

target_pos.x -= WORLD_SIZE_MC;

direction = "left";

}

}

var count = Math.round(me._getDistance(init_pos, target_pos) / step);

if (count < 1) {

me._moveNext(++me.i);

return;

}

me._intervalFlag = setInterval(function () {

if (currentCount >= count) {

clearInterval(me._intervalFlag);

if (me.i > me._path.length) {

return;

}

me._moveNext(++me.i);

} else {

currentCount++;

var x = effect(init_pos.x, target_pos.x, currentCount, count),

y = effect(init_pos.y, target_pos.y, currentCount, count),

pos = BMapGL.Projection.convertMC2LL(new BMapGL.Point(x, y));

if (pos.lng > 180) {

pos.lng = pos.lng - 360;

}

if (pos.lng < -180) {

pos.lng = pos.lng + 360;

}

if (currentCount == 1) {

var proPos = null;

if (me.i - 1 >= 0) {

proPos = me._path[me.i - 1];

}

if (me._opts.enableRotation == true) {

me.setRotation(proPos, initPos, targetPos, direction);

}

if (me._opts.autoView) {

if (!me._map.getBounds().containsPoint(pos)) {

me._map.setCenter(pos);

}

}

}

if (me._opts.autoCenter) {

me._map.setCenter(pos, { noAnimation: true });

}

me._marker.setPosition(pos);

me._setInfoWin(pos);

}

}, timer);

},

_move的源码比较长,其实总共只分为两段:

设置一些变量,比如:

currentCounttimerstepcount 然后通过计算获得一些值,比如pos相关的部分。

设置运动,也就是setInterval里面的部分。

设置的这些变量似乎有些让人摸不着头脑,我们来分析一下。首先突破口是timer这个量,因为它在setInterval中被直接用到了,含义比较明确:代表着每次执行的间隔时间。那么这段代码的终止条件是什么呢,我们来看:

if (currentCount >= count) {

clearInterval(me._intervalFlag);

if (me.i > me._path.length) {

return;

}

me._moveNext(++me.i);

} else {

currentCount++;

...

}

可以看到,事实上是每次执行都会使currentCount自增,直到等于count,我们再回过头来看count的定义:

step = this._opts.speed / (1000 / timer),

var count = Math.round(me._getDistance(init_pos, target_pos) / step);

我们来列式子算一下:

step

=

speed

1000

×

timer

\text{step} = \frac{\text{speed}}{1000}\times \text{timer}

step=1000speed​×timer 由于timer是常量,我们可以写成:

step

speed

\text{step} \propto \text{speed}

step∝speed 那么

count

=

distance

step

\text{count}=\frac{\text{distance}}{\text{step}}

count=stepdistance​ 这是什么,这可不就是

t

=

s

v

t=\frac{s}{v}

t=vs​ 嘛,所以说可以简单理解为:

count表示完成这段运动一共需要多少"帧";currentCount表示现在运动到第几"帧"了;timer表示运动一帧所需要的时间(ms);step只是一个中间量;

继续往下分析:

} else {

currentCount++;

var x = effect(init_pos.x, target_pos.x, currentCount, count),

y = effect(init_pos.y, target_pos.y, currentCount, count),

pos = BMapGL.Projection.convertMC2LL(new BMapGL.Point(x, y));

...

me._marker.setPosition(pos);

me._setInfoWin(pos);

}

}, timer);

},

中间省略的是与运动直接关系不太大的(关于rotation后面会讲)部分,可以看到其实和我们猜测的一样,每次执行都通过effect函数来获得下一帧的坐标,然后调用setPosition()来修改位置,这样就可以做出动效来了。

有了这些之后,我们简单的思路就是:既然Arc使用了OdCurve,我们只需要在lushu中也得到同样的点路径,然后通过修改effect方法来在每帧中获取对应的点坐标即可。

1. 构造OdCurve

首先引入mapvgl:

var mapvgl_1 = require("mapvgl");

在lushu的构造方法中完全仿照Arc构造一个点列出来即可:

...

const MAX_FRAME = 300;

if (opts["geodesic"]) {

this._path = getGeodesicPath(path);

}

else if (opts["odCurve"]){ // 是否使用Odcurve

var lineData = [];

var curve = new mapvgl_1.OdCurve();

for(var i = 0 ; i < path.length-1 ; i++){

var start = path[i];

var end = path[i+1];

curve.setOptions({

points: [start, end]

});

var curveModelData = curve.getPoints(MAX_FRAME-1); //最细

lineData.push(curveModelData)

}

this.lineData = lineData;

this._path = path;

}

else {

this._path = path;

}

...

这里有个小trick:我使用了MAX_FRAME-1作为getPoints的参数,来获取到长度为MAX_FRAME的点列,这么做是因为我想要让lushu在每段Arc的运动时长相等,而不是速度相等,避免非常短和非常长的Arc在同一个展示中,导致lushu非常鸡肋。

因为lushu是通过currentCount来控制的,如果要每段都定时的话其实非常简单,只需要固定下来count就可以了。这样的话如果我需要更改这个时长,也可以通过设置count来实现。

而MAX_FRAME其实是为了方便其它时长的情况方便地获取到点列,直接通过(0, MAX_FRAME)到(0, count)的映射就可以获得到对应的点,而不需要每针对一个count就重新获取一个点列。

2. 修改effect

事实上,effect函数是一个回调函数,它在_moveNext中被传入:

_moveNext: function (index) {

var me = this;

if (index < this._path.length - 1) {

me._move(me._path[index], me._path[index + 1], me._tween.linear);

// me._tween.linear

}

},

我们找到linear:

_tween: {

linear: function (initPos, targetPos, currentCount, count) {

var b = initPos;

var c = targetPos - initPos;

var t = currentCount;

var d = count;

return (c * t) / d + b;

}

},

emmmm,也许当时开发的时候是想过拓展多种方式的,只是没实现留了个接口而已。正好我们也用得上,只是需要魔改一下:

_tween: {

linear: function (initPos, targetPos, currentCount, count) {

...

},

OdCurve: function (currentCount, count, lineData, i) {

var lineDataArrayIndex = Math.round(MAX_FRAME*currentCount/count)

return lineData[i][lineDataArrayIndex>=MAX_FRAME?MAX_FRAME-1:lineDataArrayIndex]

}

},

linear我这里就直接弃用了,所以参数也重新写了,这些参数的含义都已经解释过了,函数体本身也只做了一件非常简单的事:求出对应的映射点,然后把该点直接返回。

当然在effect的调用处我们也需要小改一下(_move的setInterval里):

} else {

currentCount++; // 下一帧

// var x = effect(init_pos.x, target_pos.x, currentCount, me.speed, "x"),

// y = effect(init_pos.y, target_pos.y, currentCount, me.speed, "y"),

var nextPoint = effect(currentCount, me.speed, me.lineData, i);

var x = nextPoint[0],

y = nextPoint[1],

pos = window.BMapGL.Projection.convertMC2LL(new window.BMapGL.Point(x, y));

...

就完成了。此时会发现,确实按照轨迹运动了,但是旋转非常鬼畜,令人匪夷所思。我们再来看关于Rotation的部分:

if (me._opts.enableRotation == true) {

me.setRotation(proPos, initPos, targetPos, direction);

}

这里给setRotation传进去了四个参数,意义比较明确(proPos应该是prePos打错了,但是问题也不大,反正都是proPos不影响运行,而且实际上这个变量都没被用过,也不知道什么原因),我们直接来看setRotation:

setRotation: function (prePos, curPos, targetPos, direction) {

var me = this;

var deg = 0;

curPos = me._map.pointToPixel(curPos);

targetPos = me._map.pointToPixel(targetPos);

if (targetPos.x != curPos.x) {

var tan = (targetPos.y - curPos.y) / (targetPos.x - curPos.x),

atan = Math.atan(tan);

deg = (atan * 360) / (2 * Math.PI);

if ((!direction && targetPos.x < curPos.x) || direction === "left") {

deg = -deg + 90 + 90;

} else {

deg = -deg;

}

me._marker.setRotation(-deg);

} else {

var disy = targetPos.y - curPos.y;

var bias = 0;

if (disy > 0) {

bias = -1;

} else {

bias = 1;

}

me._marker.setRotation(-bias * 90);

}

return;

},

其实也很好理解,看到tan和atan大概就明白是直接把方向改成两个点的连线方向。但是为甚么我们使用会出问题呢,原因是传入的是这段线的起始点和终点两个点,而不是我们魔改过后的路程点列中的每个点,所以只需要在_move开始的时候设置一个新的字段currentPos:

_move: function (initPos, targetPos, effect, i) {

var me = this,

currentCount = 0,

currentPos = initPos,

...

然后再把传入的参数改成

if (me._opts.enableRotation == true) {

me.setRotation(prePos, currentPos, pos, direction);

currentPos = pos;

}

就可以了。

By JSYRD

相关推荐