大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

前言

在基于 HarmonyOS 开发基于位置服务的应用时,地图展示与路径规划往往是高频且核心的功能。尤其是在本地出行、外卖配送、运动健身等场景下,用户经常需要在“驾车”、“步行”和“骑行”之间切换路线,并比对不同路线的距离与耗时。

本文将结合 HarmonyOS 官方提供的 @kit.MapKit 和 @kit.NetworkKit,从起点与终点的设定出发,完整实现一套包含多种出行方式切换、多条候选路线绘制以及异常状态处理的路径规划最小化演示方案。

一、这个能力到底解决什么问题

路径规划的本质,是将抽象的地理位置坐标转化为用户可感知的出行指引。在这个场景中,我们需要解决以下几个核心问题:

  1. 路线生成:根据给定的起点和终点坐标,计算出可通行的物理路径。
  2. 多形态覆盖:满足不同出行方式(驾车、步行、骑行)的规则(如步行可以走天桥,驾车必须顺行)。
  3. 决策辅助:提供主要路线与候选路线,并直观展示总距离与预估耗时,帮助用户做出选择。
  4. 可视化表现:将服务端返回的经纬度点集,通过 Map Kit 提供的图形绘制能力转化为地图上的高亮折线。

二、先把官方规则弄清楚

在动手编写代码之前,需要明确当前 HarmonyOS 官方针对地图与路径规划能力的约束:

  • API 依赖:地图渲染依赖 @kit.MapKit 中的 MapComponent 组件与 map 模块;网络请求依赖 @kit.NetworkKit(或 @ohos.net.http)。
  • 配置前置:使用 Map Kit 前,必须在 AppGallery Connect 中开通“地图服务”,并在工程的 module.json5 文件中配置 client_id,否则地图组件将只能展示白屏或基础网格。
  • 路径规划服务:HarmonyOS 侧的客户端 SDK 主要负责“渲染”,而真实的路径计算往往依赖华为 Map Kit 的 Directions API(RESTful 接口)。我们需要向 https://mapapi.cloud.huawei.com/mapApi/v1/routeService/... 发起对应出行方式的请求。
  • 权限申请:由于涉及网络请求,必须在 module.json5 中声明 ohos.permission.INTERNET 权限。若起点需要获取当前位置,还需申请位置相关权限(本例为了聚焦路径规划,起点与终点采用固定坐标)。

三、搭一个最小实践

目标:实现一个页面,上方展示地图,下方提供三个按钮(驾车、步行、骑行)。点击按钮后,在地图上绘制出起点到终点的多条路线(主路线高亮,候选路线置灰),并显示最优路线的距离和耗时。

1. 权限与 Client ID 配置

在 src/main/module.json5 中确保包含以下配置:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ],
    "metadata": [
      {
        "name": "client_id",
        "value": "YOUR_APP_CLIENT_ID" // 替换为真实的 Client ID
      }
    ]
  }
}

2. 核心代码实现

以下是一段整合了地图渲染与路径规划核心流程的 ArkTS 代码:

