HarmonyOS APP开发:路线规划与导航SDK
HarmonyOS APP开发:路线规划与导航SDK
核心要点:掌握HarmonyOS路线规划API、多策略路线对比、实时导航状态机与语音播报集成的完整实现
一、背景与动机
路线规划是地图应用的核心能力之一,从外卖配送的"最短时间路线"到自驾出行的"避开收费路线",再到骑行场景的"专用自行车道路线",不同出行方式对路线规划的需求截然不同。HarmonyOS通过华为Petal Maps引擎,提供了驾车、步行、骑行三种出行方式的路线规划能力,并支持实时导航与语音播报。
然而,路线规划开发中存在以下核心挑战:
- 多策略路线对比:如何同时请求多条不同策略的路线,并在地图上差异化展示?
- 实时导航状态机:导航过程中的偏航重算、到达途经点、到达终点等状态如何管理?
- 路线数据简化:长距离路线可能包含数千个坐标点,如何在不损失精度的前提下优化渲染性能?
- 语音播报集成:如何将导航指令与TTS语音引擎无缝集成?
- 离线路线缓存:如何在无网络环境下提供基础路线规划能力?
本文将从路线规划的底层算法出发,结合完整代码实战,逐一攻克上述难题。
二、核心原理
2.1 路线规划服务架构
HarmonyOS的路线规划服务采用云端计算 + 本地渲染的混合架构:路线计算在云端完成(基于Dijkstra/A*算法),路线渲染在本地完成(基于Map组件的Polyline图层)。

