2024年最全干货!如何在高德地图实现自动巡航效果?,前端事件分发机制收藏这一篇就够了

最后

小编综合了阿里的面试题做了一份前端面试题PDF文档,里面有面试题的详细解析

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

虽只说了一个公司的面试,但我们可以知道大厂关注的东西并举一反三,通过一个知识点延伸到另一个知识点,这是我们要掌握的学习方法,小伙伴们在这篇有学到的请评论点赞转发告诉小编哦,谢谢大家的支持!

1.获取数据,生成移动路径,并绘制巡航轨迹;

269a690484f40f97ffaba7e548e927aa.gif

2.加载、放置、调整模型;

6e66963f28647714c27730adab3de4c2.png

3.移动模型,在每一帧重新设置模型的位置、正面朝向;

af18545cabcde827180ecbff81ff14ee.gif

4.随着模型移动,更新镜头的位置、以及镜头看向的位置;

c4bfb340d3bcf843a4e03d999f6c82fe.gif

5.最后让NPC图层和3DTiles图层尽量融合。

395e83ed7fa1adbde34adb05382fe082.gif

技术点分析

主体沿轨迹移动

threejs实现沿着轨迹移动有两种做法,方法一是预先计算好整条路线里,每个的关键节点之间的中间插值点(如图中绿点所示),得到一系列的坐标之后,只需要在每一帧将主体移动到插值点的位置就可以了,这个方法需要插值点足够密集,否则最终效果会不够平滑,车子会像电子跃迁一样跳着走。

5fc08af2d6101718b1cd35196759c767.png

另一个方法就是动态计算,我们每次只考虑两个关键点之间的时间与位置关系,即输入起点A、终点B、移动的总时间、移动的速度曲线(匀速、加速、缓动缓停等等),然后就可以根据当前时刻在总时长的进度获得NPC应该在的位置。

1069c91625937a9b3fbdb49f9814c1d1.gif

镜头跟随

为了实现物体的运动和镜头跟随,我尝试了高德数据可视化API提供的ViewControl镜头动画,以及基于threejs的移动方案,最终选择了threejs和高德API结合的方案,以下是技术方案的对比:

426fdf537d86473c3888bc00eb7d7adf.png

两条路线的焊接

本文演示页面的路径数据使用高德,拖拽导航插件AMap.DragRoute。通过鼠标拖拽已有导航路径上的任一点,可以实现导航起点、途经点、终点的调整,系统根据调整后的起点、途经点、终点信息,实时查询拖拽后的导航路径。然而发现返回的路径数据里,并不包括两段路线之间的连线,这种情况通常出现在红绿灯、十字路口处,需要自己处理。为避免干扰文章主题,这里只要知道有这个情况就行,路线焊接的具体编码以后再讲。

6d8524d841be6b491f3eebab4a103b7a.png

直接用线段连接LineA的终点和LineB的起点,会导致物体移动朝向异常,镜头转动也比较突兀,因此当两个端点距离大于某个阈值时,需要提供一个方法可以自动“焊接”两条线段。平滑焊接路线的目的是为了让物体移动更加平滑,我们可以选择预先处理,或者实时处理,视情况而定。

这里会遇到几种情况,我们分别处理:

LineA和LineB延长线必定相交,需要生成一条平滑的贝塞尔曲线连接这两个端点;

c90094b06dabbfa81407c558a22f6a01.gif

LineA和LineB处于同一条直线,只需要将两个端点连接起来即可;

c6101f88681e965f32d9cb3a5ae0966b.png

LineA和LineB平行,则需要生成半个圆角矩形的边线将端点连接起来。

a7f8c7c9dfe29a5afe4a4403f7f2de25.png

代码实现

1.获取数据,生成移动路径,并绘制巡航轨迹;

//最终路径数据
const PATH_DATA = {features: []} 


var path = [];
path.push([113.532592,22.788502]); //起点
path.push([113.532592,22.788502]); //经过
path.push([113.532553, 22.788321]); //终点


