JS 手撸高德地图导航

前言

由于实时导航功能需要复杂的逻辑和很高的性能要求,再加上PC端没有导航的需求,或者根本就没有GPS模块。

高德并没有像移动端SDK那样直接提供 web 端的实时导航 API。但是前端开发中不可避免的需要用到导航相关的功能,然而我们目前只有一个可怜的路线规划功能。其实想要实现web端实时导航也不是一件不可能的事儿,接下来我将使用高德 路线规划+浏览器高精度定位 实现一个基本可用的导航逻辑。

bash 复制代码
代码已开源在github : <https://github.com/LarryZhu-dev/amap_nav>

起终点设置和路线规划

地名搜索

使用 element-plus 的自动补全输入框的远程搜索功能,事件连接到高德的地名搜索

ini 复制代码
      <div class="searchBox" v-if="!currStep">
        <div class="inputs">
          <ElAutocomplete placeholder="请输入起点" v-model="points[0].keyword" size="large" @select="handleSelectStart"
            :fetch-suggestions="querySearchAsync">
            <template #prefix>
              <ElIcon color="#00b144" size="24">
                <LocationFilled />
              </ElIcon>
            </template>
          </ElAutocomplete>
          <ElAutocomplete placeholder="请输入终点" v-model="points[1].keyword" size="large" @select="handleSelectEnd"
            :fetch-suggestions="querySearchAsync">
            <template #prefix>
              <ElIcon color="#d32f19" size="24">
                <LocationFilled />
              </ElIcon>
            </template>
          </ElAutocomplete>
        </div>
      </div>

querySearchAsync 函数内实现搜索逻辑,使用callback返回即可。

php 复制代码
function querySearchAsync(queryString: string, cb: (arg: any) => void) {
  const placeSearch = new AMap.PlaceSearch({
    city: '北京',
    pageSize: 5,
    pageIndex: 1,
    citylimit: true,
    extensions: 'all',
  });
  placeSearch.search(queryString, function (status: any, result: any) {
    if (status === 'complete') {
      const res = result.poiList.pois.map((item: any) => {
        return {
          value: item.name,
          label: item.name,
          raw: item
        }
      })
      cb(res)
    }
  });
}

PlaceSearch : lbs.amap.com/api/javascr...

可以设置 citylimit 和 city 字段 以开关、切换城市范围。pageSize控制返回条数。

handleSelectStart 和 handleSelectEnd 函数 则检查是否起点、终点都有了,这样可以单独修改其中一个。修改后立即查询路线。

scss 复制代码
​
function handleSelectStart(item: any) {
  points.value[0].keyword = item.raw.name
  points.value[0].lnglat = [item.raw.location.lng, item.raw.location.lat]
  getRoute()
}
function handleSelectEnd(item: any) {
  points.value[1].keyword = item.raw.name
  points.value[1].lnglat = [item.raw.location.lng, item.raw.location.lat]
  getRoute()
}
​

路线规划

在路线规划函数内,检查 points 的两个位置是不是都有了,否则就不执行。对应我们上面的逻辑,起点和终点可以单独修改,修改后立即查询。

ini 复制代码
function getRoute() {
  if (!points.value[0].lnglat.length || !points.value[1].lnglat.length) {
    return
  }
  loading.value = true
  driving.search(points.value[0].lnglat, points.value[1].lnglat, function (status: any, result: any) {
    if (status === 'complete') {
      console.log(result.routes[0])
      currentRoute.value = {
        distance: result.routes[0].distance,
        duration: result.routes[0].time,
        policy: result.routes[0].policy,
        steps: result.routes[0].steps.map((item: any, index: number) => {
          return {
            instruction: item.instruction,
            distance: meters2kilometers(item.distance),
            action: item.action,
            icon: actionIconDict[item.action],
            startPoint: [item.start_location.lng, item.start_location.lat],
            endPoint: [item.end_location.lng, item.end_location.lat],
            time: item.time,
            index: index
          }
        })
      }
    } else {
      console.log('获取驾车数据失败:' + result)
      currentRoute.value = {}
    }
    loading.value = false
  });
}

driving.search 有两种搜索方式,关键字搜索和经纬度搜索,格式分别如下