2.2 导航状态机
导航过程是一个典型的有限状态机,理解状态转换是正确实现导航功能的关键:
| 当前状态 | 事件 | 目标状态 | 动作 |
|---|---|---|---|
| IDLE | startNavigation | NAVIGATING | 开始语音播报,启动定位追踪 |
| NAVIGATING | onRouteDeviated | RECALCULATING | 播报"已偏航,正在重新规划" |
| NAVIGATING | onWaypointReached | NAVIGATING | 播报"已到达途经点",更新剩余路线 |
| NAVIGATING | onDestinationReached | COMPLETED | 播报"已到达目的地",停止导航 |
| RECALCULATING | onRecalcSuccess | NAVIGATING | 更新路线,恢复导航 |
| RECALCULATING | onRecalcFailed | DEVIATED | 播报"无法重新规划",提示手动操作 |
| COMPLETED | stopNavigation | IDLE | 清理导航资源 |
2.3 Douglas-Peucker路线简化算法
长距离路线可能包含数千个坐标点,直接渲染会导致GPU压力过大。Douglas-Peucker算法通过递归移除偏差小于阈值的中间点,在保持路线形状的前提下大幅减少点数:
算法步骤:
1. 取路线首尾两点连线
2. 计算所有中间点到该直线的距离
3. 找到最大距离点,若距离 < 阈值,则移除所有中间点
4. 若距离 ≥ 阈值,以该点为分割点,递归处理两段
5. 直到所有段都无法再简化
效果示例:
原始路线: 2000个点 → 简化后: 300个点(误差 < 5米)
三、代码实战
3.1 路线规划请求与多策略对比
// RoutePlanManager.ets - 路线规划管理器
import { map, mapCommon } from '@kit.MapKit';
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 路线策略枚举
export enum RouteStrategy {
FASTEST = 0, // 最快路线
SHORTEST = 1, // 最短路线
NO_TOLL = 2, // 避免收费
NO_HIGHWAY = 3, // 避免高速
MULTI_ROUTE = 4 // 多路线对比
}
// 路线数据模型
export interface RouteResult {
strategy: RouteStrategy;
distance: number; // 总距离(米)
duration: number; // 预计耗时(秒)
tolls: number; // 收费金额(元)
polylinePoints: Array<{ latitude: number; longitude: number }>;
steps: RouteStep[]; // 路线步骤
isRecommended: boolean; // 是否为推荐路线
}
export interface RouteStep {
instruction: string; // 导航指令文本
distance: number; // 步骤距离(米)
duration: number; // 步骤耗时(秒)
maneuver: string; // 转向类型:turn-left, turn-right, straight, etc.
startLat: number;
startLon: number;
endLat: number;
endLon: number;
}
export class RoutePlanManager {
private controller?: map.MapComponentController;
// 路线渲染引用
private routePolylines: map.Polyline[] = [];
// 当前选中的路线
private selectedRouteIndex: number = 0;
// 路线结果缓存
private routeResults: RouteResult[] = [];
init(controller: map.MapComponentController): void {
this.controller = controller;
}
// 请求多策略路线规划
async requestMultiRoute(
origin: { lat: number; lon: number },
destination: { lat: number; lon: number },
waypoints?: Array<{ lat: number; lon: number }>
): Promise<RouteResult[]> {
this.routeResults = [];
// 并行请求多种策略的路线
const strategies = [
RouteStrategy.FASTEST,
RouteStrategy.SHORTEST,
RouteStrategy.NO_TOLL
];
const promises = strategies.map(strategy =>
this.requestRoute(origin, destination, strategy, waypoints)
);
try {
const results = await Promise.allSettled(promises);
results.forEach(result => {
if (result.status === 'fulfilled' && result.value) {
this.routeResults.push(result.value);
}
});
// 标记推荐路线(耗时最短)
if (this.routeResults.length > 0) {
const fastestIndex = this.routeResults
.reduce((minIdx, curr, idx, arr) =>
curr.duration < arr[minIdx].duration ? idx : minIdx, 0);
this.routeResults[fastestIndex].isRecommended = true;
}
// 渲染所有路线
this.renderMultiRoutes();
return this.routeResults;
} catch (error) {
const err = error as BusinessError;
console.error(`[RoutePlan] 路线规划失败: ${err.code} - ${err.message}`);
return [];
}
}
// 请求单条路线
private async requestRoute(
origin: { lat: number; lon: number },
destination: { lat: number; lon: number },
strategy: RouteStrategy,
waypoints?: Array<{ lat: number; lon: number }>
): Promise<RouteResult | null> {
try {
// 调用华为路线规划API
const routeResult = await map.searchRoute({
type: mapCommon.RouteType.DRIVING,
origin: { latitude: origin.lat, longitude: origin.lon },
destination: { latitude: destination.lat, longitude: destination.lon },
waypoints: waypoints?.map(wp => ({ latitude: wp.lat, longitude: wp.lon })),
strategy: strategy
});
// 解析路线数据
return this.parseRouteResult(routeResult, strategy);
} catch (error) {
console.error(`[RoutePlan] 策略${strategy}路线请求失败: ${error}`);
return null;
}
}
// 解析路线结果
private parseRouteResult(
rawResult: mapCommon.RouteResult,
strategy: RouteStrategy
): RouteResult {
const paths = rawResult.paths;
if (!paths || paths.length === 0) {
return {
strategy,
distance: 0,
duration: 0,
tolls: 0,
polylinePoints: [],
steps: [],
isRecommended: false
};
}
const path = paths[0]; // 取第一条路线
const polylinePoints: Array<{ latitude: number; longitude: number }> = [];
const steps: RouteStep[] = [];
// 提取路线坐标点
path.steps.forEach(step => {
step.polyline?.forEach(point => {
polylinePoints.push({
latitude: point.latitude,
longitude: point.longitude
});
});
steps.push({
instruction: step.instruction ?? '',
distance: step.distance ?? 0,
duration: step.duration ?? 0,
maneuver: step.maneuver ?? 'straight',
startLat: step.startLocation?.latitude ?? 0,
startLon: step.startLocation?.longitude ?? 0,
endLat: step.endLocation?.latitude ?? 0,
endLon: step.endLocation?.longitude ?? 0
});
});
return {
strategy,
distance: path.distance ?? 0,
duration: path.duration ?? 0,
tolls: path.tolls ?? 0,
polylinePoints,
steps,
isRecommended: false
};
}
// 渲染多路线 - 差异化展示
private renderMultiRoutes(): void {
if (!this.controller) return;
// 清除旧路线
this.clearAllRoutes();
this.routeResults.forEach((route, index) => {
// Douglas-Peucker简化路线点数
const simplifiedPoints = this.douglasPeucker(route.polylinePoints, 5);
const polylineOptions: mapCommon.PolylineOptions = {
points: simplifiedPoints,
// 推荐路线用蓝色粗线,备选路线用灰色细线
color: route.isRecommended ? '#4285F4' : '#B0BEC5',
width: route.isRecommended ? 12 : 6,
capType: mapCommon.CapType.ROUND,
joinType: mapCommon.JoinType.ROUND,
visible: true,
zIndex: route.isRecommended ? 10 : 5
};
const polyline = this.controller!.addPolyline(polylineOptions);
this.routePolylines.push(polyline);
});
// 调整视口以包含所有路线
this.fitRouteBounds();
}
// Douglas-Peucker路线简化算法
private douglasPeucker(
points: Array<{ latitude: number; longitude: number }>,
epsilon: number
): Array<{ latitude: number; longitude: number }> {
if (points.length <= 2) return [...points];
// 找到最大偏差点
let maxDist = 0;
let maxIndex = 0;
const start = points[0];
const end = points[points.length - 1];
for (let i = 1; i < points.length - 1; i++) {
const dist = this.pointToLineDistance(points[i], start, end);
if (dist > maxDist) {
maxDist = dist;
maxIndex = i;
}
}
// 递归简化
if (maxDist > epsilon) {
const left = this.douglasPeucker(points.slice(0, maxIndex + 1), epsilon);
const right = this.douglasPeucker(points.slice(maxIndex), epsilon);
return [...left.slice(0, -1), ...right];
} else {
return [start, end];
}
}
// 点到线段的距离(简化版,使用经纬度近似)
private pointToLineDistance(
point: { latitude: number; longitude: number },
lineStart: { latitude: number; longitude: number },
lineEnd: { latitude: number; longitude: number }
): number {
const dx = lineEnd.longitude - lineStart.longitude;
const dy = lineEnd.latitude - lineStart.latitude;
const lenSq = dx * dx + dy * dy;
if (lenSq === 0) {
// 线段退化为点
return Math.sqrt(
Math.pow(point.longitude - lineStart.longitude, 2) +
Math.pow(point.latitude - lineStart.latitude, 2)
);
}
// 投影参数
let t = ((point.longitude - lineStart.longitude) * dx +
(point.latitude - lineStart.latitude) * dy) / lenSq;
t = Math.max(0, Math.min(1, t));
// 投影点
const projLon = lineStart.longitude + t * dx;
const projLat = lineStart.latitude + t * dy;
return Math.sqrt(
Math.pow(point.longitude - projLon, 2) +
Math.pow(point.latitude - projLat, 2)
);
}
// 调整视口以包含所有路线
private fitRouteBounds(): void {
if (!this.controller || this.routeResults.length === 0) return;
let minLat = 90, maxLat = -90, minLon = 180, maxLon = -180;
this.routeResults.forEach(route => {
route.polylinePoints.forEach(p => {
minLat = Math.min(minLat, p.latitude);
maxLat = Math.max(maxLat, p.latitude);
minLon = Math.min(minLon, p.longitude);
maxLon = Math.max(maxLon, p.longitude);
});
});
const bounds: mapCommon.LatLngBounds = {
southwest: { latitude: minLat, longitude: minLon },
northeast: { latitude: maxLat, longitude: maxLon }
};
this.controller.animateCamera(map.newLatLngBounds(bounds, 100));
}
// 选择路线
selectRoute(index: number): void {
if (index < 0 || index >= this.routeResults.length) return;
this.selectedRouteIndex = index;
// 更新路线渲染样式
this.routePolylines.forEach((polyline, i) => {
const route = this.routeResults[i];
if (i === index) {
polyline.setColor('#4285F4');
polyline.setWidth(12);
polyline.setZIndex(10);
} else {
polyline.setColor('#B0BEC5');
polyline.setWidth(6);
polyline.setZIndex(5);
}
});
}
// 清除所有路线
clearAllRoutes(): void {
this.routePolylines.forEach(p => p.remove());
this.routePolylines = [];
}
// 获取当前选中的路线
getSelectedRoute(): RouteResult | null {
return this.routeResults[this.selectedRouteIndex] ?? null;
}
destroy(): void {
this.clearAllRoutes();
this.routeResults = [];
}
}
3.2 实时导航状态机
// NavigationStateMachine.ets - 导航状态机
import { geoLocationManager } from '@kit.LocationKit';
import { map, mapCommon } from '@kit.MapKit';
// 导航状态枚举
export enum NavState {
IDLE = 'IDLE',
NAVIGATING = 'NAVIGATING',
RECALCULATING = 'RECALCULATING',
DEVIATED = 'DEVIATED',
COMPLETED = 'COMPLETED'
}
// 导航事件枚举
export enum NavEvent {
START = 'START',
LOCATION_UPDATE = 'LOCATION_UPDATE',
ROUTE_DEVIATED = 'ROUTE_DEVIATED',
RECALC_SUCCESS = 'RECALC_SUCCESS',
RECALC_FAILED = 'RECALC_FAILED',
WAYPOINT_REACHED = 'WAYPOINT_REACHED',
DESTINATION_REACHED = 'DESTINATION_REACHED',
STOP = 'STOP'
}
// 导航信息
export interface NavigationInfo {
state: NavState;
remainingDistance: number; // 剩余距离(米)
remainingDuration: number; // 剩余时间(秒)
currentSpeed: number; // 当前速度(米/秒)
nextStepInstruction: string; // 下一步导航指令
nextStepDistance: number; // 下一步距离(米)
nextManeuver: string; // 下一步转向类型
}
export class NavigationStateMachine {
private currentState: NavState = NavState.IDLE;
// 偏航检测阈值(米)
private deviationThreshold: number = 30;
// 到达判定阈值(米)
private arrivalThreshold: number = 20;
// 当前路线步骤索引
private currentStepIndex: number = 0;
// 路线步骤列表
private steps: RouteStep[] = [];
// 位置监听ID
private locationListenerId: number = -1;
// 状态变化回调
onStateChanged?: (state: NavState) => void;
// 导航信息更新回调
onNavigationInfoUpdate?: (info: NavigationInfo) => void;
// 偏航回调
onDeviation?: () => void;
// 到达终点回调
onArrival?: () => void;
// 启动导航
startNavigation(steps: RouteStep[]): void {
this.steps = steps;
this.currentStepIndex = 0;
this.transition(NavState.NAVIGATING);
this.startLocationTracking();
}
// 处理导航事件
handleEvent(event: NavEvent, data?: object): void {
switch (this.currentState) {
case NavState.IDLE:
if (event === NavEvent.START) {
this.transition(NavState.NAVIGATING);
}
break;
case NavState.NAVIGATING:
if (event === NavEvent.LOCATION_UPDATE) {
this.processLocationUpdate(data as geoLocationManager.Location);
} else if (event === NavEvent.ROUTE_DEVIATED) {
this.transition(NavState.RECALCULATING);
this.onDeviation?.();
} else if (event === NavEvent.WAYPOINT_REACHED) {
this.currentStepIndex++;
this.emitNavigationInfo();
} else if (event === NavEvent.DESTINATION_REACHED) {
this.transition(NavState.COMPLETED);
this.stopLocationTracking();
this.onArrival?.();
} else if (event === NavEvent.STOP) {
this.transition(NavState.IDLE);
this.stopLocationTracking();
}
break;
case NavState.RECALCULATING:
if (event === NavEvent.RECALC_SUCCESS) {
this.transition(NavState.NAVIGATING);
} else if (event === NavEvent.RECALC_FAILED) {
this.transition(NavState.DEVIATED);
} else if (event === NavEvent.STOP) {
this.transition(NavState.IDLE);
this.stopLocationTracking();
}
break;
case NavState.DEVIATED:
if (event === NavEvent.START) {
this.transition(NavState.NAVIGATING);
} else if (event === NavEvent.STOP) {
this.transition(NavState.IDLE);
this.stopLocationTracking();
}
break;
case NavState.COMPLETED:
if (event === NavEvent.STOP) {
this.transition(NavState.IDLE);
}
break;
}
}
// 处理位置更新
private processLocationUpdate(location: geoLocationManager.Location): void {
const currentLat = location.latitude;
const currentLon = location.longitude;
// 检查是否偏航
if (this.isDeviated(currentLat, currentLon)) {
this.handleEvent(NavEvent.ROUTE_DEVIATED);
return;
}
// 检查是否到达下一步骤
if (this.currentStepIndex < this.steps.length) {
const step = this.steps[this.currentStepIndex];
const distToEnd = this.calculateDistance(
currentLat, currentLon, step.endLat, step.endLon
);
if (distToEnd < this.arrivalThreshold) {
if (this.currentStepIndex === this.steps.length - 1) {
this.handleEvent(NavEvent.DESTINATION_REACHED);
} else {
this.handleEvent(NavEvent.WAYPOINT_REACHED);
}
return;
}
}
// 更新导航信息
this.emitNavigationInfo();
}
// 偏航检测
private isDeviated(lat: number, lon: number): boolean {
if (this.currentStepIndex >= this.steps.length) return false;
const step = this.steps[this.currentStepIndex];
const distToRoute = this.pointToSegmentDistance(
lat, lon, step.startLat, step.startLon, step.endLat, step.endLon
);
return distToRoute > this.deviationThreshold;
}
// 计算点到线段的距离
private pointToSegmentDistance(
px: number, py: number,
ax: number, ay: number,
bx: number, by: number
): number {
const dx = bx - ax;
const dy = by - ay;
const lenSq = dx * dx + dy * dy;
if (lenSq === 0) {
return this.calculateDistance(px, py, ax, ay);
}
let t = ((px - ax) * dx + (py - ay) * dy) / lenSq;
t = Math.max(0, Math.min(1, t));
const projX = ax + t * dx;
const projY = ay + t * dy;
return this.calculateDistance(px, py, projX, projY);
}
// Haversine公式计算两点距离
private calculateDistance(
lat1: number, lon1: number,
lat2: number, lon2: number
): number {
const R = 6371000; // 地球半径(米)
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
// 发射导航信息
private emitNavigationInfo(): void {
if (this.currentStepIndex >= this.steps.length) return;
const step = this.steps[this.currentStepIndex];
let remainingDistance = 0;
let remainingDuration = 0;
for (let i = this.currentStepIndex; i < this.steps.length; i++) {
remainingDistance += this.steps[i].distance;
remainingDuration += this.steps[i].duration;
}
const info: NavigationInfo = {
state: this.currentState,
remainingDistance,
remainingDuration,
currentSpeed: 0,
nextStepInstruction: step.instruction,
nextStepDistance: step.distance,
nextManeuver: step.maneuver
};
this.onNavigationInfoUpdate?.(info);
}
// 状态转换
private transition(newState: NavState): void {
console.info(`[NavStateMachine] 状态转换: ${this.currentState} → ${newState}`);
this.currentState = newState;
this.onStateChanged?.(newState);
}
// 启动位置追踪
private startLocationTracking(): void {
const locationRequest: geoLocationManager.LocationRequest = {
priority: geoLocationManager.LocationRequestPriority.ACCURACY,
timeInterval: 1, // 1秒更新一次
distanceInterval: 5 // 5米更新一次
};
try {
this.locationListenerId = geoLocationManager.on('locationChange', locationRequest, (location) => {
this.handleEvent(NavEvent.LOCATION_UPDATE, location);
});
} catch (error) {
console.error(`[NavStateMachine] 位置追踪启动失败: ${error}`);
}
}
// 停止位置追踪
private stopLocationTracking(): void {
if (this.locationListenerId !== -1) {
try {
geoLocationManager.off('locationChange', this.locationListenerId);
this.locationListenerId = -1;
} catch (error) {
console.error(`[NavStateMachine] 位置追踪停止失败: ${error}`);
}
}
}
// 获取当前状态
getCurrentState(): NavState {
return this.currentState;
}
// 销毁
destroy(): void {
this.stopLocationTracking();
this.currentState = NavState.IDLE;
this.steps = [];
this.currentStepIndex = 0;
}
}
3.3 语音播报集成
// NavigationVoicePlayer.ets - 导航语音播报器
import { textToSpeech } from '@kit.AIKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 导航指令模板
const VOICE_TEMPLATES: Record<string, string> = {
'turn-left': '前方{distance}米左转,进入{road}',
'turn-right': '前方{distance}米右转,进入{road}',
'straight': '继续直行{distance}米',
'slight-left': '前方{distance}米稍向左转',
'slight-right': '前方{distance}米稍向右转',
'sharp-left': '前方{distance}米急左转',
'sharp-right': '前方{distance}米急右转',
'uturn': '前方{distance}米请掉头',
'arrive': '您已到达目的地',
'deviated': '您已偏离路线,正在重新规划',
'recalc-success': '路线已重新规划,请按新路线行驶',
'recalc-failed': '无法重新规划路线,请手动调整',
'speed-warning': '您已超速,当前限速{limit}公里',
'camera-warning': '前方{distance}米有测速摄像头'
};
export class NavigationVoicePlayer {
private ttsEngine?: textToSpeech.TextToSpeechEngine;
private isSpeaking: boolean = false;
// 语音播报最小间隔(毫秒)
private minInterval: number = 3000;
private lastSpeakTime: number = 0;
// 初始化TTS引擎
async init(context: Context): Promise<void> {
try {
const initParams: textToSpeech.CreateEngineParams = {
language: 'zh-CN',
person: 0, // 女声
online: 1, // 在线模式,音质更好
extraParams: {
speed: 1.0, // 语速
pitch: 1.0, // 音调
volume: 1.0 // 音量
}
};
this.ttsEngine = textToSpeech.createEngine(initParams);
// 设置语音合成回调
this.ttsEngine.on('speakComplete', () => {
this.isSpeaking = false;
});
this.ttsEngine.on('error', (error: BusinessError) => {
console.error(`[VoicePlayer] TTS错误: ${error.code} - ${error.message}`);
this.isSpeaking = false;
});
console.info('[VoicePlayer] TTS引擎初始化成功');
} catch (error) {
console.error(`[VoicePlayer] TTS引擎初始化失败: ${error}`);
}
}
// 播报导航指令
speak(maneuver: string, params?: Record<string, string>): void {
// 防止频繁播报
const now = Date.now();
if (now - this.lastSpeakTime < this.minInterval) {
return;
}
// 如果正在播报,先停止
if (this.isSpeaking && this.ttsEngine) {
this.ttsEngine.stop();
this.isSpeaking = false;
}
// 生成播报文本
let text = VOICE_TEMPLATES[maneuver] ?? `前方请注意${maneuver}`;
if (params) {
Object.entries(params).forEach(([key, value]) => {
text = text.replace(`{${key}}`, value);
});
}
// 执行语音合成
if (this.ttsEngine) {
try {
const speakParams: textToSpeech.SpeakParams = {
requestId: `nav_${Date.now()}`,
extraParams: {
queueMode: 0, // 清空队列后播报
playType: 0 // 播放模式
}
};
this.ttsEngine.speak(text, speakParams);
this.isSpeaking = true;
this.lastSpeakTime = now;
console.info(`[VoicePlayer] 播报: ${text}`);
} catch (error) {
console.error(`[VoicePlayer] 语音合成失败: ${error}`);
}
}
}
// 播报自定义文本
speakText(text: string): void {
if (this.ttsEngine) {
try {
const speakParams: textToSpeech.SpeakParams = {
requestId: `nav_${Date.now()}`,
extraParams: {
queueMode: 0,
playType: 0
}
};
this.ttsEngine.speak(text, speakParams);
this.isSpeaking = true;
this.lastSpeakTime = Date.now();
} catch (error) {
console.error(`[VoicePlayer] 自定义播报失败: ${error}`);
}
}
}
// 停止播报
stop(): void {
if (this.ttsEngine) {
this.ttsEngine.stop();
this.isSpeaking = false;
}
}
// 销毁
destroy(): void {
if (this.ttsEngine) {
this.ttsEngine.shutdown();
this.ttsEngine = undefined;
}
this.isSpeaking = false;
}
}
3.4 导航UI面板组件
// NavigationPanel.ets - 导航面板UI组件
import { NavState, NavigationInfo } from './NavigationStateMachine';
@Component
export struct NavigationPanel {
@Prop navInfo: NavigationInfo = {
state: NavState.IDLE,
remainingDistance: 0,
remainingDuration: 0,
currentSpeed: 0,
nextStepInstruction: '',
nextStepDistance: 0,
nextManeuver: ''
};
@Prop isNavigating: boolean = false;
// 退出导航回调
onExitNavigation?: () => void;
// 格式化距离
private formatDistance(meters: number): string {
if (meters >= 1000) {
return `${(meters / 1000).toFixed(1)}公里`;
}
return `${Math.round(meters)}米`;
}
// 格式化时间
private formatDuration(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}小时${minutes}分钟`;
}
return `${minutes}分钟`;
}
// 获取转向图标
private getManeuverIcon(maneuver: string): Resource {
const iconMap: Record<string, Resource> = {
'turn-left': $r('app.media.ic_nav_turn_left'),
'turn-right': $r('app.media.ic_nav_turn_right'),
'straight': $r('app.media.ic_nav_straight'),
'slight-left': $r('app.media.ic_nav_slight_left'),
'slight-right': $r('app.media.ic_nav_slight_right'),
'uturn': $r('app.media.ic_nav_uturn')
};
return iconMap[maneuver] ?? $r('app.media.ic_nav_straight');
}
build() {
Column() {
if (this.isNavigating) {
// 导航中 - 显示导航信息面板
this.NavigationInfoPanel()
} else {
// 非导航状态 - 显示路线概览
this.RouteOverviewPanel()
}
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius({ topLeft: 20, topRight: 20 })
.shadow({
radius: 20,
color: 'rgba(0,0,0,0.1)',
offsetX: 0,
offsetY: -4
})
}
@Builder
NavigationInfoPanel() {
Column() {
// 下一步导航指令
Row() {
Image(this.getManeuverIcon(this.navInfo.nextManeuver))
.width(48)
.height(48)
.fillColor('#4285F4')
Column() {
Text(this.formatDistance(this.navInfo.nextStepDistance))
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
Text(this.navInfo.nextStepInstruction)
.fontSize(14)
.fontColor('#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
}
.width('100%')
.padding({ left: 20, right: 20, top: 16, bottom: 12 })
Divider().color('#F0F0F0')
// 剩余距离与时间
Row() {
Column() {
Text(this.formatDistance(this.navInfo.remainingDistance))
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#1A1A1A')
Text('剩余距离')
.fontSize(12)
.fontColor('#999999')
.margin({ top: 2 })
}
Blank()
Column() {
Text(this.formatDuration(this.navInfo.remainingDuration))
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#1A1A1A')
Text('预计到达')
.fontSize(12)
.fontColor('#999999')
.margin({ top: 2 })
}
Blank()
// 退出导航按钮
Button('退出导航')
.fontSize(12)
.fontColor('#FF5252')
.backgroundColor('#FFF0F0')
.borderRadius(16)
.height(32)
.padding({ left: 12, right: 12 })
.onClick(() => {
this.onExitNavigation?.();
})
}
.width('100%')
.padding({ left: 20, right: 20, top: 12, bottom: 16 })
}
}
@Builder
RouteOverviewPanel() {
Column() {
Text('路线概览')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
.padding({ left: 20, top: 16 })
}
.width('100%')
}
}
四、踩坑与注意事项
4.1 路线规划API调用限制
| 限制项 | 免费版 | 付费版 | 建议 |
|---|---|---|---|
| 日调用次数 | 5000次 | 100000次 | 本地缓存路线结果 |
| 并发请求数 | 5次/秒 | 20次/秒 | 串行请求或限流 |
| 路线坐标点数 | 500点/路线 | 2000点/路线 | Douglas-Peucker简化 |
| 途经点数量 | 5个 | 16个 | 合并相近途经点 |
4.2 导航偏航检测精度
偏航检测的阈值设置直接影响用户体验:
- 阈值过小(< 10米):GPS漂移导致频繁误报偏航,用户体验极差
- 阈值过大(> 50米):真正偏航时无法及时检测,错过重算时机
- 推荐值:城市道路 20-30米,高速公路 30-50米
优化策略:结合道路匹配算法,将GPS点吸附到最近的道路上,再判断是否偏航。
4.3 语音播报时序问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 播报延迟 | TTS合成耗时 | 预合成常用指令,缓存音频 |
| 播报中断 | 新指令打断旧指令 | 设置minInterval防止频繁播报 |
| 播报堆积 | 多条指令排队 | 清空队列后播报最新指令 |
| 离线无声 | 在线TTS无网络 | 切换到离线TTS引擎 |
4.4 路线渲染性能优化
- 分级渲染:远距离缩放时只渲染路线骨架(10%点数),近距离缩放时渲染完整路线
- 分段着色:已走过的路线用浅色,未走过的用深色,当前步骤用高亮色
- 路线动画:导航时路线末端添加脉冲动画,指示行进方向
- 避免频繁重绘:路线数据变化时使用
updatePolylinePoints而非删除重建
五、HarmonyOS 6适配
5.1 沉浸式导航模式
HarmonyOS 6支持全屏沉浸式导航,地图自动切换为深色模式,导航面板半透明叠加:
// HarmonyOS 6 沉浸式导航配置(预览)
NavigationConfig.create({
immersive: true,
mapStyle: MapStyle.DARK_NAVIGATION,
panelTransparency: 0.85,
autoZoom: true, // 自动调整缩放级别
autoTilt: true, // 自动倾斜视角
nightMode: 'auto' // 根据时间自动切换
});
5.2 实时路况集成
HarmonyOS 6新增了实时路况图层API,可直接在路线上叠加拥堵信息:
// 实时路况图层(预览)
const trafficLayer = controller.addTrafficLayer({
autoRefresh: true,
refreshInterval: 60, // 60秒刷新
opacity: 0.7
});
5.3 多设备导航流转
HarmonyOS 6支持导航状态在手机与车机之间无缝流转:
// 导航流转(预览)
import { continuationManager } from '@kit.DistributedKit';
// 发起导航流转
continuationManager.startNavigationContinuation({
sessionId: this.navSessionId,
state: this.navStateMachine.getCurrentState(),
routeData: this.selectedRoute
});
六、总结
本文系统讲解了HarmonyOS路线规划与导航SDK的完整技术栈,从多策略路线对比到导航状态机、语音播报集成。核心要点回顾:
flowchart LR
A[路线规划与导航] --> B[多策略对比]
A --> C[导航状态机]
A --> D[语音播报]
A --> E[路线渲染]
B --> B1[并行请求3种策略]
B --> B2[差异化路线展示]
B --> B3[推荐路线标记]
C --> C1[5种状态转换]
C --> C2[偏航检测]
C --> C3[到达判定]
D --> D1[TTS引擎集成]
D --> D2[指令模板系统]
D --> D3[播报频率控制]
E --> E1[Douglas_Peucker简化]
E --> E2[分级渲染策略]
E --> E3[分段着色]
classDef coreStyle fill:#66BB6A,stroke:#2E7D32,color:#FFF,font-weight:bold
classDef subStyle fill:#A5D6A7,stroke:#66BB6A,color:#000
classDef leafStyle fill:#E8F5E9,stroke:#A5D6A7,color:#000
class A coreStyle
class B,C,D,E subStyle
class B1,B2,B3,C1,C2,C3,D1,D2,D3,E1,E2,E3 leafStyle
关键实践原则:
- 多策略并行:同时请求3种策略的路线,让用户选择最合适的
- 状态机驱动:导航过程必须用状态机管理,避免if-else地狱
- 路线简化:所有路线渲染前必须经过Douglas-Peucker简化
- 偏航阈值:城市道路20-30米,高速公路30-50米
- 语音节流:设置最小播报间隔,防止指令堆积
下一篇文章将深入讲解地图交互手势与事件处理,包括自定义手势、多点触控、地图快照等核心技术。
- 点赞
- 收藏
- 关注作者
评论(0)