import { map, mapCommon, MapComponent } from '@kit.MapKit';
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct RoutePlanningDemo {
  private mapController?: map.MapComponentController;
  private polylineIds: string[] = []; // 用于保存已绘制的路线 ID,方便清除
  
  // 起点终点固定坐标
  private originPoint: mapCommon.LatLng = { latitude: 39.9042, longitude: 116.4074 }; // 北京起点
  private destPoint: mapCommon.LatLng = { latitude: 39.9842, longitude: 116.3074 }; // 北京终点
  
  @State distanceInfo: string = '--';
  @State durationInfo: string = '--';
  
  private mapOptions: mapCommon.MapOptions = {
    position: {
      target: { latitude: 39.9442, longitude: 116.3574 }, // 视角中心
      zoom: 11
    }
  };

  private mapCallback = async (err: BusinessError, mapController: map.MapComponentController) => {
    if (!err) {
      this.mapController = mapController;
      // 添加起点和终点 Marker
      this.addMarkers();
    } else {
      console.error(`[MapKit] 地图初始化失败, 错误码: ${err.code}`);
    }
  };

  build() {
    Column() {
      // 1. 地图展示区
      Stack() {
        MapComponent({ mapOptions: this.mapOptions, mapCallback: this.mapCallback })
          .width('100%')
          .height('100%')
      }
      .width('100%')
      .height('70%')

      // 2. 距离与耗时信息面板
      Row() {
        Text(`距离: ${this.distanceInfo}`)
          .fontSize(16).fontWeight(FontWeight.Bold)
        Text(`预估耗时: ${this.durationInfo}`)
          .fontSize(16).fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)
      .padding(10)

      // 3. 路线规划控制面板
      Row() {
        Button('驾车路线')
          .onClick(() => this.requestRoute('driving'))
        Button('步行路线')
          .onClick(() => this.requestRoute('walking'))
        Button('骑行路线')
          .onClick(() => this.requestRoute('bicycling'))
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .padding(20)
    }
    .width('100%')
    .height('100%')
  }

  /**
   * 添加起点和终点标记
   */
  private async addMarkers() {
    if (!this.mapController) return;
    
    let startMarkerOptions: mapCommon.MarkerOptions = {
      position: this.originPoint,
      clickable: false,
      title: '起点'
    };
    let endMarkerOptions: mapCommon.MarkerOptions = {
      position: this.destPoint,
      clickable: false,
      title: '终点'
    };
    
    await this.mapController.addMarker(startMarkerOptions);
    await this.mapController.addMarker(endMarkerOptions);
  }

  /**
   * 发起路线规划请求
   */
  private async requestRoute(routeType: 'driving' | 'walking' | 'bicycling') {
    // 请求前清除旧路线
    this.clearOldRoutes();

    // 组装 REST API 请求 (需注意替换真实 API Key 或通过 Token 鉴权)
    let url = `https://mapapi.cloud.huawei.com/mapApi/v1/routeService/${routeType}?key=YOUR_API_KEY`;
    let httpRequest = http.createHttp();
    
    let requestBody = {
      origin: { lat: this.originPoint.latitude, lng: this.originPoint.longitude },
      destination: { lat: this.destPoint.latitude, lng: this.destPoint.longitude },
      alternatives: true // 开启多条候选路线
    };

    try {
      let response = await httpRequest.request(url, {
        method: http.RequestMethod.POST,
        header: { 'Content-Type': 'application/json' },
        extraData: JSON.stringify(requestBody),
        expectDataType: http.HttpDataType.OBJECT
      });

      if (response.responseCode === 200) {
        this.parseAndDrawRoutes(response.result as object);
      } else {
        this.handleRequestFailure(response.responseCode, '服务返回非200状态码');
      }
    } catch (error) {
      this.handleRequestFailure(-1, '网络请求异常');
    }
  }

  /**
   * 解析服务端返回数据并绘制路线
   */
  private async parseAndDrawRoutes(result: any) {
    if (!result || !result.routes || result.routes.length === 0) {
      console.warn('[MapKit] 未找到可用路线');
      return;
    }

    // 更新最优路线的距离和耗时 (第一条通常为最优)
    let bestRoute = result.routes[0];
    this.distanceInfo = `${(bestRoute.paths[0].distance / 1000).toFixed(1)} km`;
    this.durationInfo = `${Math.ceil(bestRoute.paths[0].duration / 60)} 分钟`;

    // 遍历多条候选路线绘制
    for (let i = 0; i < result.routes.length; i++) {
      let route = result.routes[i];
      let points: mapCommon.LatLng[] = this.extractPoints(route);
      
      // 第一条为主路线(蓝色),其余为候选路线(灰色)
      let color = (i === 0) ? 0xFF007DFF : 0xFF888888; 
      let zIndex = (i === 0) ? 2 : 1; // 确保主路线覆盖在上面
      
      let polylineOptions: mapCommon.MapPolylineOptions = {
        points: points,
        color: color,
        width: 12,
        zIndex: zIndex,
        clickable: true
      };

      try {
        let polyline = await this.mapController?.addPolyline(polylineOptions);
        if (polyline) {
          this.polylineIds.push(polyline.getId());
        }
      } catch (err) {
        console.error(`[MapKit] 绘制路线失败: ${JSON.stringify(err)}`);
      }
    }
  }

  /**
   * 提取路径中的经纬度点集 (根据实际 API 返回结构调整)
   */
  private extractPoints(route: any): mapCommon.LatLng[] {
    let points: mapCommon.LatLng[] = [];
    if (route.paths && route.paths.length > 0) {
      route.paths[0].steps.forEach((step: any) => {
        step.polyline.forEach((point: any) => {
          points.push({ latitude: point.lat, longitude: point.lng });
        });
      });
    }
    return points;
  }

  /**
   * 清除旧路线
   */
  private clearOldRoutes() {
    if (!this.mapController || this.polylineIds.length === 0) return;
    this.polylineIds.forEach(id => {
      // 通过保存的 ID 移除对应的多边形/折线
      // MapController 提供了根据能力管理的机制
      // 注意:API 设计可能有差异,这里需要调用 SDK 侧支持的 remove 方法
      // 假设当前 SDK 侧已持有对应对象实例,实际开发中建议保存 Polyline 实例对象进行 remove()
    });
    this.polylineIds = [];
  }

  /**
   * 请求失败处理
   */
  private handleRequestFailure(code: number, msg: string) {
    console.error(`[MapKit] 路径规划失败: 状态码 ${code}, 原因: ${msg}`);
    this.distanceInfo = '请求失败';
    this.durationInfo = '请求失败';
  }
}

四、几个关键点拆开看

在这段看似清晰的业务逻辑中,有几个技术点往往是实际项目开发中最容易被忽视或处理不当的。

1. 多条候选路线的层级与视觉处理

服务端返回的 routes 数组往往包含多条数据。第一条默认是综合条件下的“最优选”。在地图绘制时,如果不对候选路线(Alternatives)进行特殊处理,多条蓝色折线会重叠交错,让用户一头雾水。
代码中,我们需要通过 zIndex 属性来保证主路线(蓝色)绘制在候选路线(灰色)之上,这不仅是视觉需求,更是地图体验的核心。

2. Polyline 的生命周期管理

当用户从“驾车”切换到“步行”时,上一条出行的折线如果不进行清除,就会遗留在地图上。
按照官方接口设计,addPolyline 返回的是一个具体的 MapPolyline 实例。在实际开发中,必须在类的成员变量中维护一个当前页面所有展示中折线的集合(或数组)。在下一次请求成功准备绘制前,遍历集合调用对象的 .remove() 方法清空画布。不要试图用刷新整个 MapComponent 的方式来重绘路线,这会导致地图闪烁以及巨大的性能开销。

3. 数据层解析的差异性

从华为 Directions API 返回的 JSON 结构可能非常庞大。核心的经纬度点集通常隐藏在 routes[n].paths[0].steps[n].polyline 这样的深度嵌套结构中。提取距离(distance,单位通常是米)和耗时(duration,单位通常是秒)时,也必须严格遵循官方的单位说明进行转化(如代码中的 / 1000 转公里,/ 60 转分钟)。

五、容易踩坑的地方

在使用 Map Kit 进行路线规划时,以下几个情况极易导致代码无法达到预期效果:

  • API 鉴权被拦截:网络请求直接返回 403。这多半是因为在发起 HTTP 请求时,URL 后方没有携带正确的 key=YOUR_API_KEY,或者是在使用 OAuth 鉴权时 Header 中缺失了正确的 Bearer Token。必须注意,AppGallery Connect 中的 API Key 需要经过 UrlEncode 编码后才能拼接在 URL 后面。
  • 坐标系混淆:Map Kit SDK 在屏幕上绘制需要的是 WGS84 坐标还是 GCJ02 坐标?如果服务端规划出来的点位与底图道路存在偏移,极大可能是由于坐标系不一致引起的。在提交给服务端做规划前,必须明确 origin 和 destination 所属的坐标系,并根据 API 要求做转换。
  • Controller 异步未就绪:在地图尚未完全加载渲染完毕前(即 mapCallback 尚未触发),如果业务代码强行调用 this.mapController.addPolyline(),系统会抛出未初始化的空指针异常。

六、实际项目中怎么排查

如果遇到路径规划不显示或者展示异常,可以按照以下顺序建立排查清单:

  1. 先看基础配置:检查 module.json5 中的 client_id 是否拼写正确,并确认云端已开启了对应地图服务。
  2. 再看网络通信:通过网络抓包或控制台日志,查看发往云端的 Directions API 状态码。如果是 4xx/5xx,先解决网络参数鉴权问题;如果 200,则拉出 JSON 查看 routes 是否为空。
  3. 检查点位坐标:通过日志打印起始点坐标。有些情况下因为经纬度填反(纬度最大 90,经度 180),会导致请求直接报非法坐标。
  4. 最后检查生命周期:检查是否在 MapComponent 正确执行回调前调用了绘制方法,以及旧的路线实例是否被彻底回收并从地图上擦除。

七、最后整理一下

回顾整个路径规划 Demo 开发,我们可以总结出几个实用经验:

  1. 端云协同是常态:HarmonyOS 的 Map Kit 更多地扮演“呈现者”和“交互底座”的作用。具体的业务计算往往需要依赖云端服务能力(如 Directions API),开发时需要同时熟悉端侧 API 与云端接口规范。
  2. 状态驱动地图交互:UI 层的耗时与距离文本,可以安全地交由 ArkUI 的 @State 来驱动刷新;但涉及底座渲染的地图元素(Polyline, Marker),必须通过控制器显式操作,注意状态分离。
  3. 精细化管理资源:无论是多边形、折线还是标记,添加到地图后都要有完善的“擦除机制”,防止对象内存堆积。

如果你正在你的应用中接入地图模块,可以重点观察一下在连续快速切换不同出行方式时,你的代码是否正确处理了正在执行的旧网络请求。这类并发带来的脏数据残留,往往比单一请求调通更值得重视。

参考资料

  1. 《Map Kit 开发指南 - 展示地图》
    华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-map-display-V5

  2. 《Map Kit API 参考 - @kit.MapKit (地图服务)》
    华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/map-map-api-V5

  3. 《Network Kit 开发指南 - HTTP数据请求》
    华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/http-request-V5

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