ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

uniapp跨端地图轨迹绘制与回放:坐标定位与抽稀实战

uniapp跨端地图轨迹绘制与回放:坐标定位与抽稀实战 从接手这个项目到现在差不多两周时间基本功能已经稳定跑起来了这里把整个开发过程、踩过的坑和一些关键思路完整记录下来。项目本身不算复杂核心就一件事在uniapp地图上把一个人的移动轨迹实时画出来同时支持轨迹回放。先说一下背景。这个需求来自一个园区内部人员定位系统管理人员需要在后台和手机端同时看到巡逻人员在园区内的移动路线方便调度和追溯。要求适配微信小程序、Android App和H5三个端其中小程序和App是主要的落地场景。技术栈指定是uniapp地图服务当时没有硬性指定给我们留了选型空间。先说结论我最终选择了高德地图作为基础地图服务配合uniapp内置的map组件完成轨迹绘制。为什么这么选、中间踩了哪些坑、具体怎么实现下面逐一展开。1. 需求拆解与技术选型1.1 核心需求其实只有三件事人员轨迹绘制拆开来看本质上就三个核心链路拿定位点、存点、画线。拿定位点是指实时获取人员当前位置的经纬度这个需要用到定位服务存点是指把连续采集到的坐标点按时间顺序串联起来形成一条完整的数据链画线则是在地图上用折线把这一串点渲染出来形成可视化的移动路线。但实际落地时这三个环节每一个都有不少细节要处理。定位点怎么拿才稳定采集频率多高才不会漏点也不会过度消耗电量画线时点太多会不会卡轨迹回放怎么做得流畅这些都是隐藏在工作量背后的难题。除了基础功能还有两个非功能需求也必须重视一是跨端兼容因为要同时跑在小程序和App上两端的定位API、地图组件、权限机制差异非常大二是准确度轨迹不能是跳来跳去的需要有一定的平滑处理。1.2 地图服务选型高德、百度还是腾讯在uniapp里做地图可选的主要有几条路线我整理了一下它们的差别。方案优点缺点适用场景高德地图uni-app官方插件生态好文档全覆盖小程序和App部分功能在H5端表现一般需要跨小程序和App的正式项目百度地图地图数据在某些区域更丰富API稳定uni-app集成相对麻烦插件更新慢已有百度系基础服务的团队腾讯地图微信生态结合好小程序上原生支持独立App端集成成熟度不如高德以微信小程序为主的轻量项目uni内置map组件一套代码多个端都能跑无需单独申请Key功能受限于组件本身定制化难需求简单、无复杂交互的项目我最终选择高德原因很实际uniapp官方有完善的高德地图插件支持微信小程序端可以直接通过map组件绑定高德地图App端也能通过原生SDK拿到高性能的定位和地图渲染能力。最关键的是高德的定位SDK在室外精度表现比较稳定这一点对轨迹绘制是致命的——如果定位点乱跳画出来的轨迹就是一团乱麻。1.3 整体技术方案怎么定的技术方案我分了三层来设计。表现层使用uniapp的map组件配合markers人员位置标记和polyline轨迹线来渲染。地图组件是uniapp内置的各端都有原生兼容不需要自己造轮子。数据层维护一个坐标点数组按时间顺序存入本地storage同时接口上报到服务器。这里有个设计细节为了支持轨迹回放和轨迹重绘我不仅存坐标点还每一条轨迹记录一个时间戳数组这样回放时可以按时间轴走。逻辑层负责定位管理、轨迹抽稀、坐标系转换、轨迹完整性校验等。这一层是核心也是后面要重点展开的部分。注意地图组件在不同端的底层实现并不一致微信小程序用的是腾讯地图的渲染App端用的是高德原生SDKH5端则取决于浏览器环境。这会导致同一套坐标在不同端有微小差异后面我会讲怎么处理。2. 核心原理与数据链路详解2.1 坐标系你拿到的坐标可能不是同一个坐标系做轨迹系统第一个必须搞懂的概念是坐标系。国内地图服务使用的坐标系不是全球通用的WGS-84而是经过加密偏移的GCJ-02俗称火星坐标系。部分地图服务使用的BD-09又是基于GCJ-02再做二次偏移。具体来说手机GPS芯片直接返回的坐标是WGS-84标准而高德地图、腾讯地图内部使用的都是GCJ-02。如果你直接把GPS的原始坐标扔给高德地图去标点点位会偏移几百米甚至更远——在户外空旷地带这个误差可能不明显但在楼宇密集的园区里误差会直接导致轨迹穿墙看起来很离谱。坐标系全称特点WGS-84全球定位系统标准坐标系GPS原始输出国际通用GCJ-02火星坐标系国内地图通用经过加密偏移BD-09百度坐标系在GCJ-02基础上再次偏移处理方式很简单uni.getLocation在微信小程序端返回的默认就是GCJ-02坐标但在App端可以通过type参数指定坐标系类型。为了保证各端统一我在App端定位时显式指定返回GCJ-02坐标小程序端保持默认这样落到地图上就不会出现偏移。如果是后端下发坐标那就必须在接口层约定好坐标系必要的时候写一个WGS-84转GCJ-02的转换函数。这个转换算法是公开的高德开放平台文档里有提供uni端可以直接用。2.2 定位采集机制不是拿到坐标就完事定位采集是整个轨迹系统最源头的一环也是最容易出问题的地方。uniapp里拿定位有几种方式我在这个项目里都试过说下区别。uni.getLocation是单次定位调用一次返回一个坐标点适合临时定位的场景比如用户点击我在哪。但如果要实现持续轨迹记录就得在一个定时器里反复调用或者使用持续定位接口。频繁调用getLocation有两个问题一是定位间隔不好控制二是部分端会频繁拉起定位导致耗电严重。uni.startLocation是持续定位接口可以设置每次定位的时间间隔定位结果会通过回调持续上报。这个接口更适合轨迹采集场景。Apple和Android都支持后台持续定位但权限要求各不相同这一点在App端要特别注意权限配置。我自己实测下来的建议是Android端定位间隔设置到2000-5000毫秒比较合理既能保证轨迹连续又不会对电量造成太大影响。如果是步行巡逻场景2000毫秒采集到的点已经足够密了后面再配合抽稀算法减点即可。还有一个小细节定位结果里有个accuracy字段代表着这次的定位精度。实测中发现刚进入室内或者信号不佳的区域时精度值会突然变大有时候从10米直接飙到100米。这时候如果还继续把坐标点写入轨迹就会出现漂移点。我的做法是丢弃精度大于50米的点位同时保留最近一次有效点等精度恢复后再继续连线。2.3 轨迹抽稀点太多不是好事轨迹采集久了之后坐标点会积累得非常多。假设2秒采集一个点一个人巡逻1个小时就是1800个点。如果直接用这1800个点在地图上渲染折线性能压力会成倍增加尤其在低端Android机上会出现明显的卡顿和掉帧。这里就需要抽稀算法。最常用的是道格拉斯-普克算法Douglas-Peucker原理是用一条直线连接轨迹的首尾点计算中间每个点到这条直线的垂直距离如果最大距离小于阈值就认为该点可以忽略反之则保留那个点并递归处理。抽稀阈值一般设置在5到10米既能保持轨迹形状又能大幅减少点的数量。我在项目里用的就是这么干的。抽稀算法放在每次轨迹停止后执行实时记录时还是全量保存原始点停止的时候统一做一次清洗把清洗后的精简轨迹上报后端和用于回放。提示实时画线的时候不要每次都从头到尾渲染整条折线性能会很差。我采用增量式更新地图只画最近一段新轨迹整体轨迹在轨迹停止或回放时才完整渲染。3. 实操落地从零实现轨迹绘制3.1 工程初始化和地图SDK接入先讲一下工程怎么搭。我用的HBuilderX创建了uniapp项目Vue3版本。因为要跨微信小程序、Android和H5三个端我直接选用了vue3模板没有用Vite版本主要是为了HBuilderX的云打包链路更顺避免一些工程配置上的坑。地图接入这里要分两步走微信小程序端需要在小程序公众平台后台配置高德地图的域名白名单同时在高德开放平台申请Web服务API KeyApp端需要在高德开放平台申请Android平台的Key并且把包名、签名等信息配对。这里有一个非常容易踩的坑高德的Android Key校验是绑死包名签名的如果你云打包和正式打包用不同的签名就必须分别申请对应的Key否则地图就是白屏连报错都没有。H5端相对简单直接在代码里配置Key就行。但H5端如果部署的域名不止一个需要在高德后台把多个域名都加入白名单否则线上环境请求会被拦截。3.2 实时定位与轨迹点采集的核心代码定位采集的核心逻辑我封装成了一个组合式函数这里把关键代码贴出来。// useTrackLocation.js import { ref } from vue export function useTrackLocation() { const tracking ref(false) const currentPoint ref(null) const trackPoints ref([]) function startTrack() { // #ifdef MP-WEIXIN uni.startLocation({ type: gcj02, interval: 2000, success: () { tracking.value true } }) // #endif // #ifdef APP-PLUS uni.startLocation({ coordsType: gcj02, type: gcj02, interval: 2000, accuracy: high, success: () { tracking.value true } }) // #endif // #ifdef H5 const watchId navigator.geolocation.watchPosition( (pos) { const { latitude, longitude, accuracy } pos.coords handleNewPoint({ latitude, longitude, accuracy }) }, (err) console.error(定位失败, err), { enableHighAccuracy: true, maximumAge: 1000, timeout: 5000 } ) // #endif uni.onLocationChange((res) { const point { latitude: res.latitude, longitude: res.longitude, accuracy: res.accuracy || 0, timestamp: Date.now() } handleNewPoint(point) }) } function handleNewPoint(point) { if (point.accuracy point.accuracy 60) { return } currentPoint.value point trackPoints.value.push(point) } function stopTrack() { uni.stopLocation() tracking.value false } return { tracking, currentPoint, trackPoints, startTrack, stopTrack } }这段代码里有几个值得注意的地方。// #ifdef MP-WEIXIN、// #ifdef APP-PLUS、// #ifdef H5是uniapp的条件编译语法每个端编译时只保留对应的代码这是跨端开发的核心手段。小程序和App的定位API虽然都叫startLocation但参数不完全统一所以必须分开写。onLocationChange这个监听器在不同端的生命周期表现不太一样。在App端只要startLocation成功这个监听就会持续回调在微信小程序端还需要配合后台配置的定位权限说明才能在退到后台时继续定位。还有一点我丢弃了精度大于60米的点。这个阈值是我在实际测试中调出来的园区场景下室内外混合60米以下的口径能过滤掉大部分漂移点又不会太激进地砍掉有效点。3.3 折线绘制与动态更新实现轨迹线用map组件的polyline属性渲染。uniapp的polyline接收一个坐标点数组底层会帮我们画一条连续的折线。template map idtrackMap :latitudemapCenter.latitude :longitudemapCenter.longitude :markersmarkers :polylinepolyline :scale16 show-location regionchangeonRegionChange /map /template script setup import { computed, ref } from vue const trackPoints ref([]) const currentPostion ref(null) // 增量折线只展示最近50个点避免重复渲染 const displayPolyline computed(() { if (trackPoints.value.length 0) return [] const tailPoints trackPoints.value.slice(-50) return [{ points: tailPoints, color: #FF6600, width: 4, arrowLine: true, borderColor: #FFFFFF, borderWidth: 1 }] }) const markers computed(() { if (!currentPostion.value) return [] return [{ id: 1, latitude: currentPostion.value.latitude, longitude: currentPostion.value.longitude, iconPath: /static/location.png, width: 32, height: 32, callout: { content: 当前位置, display: BYCLICK, borderRadius: 6, padding: 6, color: #333333 } }] }) /script这里有个设计重点displayPolyline用了computed且只保留最近50个点。原因是map组件的polyline属性每次变化都会触发整条线重新渲染如果数据量很大地图会频繁刷新操作时能明显感觉到卡顿。只保留尾巴上的50个点画面看起来还是连续的一条线但性能完全不一样。轨迹的完整历史线则保存在trackPoints里不直接渲染到地图上。这样设计的好处是实时模式下地图始终保持流畅需要查看完整轨迹时再一次性渲染全量精简点。还有个细节是箭头的配置arrowLine: true用来显示轨迹方向。这对人员轨迹回溯特别重要光一条线看不出移动方向加了箭头后整个移动方向一目了然。实测下来4号线的宽度加箭头画出来比较清晰太细了会看不清箭头太粗了又遮挡底图。3.4 轨迹回放功能怎么实现轨迹回放是我觉得整个项目里最有意思的部分。需求是选一个人某段时间的轨迹地图上动态重演他的移动过程。实现思路其实不复杂本质是一个定时器驱动的坐标数组遍历。每间隔一段时间就往后推几个点同时更新地图上的当前轨迹折线和当前位置标记。function startPlayback(points, speed 1) { let index 0 const playbackTimer setInterval(() { if (index points.length) { clearInterval(playbackTimer) return } const p points[index] // 更新当前播放位置 currentPostion.value { latitude: p.latitude, longitude: p.longitude } // 更新已播放轨迹 playbackPoints.value.push(p) // 让地图中心跟随 mapCenter.value { latitude: p.latitude, longitude: p.longitude } index speed }, 200) }速度控制上我做了倍速选择1倍、2倍、4倍。这里的speed指的是每次掠过几个点不是时间倍率。如果点的采集间隔是2秒一个那么1倍速就是每200毫秒往前推进一个点播放速度比真实时间快10倍。如果希望接近真实速度就应该把定时器间隔调到2000毫秒但这样等待时间太长实际使用中没人会等。所以我把播放速度和点密度解耦了默认用200毫秒一个点的节奏播放提供倍速选项。这样不管原始点有多密回放节奏都是可控的。注意回放完记得清除定时器否则组件销毁后定时器还在跑页面却已经没有了轻则白屏闪一下重则直接报内存泄漏。可以在onUnload里统一清理。4. 跨端适配与打包经验4.1 微信小程序的差异化处理微信小程序端是这套系统最常见的落地载体但小程序平台有一些特殊的限制我踩过几个实实在在的坑。第一是后台定位权限。小程序在用户退到后台之后默认会停止定位如果轨迹系统需要用户切到微信后台时继续记录位置必须在app.json里声明requiredBackgroundModes: [location]同时在小程序后台的设置-接口权限里申请后台定位能力。这个能力不是所有小程序类目都能申请的个人主体小程序基本无缘企业主体也需要提交审核材料。第二是从后台回到前台的数据恢复。即使申请了后台定位小程序被系统回收后代码会重新执行之前内存里的轨迹数组会丢失。我在本地Storage里同步保存了轨迹点每次新进入页面时会先检查Storage里有没有未上报的轨迹数据有就自动补回。第三是小程序端的map组件有层级限制markers和polyline都在地图组件内部渲染无法被普通view盖住。所以所有控件都要放在map组件外部或者用cover-view来实现。这个限制也挺容易踩的在小程序里放一个遮罩引导层用普通view会被地图盖住必须用cover-view。4.2 App端的权限与打包配置App端绕不开权限申请和打包配置。uniapp打包成APK有两种方式云打包和本地打包。云打包方便只要在HBuilderX里填好包名、证书等参数云端自动出包。本地打包需要下载Android Studio和对应的SDK适合需要深度定制的场景。我这边的项目因为不需要原生插件直接用了云打包效率最高。App端定位权限要注意Android 13的颗粒化定位权限。以前只分粗略定位和精确位置两档现在权限模型中还引入了仅本次运行允许选项。如果用户选择了仅本次运行允许App退到后台后定位就会被系统切断需要主动引导用户改成始终允许才能持续记录轨迹。在manifest.json里需要明确的权限声明包括权限名用途备注ACCESS_FINE_LOCATION精确定位必须声明ACCESS_COARSE_LOCATION粗略定位建议同时声明兼容部分机型ACCESS_BACKGROUND_LOCATION后台定位Android 10需要单独声明ACCESS_NETWORK_STATE网络状态检查定位服务往往依赖网络定位iOS端的权限是另一个套路。iOS定位权限弹窗文案必须写清楚为什么需要定位如果用户拒绝权限后续无法通过代码再弹一次只能引导用户去系统设置里手动开启。我的做法是第一次拒绝后在页面上展示一个引导卡片提示去设置里打开权限实测下来比直接再次弹窗体验好很多。4.3 H5端与多域名部署问题H5端是这个系统里最省事但也最容易被忽略的端。省事在于不用处理原生权限直接调用浏览器的navigator.geolocation接口容易被忽略在于跨域和域名白名单。如果H5页面部署的域名不止一个比如测试环境一个域名、正式环境一个域名高德地图的Key就必须同时在后台把两个域名都添加上。这里有个细节高德后台的域名白名单是按HTTP头里的Referrer判断的如果你用的是IP访问或者通过本地代理转发Referrer可能为空或者携带的是代理服务器域名这时候请求会被拦截地图加载不出来但不报错排查起来非常费劲。H5端的轨迹记录还有一个限制浏览器在页面切到后台后对定时器的执行会节流甚至暂停。也就是说用户把浏览器标签页切到后台轨迹记录就基本停了。这个问题在移动端浏览器上尤其明显。如果H5端也有持续记录轨迹的需求最好加入Web Worker或者用socket连接配合后端去做位置上报而不是纯依赖前端定时器。4.4 打包上架的一些建议关于上架Android应用市场有几点经验值得分享。首先是隐私政策必须写到位。现在的应用市场对隐私合规审查非常严格定位权限属于敏感权限隐私政策里必须明确说明搜集定位信息的目的、使用范围、保存期限否则审核直接驳回。我遇见过一个应用因为隐私政策里没有单独说定位权限用途被打回三次才通过。其次是小程序端的审核。涉及地图展示的小程序页面里需要有适当的标注或者示意不能只是一个光秃秃的地图。部分类目的小程序接入地图能力时还需要提供相关材料提前准备好可以省不少时间。5. 常见问题与踩坑实录5.1 问题速查表把我在这个项目里实际遇到的典型问题整理成了一张表如果不是很确定问题出在哪里可以直接对照排查。问题现象可能原因解决方案地图白屏什么都没有高德Key未配置或包名签名不匹配检查manifest.json中的应用Key配置核对高德后台的包名和签名定位点长时间不动定位权限未授予检查manifest.json权限声明引导用户开启定位权限定位点大范围跳动室内定位信号差精度值大在定位回调里判断accuracy过滤低精度数据轨迹线穿墙或明显偏移坐标系不统一统一到GCJ-02坐标系检查拿到的坐标是WGS-84还是GCJ-02polyline数据量大时卡顿参与了全量点的渲染改用增量渲染只渲染最近一段轨迹回放开始后页面卡死定时器未清理反复触发在onUnload中清除定时器增加防重入判断H5端地图加载失败域名未加入白名单在高德后台将所有部署域名加入白名单App后台一段时间后轨迹断了系统限制了后台定位引导用户设置始终允许申请后台定位权限微信小程序退后台轨迹丢失内存被回收或定位被暂停本地Storage同步保存重进页面时恢复数据地图显示时总是以不恰当的中心点未处理地图中心跟随每次更新点时同步更新mapCenter或者使用include-points自动调整视野5.2 最容易被忽视的三个细节第一个是onLocationChange在App端的生命周期问题。onLocationChange要在startLocation成功之后才能触发如果startLocation失败比如权限没有授权这个监听永远不会回调。很多人在权限被拒绝后反复确认代码逻辑其实问题出在启动定位的时机上。我建议在onReady后再启动定位不要在onLoad里就调用因为onLoad时页面还没完全渲染部分端的权限弹窗和回调可能没有就绪。第二个是地图组件的scale值和轨迹显示的矛盾。地图缩放级别太大轨迹只显示一小段看起来不完整缩放级别太小轨迹缩成一团看不清细节。我做了个处理实时跟踪时固定scale为16轨迹回放开始时用include-points让地图自动适配全部轨迹点播放过程中再逐步缩小到合适的级别。这样既能看到全貌又不影响局部细节。第三个是轨迹点的时序性。同一个人的轨迹点虽然存在数组里理论上是有序的但如果定位回调出现乱序画出来的轨迹就会来回乱转。我在写入轨迹数组时加了时间戳判断如果新点的时间戳小于最后一个点的时间戳就跳过这个点。这样可以有效避免部分端在信号恢复时把缓存的老点位插到新点位后面。5.3 一些建议和经验代码层面建议把轨迹采集和轨迹渲染完全解耦。采集只管往数组里塞点渲染层通过computed或者watch来响应变化。这样即使采集频率很高渲染层也有机会做节流和合并不会因为采集频率导致渲染卡顿。性能调优上除了抽稀和增量渲染还有一个技巧在地图视野变化时暂停折线更新。用户看地图时正在拖动地图系统每200毫秒画一条新线画线本身没问题但触发的地图重绘会跟用户手势打架导致地图卡顿感明显。我是在regionchange事件里判断如果地图处于拖拽状态就暂停更新等拖动结束再补上新点。数据上报方面建议轨迹数据不要一条条上报而是攒够一定量或者每次停顿后作为一个批次上报。一个是减少网络请求另一个是轨迹本身是有完整性的单独一个点说明不了问题只有一条完整的轨迹才有分析价值。最后再说一点体会做这个项目最大的感受是看似简单的功能把细节抠深了才发现水很深。坐标系、定位精度、跨端差异、性能优化随便拉出来一个都能埋不少坑。我自己在这一周里来回调试定位参数就花了大半天最终把阈值焊死在60米时才真正看到了干净稳定的轨迹线。如果你现在也在做类似的人员轨迹绘制我建议你先把坐标系和定位精度这两个地基打牢再开始写画线代码。否则后面每加一个功能前面的坑都会重新翻出来找你。上面这些代码和方案基本可以直接拿去用只要注意把Key和包名换成你自己的就行。祝顺利。
返回列表