arduino 复制代码
const points = [
  { keyword: '北京市地震局(公交站)',city:'北京' }, //起始点坐标
  { keyword: '亦庄文化园(地铁站)',city:'北京' } //终点坐标
]
arduino 复制代码
const startLngLat = [116.379028, 39.865042] //起始点坐标
const endLngLat = [116.427281, 39.903719] //终点坐标

使用经纬度比较准确,我个人比较推荐。

在👆这段代码中,我将获取到的路线信息组装了一个 易操作的 对象 steps,包含以下参数

  • distance:这段路线的长度
  • instruction:这一段路的行为描述
  • action:这一段结束后的行为(字典将在下文提到)
  • icon:根据行为字典确定方向 icon ,下文将提到
  • startPoint、endPoint :这一段路的第一个点和最后一个点,其实这一段路可能有很多点,但是这里为了方便演示,简化成了两个点
  • time:这一段的路程时间
  • index:这一段的下标值,看似没什么用,但是却是我们接下来配合 turf.js 找到当前 step 的关键。

currentRoute 中有以下对象

  • distance:路线总长度
  • duration:总时间
  • policy:路线方案推荐理由,如速度最快
  • steps:即上面的组装对象

手撸导航

到了本文的重头戏,我们要用 turf.js + 高德的路线规划 亲手把核心功能:导航!给做出来。

由于我目前的开发电脑就没有GPS模块😅,所以我们要先实现一个模拟位置方法,便于在PC端开发测试。

浏览器精确定位

首先说明,高德地图提供了浏览器精确定位 api AMap.Geolocation,如果你的设备有GPS模块,那么完全可以使用真实位置,当然,在本项目的开源代码中,包含了真实位置的代码,如果需要使用真实位置,将下面提到的代码中的 mock变量改为false即可。

初始化地图是,需要将位置以插件的形式加载进来:

javascript 复制代码
 AMap = await AMapLoader.load({
    key: import.meta.env.VITE_AMAP_KEY, // 申请好的Web端开发者Key,首次调用 load 时必填
    version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
    plugins: ["AMap.GeoJSON", "AMap.PlaceSearch", "AMap.Driving", "AMap.Geolocation", "AMap.Marker"], //需要使用的的插件列表,如比例尺'AMap.Scale',支持添加多个如:['...','...']
    Loca: {                // 是否加载 Loca, 缺省不加载
      "version": '2.0.0'  // Loca 版本,缺省 1.3.2
    },
  })

"AMap.Geolocation" 就是我们需要用的,当然,可以看到包括PlaceSearch、Driving 都是依靠这种方式加载的。

初始化定位插件:

php 复制代码
  location = new AMap.Geolocation({
    enableHighAccuracy: true,
    timeout: 10000,
  });

文档参考:lbs.amap.com/api/javascr...

enableHighAccuracy打开时将会尝试浏览器默认的高精度定位(谷歌默认的一般用不了)

模拟位置

如果你像我一样,电脑没有GPS模块,又想体验一下模拟导航,建议像我一样,根据路线信息,写一个简单的模拟位置。

typescript 复制代码
const mock = true
const mockSpeed = 1000
let mockTimer: any
function getCurrentLocation(cb: (arg: number[]) => void) {
  if (mock) {
    clearInterval(mockTimer)
    mockTimer = setInterval(() => {
      let currentTimestamp = new Date().getTime()
      let runTime = (currentTimestamp - startTimestamp.value) / 1000
      for (let item of currentRoute.value.steps!) {
        if (runTime < item.time) {
          let distance = pointToPointDistance(item.startPoint, item.endPoint)
          let speed = (distance / item.time) * mockSpeed
          let currentDistance = speed * runTime
          try {
            let currentLocation = along({
              type: 'LineString',
              coordinates: [item.startPoint, item.endPoint]
            }, currentDistance, { units: 'meters' })
            let position = nearestPointOnLine({
              type: 'Feature',
              properties: {},
              geometry: {
                type: 'LineString',
                coordinates: [item.startPoint, item.endPoint]
              }
            }, currentLocation.geometry.coordinates)
            cb(position.geometry.coordinates)
          } catch (e) {
            console.log('e::: ', e);
            continue
          }
          break
        }
      }
    }, 100)
    return
  }
  location.getCurrentPosition(function (status: string, result: any) {
    if (status == 'complete') {
      console.log('result::: ', result);
    } else {
      console.log('result::: ', result);
    }
  });
}