map.plugin("AMap.DragRoute", function() {
    //构造拖拽导航类
    route = new AMap.DragRoute(map, path, AMap.DrivingPolicy.LEAST_FEE); 
    //查询导航路径并开启拖拽导航
    route.search(); 
    route.on('complete',function({type,target, data}){
      // 获得路径数据后,处理成GeoJSON
      const res =  data.routes[0].steps.map(v=>{
            var arr = v.path.map(o=>{
                return [o.lng, o.lat]
            })
            return  { 
              "type": "Feature",
               "geometry": {
                    "type": "MultiLineString",
                    "coordinates": [arr]
                 },
                 "properties": {
                    "instruction": v.instruction,
                    "distance": v.distance,
                    "duration": v.duration,
                    "road": v.road
                  }
             }
        })
        PATH_DATA.features = res
    })
});


// 使用数据绘制流光的轨迹线
// 这个图层的作用是便于调试运动轨迹是否吻合
const layer = new FlowlineLayer({
  map: getMap(),
  zooms: [4, 22],
  data: PATH_DATA,
  speed: 0.4,
  lineWidth: 2,
  altitude: 0.5
})

2.将GeoJSON数据合并成一整条路线数据,并预处理好数据;

// 合并后的路径数据(空间坐标)
var _PATH_COORDS = []
// 合并后的路径数据(地理坐标)
var _PATH_LNG_LAT = []


//处理转换图层基础数据的地理坐标为空间坐标,保留z轴数据
initData (geoJSON) {
    const { features } = geoJSON
    this._data = JSON.parse(JSON.stringify(features))


    this._data.forEach((feature, index) => {
      const { geometry } = feature
      const { type, coordinates } = geometry


      if (type === 'MultiLineString') {
        feature.geometry.coordinates = coordinates.map(sub => {
          return this.handleOnePath(sub)
        })
      }
      if (type === 'LineString') {
        feature.geometry.coordinates = this.handleOnePath(coordinates)
      }
    })
}
/**
   * 处理单条路径数据
   * @param {Array} path 地理坐标数据 [[x,y,z]...]
   * @returns {Array} 空间坐标数据 [[x',y',z']...]
   */
  handleOnePath (path) {
    const { _PATH_LNG_LAT, _PATH_COORDS, _NPC_ALTITUDE } = this
    const len = _PATH_COORDS.length
    const arr = path.map(v => {
      return [v[0], v[1], v[2] || this._NPC_ALTITUDE]
    })


    // 如果与前线段有重复点,则去除重复坐标点
    if (len > 0) {
      const { x, y, z } = _PATH_LNG_LAT[len - 1]
      if (JSON.stringify([x, y, z]) === JSON.stringify(arr[0])) {
        arr.shift()
      }
    }


    // 合并地理坐标
    _PATH_LNG_LAT.push(...arr.map(v => new THREE.Vector3().fromArray(v)))


    // 转换空间坐标
    // customCoords.lngLatsToCoords会丢失z轴数据,需要重新赋值
    const xyArr = this.customCoords.lngLatsToCoords(arr).map((v, i) => {
      return [v[0], v[1], arr[i][2] || _NPC_ALTITUDE]
    })
    // 合并空间坐标
    _PATH_COORDS.push(...xyArr.map(v => new THREE.Vector3().fromArray(v)))
    // 返回空间坐标
    return arr
  }

3.加载、放置、调整模型;

// 加载主体NPC
function getModel (scene) {
  return new Promise((resolve) => {
    const loader = new GLTFLoader()
    loader.load('./static/gltf/car/car1.gltf', function (gltf) {
      const model = gltf.scene.children[0]


      // 调试代码
      // const axesHelper = new THREE.AxesHelper(50)
      // model.add(axesHelper)


      // 调整模型大小
      const size = 1.0
      model.scale.set(size, size, size)


      resolve(model)
    })
  })
}


