Advertisement

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… 😉

https://juejin.cn/post/7427141349483757595

全部评论 (0)

还没有任何评论哟~