这个函数同样兼容了真实定位,正如上面提到的,你只需要把 mock 改为false即可。

下面讲解一下它是怎么模拟位置的

首先,它需要传入一个 callback,这个callback将在setInterval中,流式的返回位置,就像真实的变化一样。

大致的逻辑是:在开始导航(startNav函数,下面会提到)时,记录开始导航的时间,根据路线长度、已走的时间来判断路线上的长度,再生成一个新点,这个新点就是我们要的位置。要注意的是,这个方法虽然可以在路线上正确的行进,但是无法模拟偏航的情况。

这里有张图帮助大家理解

ini 复制代码
let runTime = (currentTimestamp - startTimestamp.value) / 1000

算出已走的时间

还记得上面我们在 steps 对象里记录了每一步走到的预估时间吧,在这里,我们遍历 steps,找出我们所在的 step item,注意,这里找到的步不能直接作为真实的动作提醒,因为这是我们用走过的时间和平均速度取的。

使用 turf.pointToPointDistance 算出此段路起点到终点的长度👇

ini 复制代码
let distance = pointToPointDistance(item.startPoint, item.endPoint)

算出平均速度

ini 复制代码
let speed = (distance / item.time) * mockSpeed

算出我们当前应该走到的路程

ini 复制代码
let currentDistance = speed * runTime

尝试计算走过的路程在线上的终点

php 复制代码
          try {
            let currentLocation = along({
              type: 'LineString',
              coordinates: [item.startPoint, item.endPoint]
            }, currentDistance, { units: 'meters' })
            let position = nearestPointOnLine({
              type: 'Feature',
              properties: {},
              geometry: {
                type: 'LineString',
                coordinates: [item.startPoint, item.endPoint]
              }
            }, currentLocation.geometry.coordinates)
            cb(position.geometry.coordinates)
          } catch (e) {
            console.log('e::: ', e);
            continue
          }

因为测试时发现如果速度设置的过高,可能会超出这一段路,turf就会报错,但是为了看更快的效果,我们有时就是需要把速度设置高一点,毕竟是模拟,这里偷个懒,如果过快超出了这段路,导致报错了,就直接进入下一段就好了🤣,所以在我的demo中,你将看到两个不正常而故意为之的状态:1. 模拟点不在路线上(因为简化掉了中间点,只留了每段的两个点)、2. 点不动了或者跳出去了(这里报错了)

ok,到此我们已经完成了位置模拟,这段代码将源源不断的给我们返回可用的位置点。

导航逻辑实现

上面提到的 startNav 函数是这样实现的:

php 复制代码
function startNav() {
  if (navStarted.value) {
    return
  }
  startTimestamp.value = new Date().getTime()
  navStarted.value = true
​
  // 创建一个 icon
  var endIcon = new AMap.Icon({
    size: new AMap.Size(40, 40),
    image: car,
    imageSize: new AMap.Size(40, 40),
  });
​
  // 将 icon 传入 marker
  var endMarker = new AMap.Marker({
    position: new AMap.LngLat(0, 0),
    icon: endIcon,
    offset: new AMap.Pixel(-13, -30)
  });
​
  // 将 markers 添加到地图
  map.add([endMarker]);
  getCurrentLocation((currLocation: number[]) => {
    endMarker.setPosition(currLocation)
    routeNav(currLocation)
  })
}
​

添加了一个方向标作为定位点。

接下来我们看重头戏:routeNav 函数

scss 复制代码
function routeNav(currLocation: number[]) {
  if (checkIsCloseToEndpoint()) {
    console.log('导航结束')
    navStarted.value = false
    currStep.value = undefined
    return
  }
  currStep.value = getCurrStep(currLocation)
  heading.value = -bearing(currStep.value.startPoint, currStep.value.endPoint)
  map.setCenter(currLocation)
  map.setZoom(16)
  map.setRotation(heading.value)
  map.setPitch(45)
}

首先检查 是否距离终点过近了,如果过近,就直接触发导航结束逻辑

php 复制代码
function checkIsCloseToEndpoint() {
  if (!currStep.value?.endPoint) {
    return false
  }
  const distance = pointToPointDistance({
    type: 'Point',
    coordinates: currStep.value!.endPoint
  }, currentRoute.value.steps![currentRoute.value.steps!.length - 1].endPoint, { units: 'meters' })
  return distance < 10
}

