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