慧视项目微信小程序步行导航轮询与偏航检测实现
轮询间隔为什么定 3 秒实时导航有两种思路一种是服务端主动推WebSocket / SSE另一种是客户端定时拉。对于步行导航服务端没有主动推的依据因为用户的实时位置只有客户端知道。每次轮询都得客户端先上报位置服务端再告知下一步怎么走所以定时轮询在这个场景下更合适。间隔定为 3 秒POLL_INTERVAL_MS 3000。步行速度大约 1.2 m/s3 秒约移动 3.6 米。这个粒度下用户走一步路就能收到一次更新指令不会滞后太明显。间隔再短网络请求和定位调用的成本上来了再长用户已经走过转弯口了指令才到。启动时立即执行一次 tick不等第一个 interval 触发避免用户开始导航后盯着屏幕等 3 秒没反应tick();navCtx.pollTimersetInterval(tick,POLL_INTERVAL_MS)asunknownasnumber;startNavLoop返回一个 stop 函数调用方在切换模式或离开页面时调用它确保 setInterval 不会在后台静默地跑着。步骤键判断换步骤不能比 instruction 文本每次轮询拿到后端返回的导航指令需要判断这条指令和上一条是否属于同一个导航步骤。看起来比 instruction 文本是否相同就够了但 instruction 里含有实时米数比如还有 18 米后右转下一次轮询可能变成还有 15 米后右转。文本变了但步骤根本没切换用户还在同一段路上走。真正标识一个步骤的是这一步的结构性信息当前在哪条路、动作是什么、转入哪条路。把这三个字段拼成步骤键conststepKeyresp.maneuver!undefined?${resp.currentRoad??}|${resp.maneuver}|${resp.nextRoad??}:resp.instruction;// 旧版后端字段不全时降级用整句stepKey变了才说明真正进入新步骤stepKey没变说明还在同一步骤里移动只是距离数值在变化。偏航检测同步骤内距离反增连续两次才触发偏航的直觉很简单正常行进时距离下个转折点的距离应该越来越短。如果反而越来越远说明走偏了。但 GPS 本身有抖动静止不动也会漂移几米单次距离增加不足以作为判断依据。因此加了两个过滤条件if(lastStepKey!nulllastStepKeystepKey// 同一步骤步骤键没变prevDistance!nullresp.distanceToTurnprevDistance5// 增加超过 5 米){deviationHits1;}else{deviationHits0;// 正常行进时清零}5 米容差是为了过滤 GPS 抖动。连续两次命中deviationHits 2才触发偏航回调一次抖动不足以让判断成立。触发后deviationHits归零。这是因为偏航后会异步重规划归零避免重规划期间的轮询结果继续累加命中。防重规划死循环偏航后触发回调导航页负责重规划停掉当前轮询发起新的路线规划用新 routeId 开启新的轮询循环。关键细节在于重规划后启动的新 loop其内部的onDeviation回调是空的// navigation.tsthis.stopLoopstartNavLoop(newRoute.routeId,app.globalData.navContext,{onDeviation:async(){/* 二次偏航不再重规划避免死循环 */},// ...});如果用户在某个地方反复偏航比如路被封了、GPS 一直漂移无限重规划会造成循环。第一次偏航自动重规第二次就不动了用户可以手动说重新规划来主动触发。弱信号检测每段弱信号只提示一次GPS 的accuracy表示定位误差半径单位是米值越大说明定位越不准。室内或高楼峡谷里accuracy 容易飙到 50 米以上这时候导航指令基本没参考价值。constINDOOR_ACCURACY_M50;if(loc.accuracyINDOOR_ACCURACY_M){weakSignalHits1;if(weakSignalHits2!weakSignalFired){weakSignalFiredtrue;callbacks.onWeakSignal?.();}}else{weakSignalHits0;weakSignalFiredfalse;// 信号恢复后下次进入弱信号区域可以再提示}和偏航一样连续两次才触发。weakSignalFired标志保证同一段弱信号区域内只提示一次而不是每隔 3 秒播一次定位不准。信号恢复后这个标志复位下次走进室内还会提示。到达终点判定到达逻辑放在偏航检测之前距离下个转折点小于 10 米就判为到达constARRIVED_DISTANCE_M10;if(resp.distanceToTurnARRIVED_DISTANCE_M){callbacks.onArrived();return;// 不再继续 tick}10 米对步行场景够精确了用户已经走进目的地范围。tick 函数在这里 return 之后不再继续由onArrived的调用方负责停掉轮询。后端Haversine 公式找离用户最近的路段步骤前端把当前经纬度发给后端后端需要判断用户走到了哪一步。高德返回的每个 step 里有polyline字段格式是折线坐标串经度,纬度;经度,纬度;...。取每段 step 的起点坐标用 Haversine 公式算出它和用户当前位置的距离找距离用户位置最近的那个defcalculate_distance(lat1,lng1,lat2,lng2):rad_lat1math.radians(lat1)rad_lat2math.radians(lat2)arad_lat1-rad_lat2 bmath.radians(lng1)-math.radians(lng2)s2*math.asin(math.sqrt(math.pow(math.sin(a/2),2)math.cos(rad_lat1)*math.cos(rad_lat2)*math.pow(math.sin(b/2),2)))returns*6378137# 地球半径 6378137 米找到匹配步骤后再算用户到该步骤终点转折点坐标的距离作为distanceToTurn返回。如果已经是路线的最后一步转折点换成终点坐标来算ifclosest_step_indexlen(steps)-1:distance_to_turncalculate_distance(user_lat,user_lng,route_data[dest_lat],route_data[dest_lng])路线缓存在内存里的限制规划路线后后端把高德返回的完整 steps 存在一个模块级 dict_route_cache里以 routeId 为键。前端每次轮询带上 routeId后端从这个 dict 取。多进程或多实例部署时两个进程各自的_route_cache互不共享同一个 routeId 可能在另一个进程里找不到需要换成 Redis 或数据库来存。整套逻辑里步骤键的设计是个比较容易忽略的细节。instruction 文本里夹着实时米数实际上每隔 3 秒就变没法用来判断步骤是否切换。换成currentRoad|maneuver|nextRoad三元组之后才有了稳定的比较基准。