这里使用 turf.pointToPointDistance 点到点的距离来计算当前位置和路线最后一个点的距离,如果小于10m,判定为导航结束。

在正常导航时,我们需要判断当前位置处于哪一段 step,并显示相应的动作提示和 icon

这是 驾车 的 action 字典:

c 复制代码
const actionIconDict: { [key: string]: string } = {
  '左转': 'icon-xiangzuozhuan',
  '右转': 'icon-xiangyouzhuan',
  '直行': 'icon-xiangshang',
  '向左前方行驶': 'icon-xiangzuozhuan',
  "向右前方行驶": "icon-xiangyouzhuan",
  "向左后方行驶": "icon-xiangzuozhuan",
  "向右后方行驶": "icon-xiangyouzhuan",
  "左转调头": "icon-xiangzuozhuan",
  "靠左": "icon-xiangzuozhuan",
  "靠右": "icon-xiangyouzhuan",
  "进入环岛": "icon-xiangshang",
  "离开环岛": "icon-xiangshang",
  "减速行驶": "icon-xiangshang"
}

目前我只按照方向加了 向左、向右、向前 三个图标。

接下来获取当前 step

php 复制代码
function getCurrStep(currLocation: number[]) {
  const paths = currentRoute.value.steps!.map((item) => {
    return item.startPoint
  })
  const nearestPoint = nearestPointOnLine({
    type: 'Feature',
    properties: {},
    geometry: {
      type: 'LineString',
      coordinates: paths
    }
  }, currLocation)
  return currentRoute.value.steps![nearestPoint.properties.index]
}

👆 turf.nearestPointOnLine 是计算点到多线段最短间距的点,返回时会自带一个 index 参数,这个 index 就是指的在原数组的 index 值(上面提到的关键),好像最难的一步我们反而解决的最容易,直接返回 currentRoute.value.steps![nearestPoint.properties.index] 我们就拿到了当前路段。

回到 routeNav 函数中

scss 复制代码
function routeNav(currLocation: number[]) {
  if (checkIsCloseToEndpoint()) {
    console.log('导航结束')
    navStarted.value = false
    currStep.value = undefined
    return
  }
  currStep.value = getCurrStep(currLocation)
  heading.value = -bearing(currStep.value.startPoint, currStep.value.endPoint)
  map.setCenter(currLocation)
  map.setZoom(16)
  map.setRotation(heading.value)
}

此段代码无修改,与上文的routeNav相同

使用 turf.bearing 计算出当前位置与此段路下一个点的方位角,但是由于我们每段路只留了两个点,所以这个方位角目前总是指向这段路的最后一个点,在实际应用时,应该把 steps 的所有点都加进来。

图中红色箭头指向了正确方向,而我们的案例指向了此段路的最后一个点,在实际应用时应该要注意这点。

完成(效果图)

到此为止我们已经实现了导航的基本逻辑,并且已经成功跑起来了!

再贴一遍项目地址:github.com/LarryZhu-de... 😉

相关推荐
大猫会长3 分钟前
tailwindcss中,自定义多个背景渐变色
前端·html
xj7573065339 分钟前
《python web开发 测试驱动方法》
开发语言·前端·python
IT=>小脑虎12 分钟前
2026年 Vue3 零基础小白入门知识点【基础完整版 · 通俗易懂 条理清晰】
前端·vue.js·状态模式
trojan__35 分钟前
arcgispro水文操作失败——修改并行处理因子为0
arcgis·gis·arcgispro
IT_陈寒40 分钟前
Python 3.12性能优化实战:5个让你的代码提速30%的新特性
前端·人工智能·后端
赛博切图仔41 分钟前
「从零到一」我用 Node BFF 手撸一个 Vue3 SSR 项目(附源码)
前端·javascript·vue.js
爱写程序的小高41 分钟前
npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree
前端·npm·node.js
loonggg41 分钟前
竖屏,其实是程序员的一个集体误解
前端·后端·程序员
程序员爱钓鱼1 小时前
Node.js 编程实战:测试与调试 - 单元测试与集成测试
前端·后端·node.js
码界奇点1 小时前
基于Vue.js与Element UI的后台管理系统设计与实现
前端·vue.js·ui·毕业设计·源代码管理