// 初始化主体NPC的状态
initNPC () {
  const { _PATH_COORDS, scene } = this
  const { NPC } = this._conf


  // z轴朝上
  NPC.up.set(0, 0, 1)


  // 初始位置和朝向
  if (_PATH_COORDS.length > 1) {
    NPC.position.copy(_PATH_COORDS[0])
    NPC.lookAt(_PATH_COORDS[1])
  }


  // 添加到场景中
  scene.add(NPC)
}

4.重点来了!移动模型,并更新NPC的位置和朝向、更新镜头的位置和朝向,这里使用了TWEEN做移动状态的控制器,它控制的是一整条路线(A-B-C-D…)里两个关键点(A和B)连线的移动状态,当连线AB的移动结束后,立即开启下一个连线BC,以此类推。我们简单过一下实现逻辑。

initController () {
  // 状态记录器
  const target = { t: 0 } 
  // 获取第一段线段的移动时长,具体实现就是两个坐标点的距离除以速度参数speed
  const duration = this.getMoveDuration()
  // 路线数据 这里用了两组空间坐标和地理坐标两组数据
  // 目的是为了省掉中间坐标转换花费的时间
  const { _PATH_COORDS, _PATH_LNG_LAT, map } = this


  this._rayController = new TWEEN.Tween(target)
    .to({ t: 1 }, duration)
    .easing(TWEEN.Easing.Linear.None)
    .onUpdate(() => {
        //todo: 处理当前连线当前时刻,NPC的位置


        //通过状态值t, 计算NPC应该在的位置
        const position = new THREE.Vector3().copy(point).lerp(nextPoint, target.t)


        //todo: 处理地图中心位置,地图镜头朝向
     })
    .onStart(()=>{
       // todo: 处理NPC的朝向,每次开启路线都会执行
    })
    .onComplete(()=>{
      // todo: 停止当前路线、开启下一段路线
      this._rayController
          .stop()
          .to({ t: 1 }, duration)
          .start()
    })
}

5.随着模型移动,更新镜头的位置、以及镜头朝向的位置;

(1)更新镜头位置与更新NPC位置思路一样,不同的就是使用了地理坐标去计算中间插值,以方便直接调用高德的map.panTo(), 用map.setCenter()也是一样的。

// 计算两个lngLat端点的中间值
const pointLngLat = new THREE.Vector3().copy(_PATH_LNG_LAT[this.npc_step])
const nextPointLngLat = new THREE.Vector3().copy(_PATH_LNG_LAT[nextIndex])
const positionLngLat = new THREE.Vector3().copy(pointLngLat).lerp(nextPointLngLat, target.t)
// 更新地图镜头位置
this.updateMapCenter(positionLngLat)


// 更新地图中心到指定位置
updateMapCenter (positionLngLat) {
   // duration = 0 防止画面抖动
   this.map.panTo([positionLngLat.x, positionLngLat.y], 0)
}

(2)更新镜头朝向,朝向其实就是矢量方向,两个点确定矢量,在这里取NPC当前坐标和后面第四个关键点的坐标确定朝向,也可以根据实际情况而定。

//计算偏转角度
const angle = this.getAngle(position, _PATH_COORDS[(this.npc_step + 3) % _PATH_COORDS.length]
this.updateMapRotation(angle)


//更新地图旋转角度,正北为0度


### 最后

小编综合了阿里的面试题做了一份前端面试题PDF文档,里面有面试题的详细解析

**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**

![](https://img-blog.csdnimg.cn/img_convert/45f2869b91b538dd3bb3290ba13bc806.png)  

![](https://img-blog.csdnimg.cn/20210419193354991.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0tlcGxlcl9JSQ==,size_16,color_FFFFFF,t_70)

虽只说了一个公司的面试,但我们可以知道大厂关注的东西并举一反三,通过一个知识点延伸到另一个知识点,这是我们要掌握的学习方法,小伙伴们在这篇有学到的请评论点赞转发告诉小编哦,谢谢大家的支持!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值