【Harmony OS 6】地图操作系列-路程规划

背景

实现两个地点之间的路程规划,结合上篇添加标签(Marker)的文章来实现一下的需求:

  • 提供地点的选择。
  • 根据两个地点,计算路线规划,包括驾车、步行、骑行、公共交通四种交通类型选择。
  • 展示不同交通方式需要的距离和时间。

实现如下图的操作效果:

关键代码分析

计算驾车路径

DrivingRouteParams的关键属性:

  • origins:道路绑定的起点坐标列表,最后一个坐标是真正的出发地。这里不太明白起点可以是多个的使用场景在什么地方。
  • destination:目的地,通过latitude(纬度)和longitude(经度)来设置目的地的坐标,通过matchType属性来绑定路类型,默认限制绑路。起点避免绑定封闭、施工、限行、偏好设置所避免的道路,终点避免绑定高速、轮渡、 偏好设置所避免的道路。
  • optimize:是否对途径点进行优化。
arkts 复制代码
  /**
   * 计算驾车路径
   * @param start 开始经纬度
   * @param end 结束经纬度
   * @param wayPoint 途径点
   * @returns 驾车路径结果
   */
  public static async DrivingRoute(start: mapCommon.LatLng,
    end: mapCommon.LatLng, wayPoint: mapCommon.LatLng[]): Promise<GetRouteResult> {
    let getDrivingResult: GetRouteResult = new GetRouteResult(RouterType.Driving);

    let drivingParams: navi.DrivingRouteParams = {
      origins: [{
        latitude: start.latitude,
        longitude: start.longitude
      }],
      destination: {
        latitude: end.latitude,
        longitude: end.longitude
      },
      language: "zh_CN",
      avoids: [0],
      //途径点
      waypoints: wayPoint,
      //途径点类型。false:经停点,将分段下发路线信息。true:经过点,将整段下发路线信息。
      isViaType: true,
      //是否对途经点进行优化。true:进行途经点优化
      optimize: true,
      //是否返回多条规划路线结果。false:返回单条规划路线结果
      alternatives: false,
      //预计出发时间。时间戳。必须是当前或者未来时间,不能是过去时间。0为当前时间处理
      departAt: 0,
      //事故华北预估模型。0:智能预测;1:路况差于历史平均水平;2:路况优于历史平均水平。
      trafficMode: 0
    }
    try {
      const drivingResult: navi.RouteResult = await navi.getDrivingRoutes(drivingParams);
      if (drivingResult.routes.length == 0) {
        throw new Error("获取驾驶信息出现错误. ");
      }
      MapRouterUtils.SetResult(getDrivingResult, drivingResult.routes[0]);
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`获取驾驶信息出现错误. 错误代码:${err.code}, 错误信息: ${err.message}`);
    }
    return getDrivingResult;
  }

计算步行路径

关键属性:

  • origins:道路绑定的起点坐标列表,最后一个坐标是真正的出发地。这里不太明白起点可以是多个的使用场景在什么地方。
  • destination:目的地,通过latitude(纬度)和longitude(经度)来设置目的地的坐标,通过matchType属性来绑定路类型,默认限制绑路。起点避免绑定封闭、施工、限行、偏好设置所避免的道路,终点避免绑定高速、轮渡、 偏好设置所避免的道路。
arkts 复制代码
  /**
   * 计算步行路径
   * @param start 开始经纬度
   * @param end 结束经纬度
   * @returns
   */
  public static async WalkingRoute(start: mapCommon.LatLng,
    end: mapCommon.LatLng): Promise<GetRouteResult> {

    let getWalkingResult: GetRouteResult = new GetRouteResult(RouterType.Walking);
    let walkingParams: navi.RouteParams = {
      origins: [start],
      destination: {
        latitude: end.latitude,
        longitude: end.longitude
      },
      language: "zh_CN",
      avoids: [0],
    }
    try {
      const walkingResult: navi.RouteResult = await navi.getWalkingRoutes(walkingParams);
      if (walkingResult.routes.length == 0) {
        throw new Error("获取步行信息出现错误. ");
      }

      MapRouterUtils.SetResult(getWalkingResult, walkingResult.routes[0]);
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`获取步行信息出现错误. 错误代码:${err.code}, 错误信息: ${err.message}`);
    }
    return getWalkingResult;
  }

计算骑行路径

和步行的传参方式一致

arkts 复制代码
  /**
   * 计算骑行路径
   * @param start
   * @param end
   * @returns
   */
  public static async CyclingRoute(start: mapCommon.LatLng,
    end: mapCommon.LatLng): Promise<GetRouteResult> {
    let getCyclingResult: GetRouteResult = new GetRouteResult(RouterType.Cycling);
    let cyclingParams: navi.RouteParams = {
      origins: [{
        latitude: start.latitude,
        longitude: start.longitude
      }],
      destination: {
        latitude: end.latitude,
        longitude: end.longitude
      },
      language: "zh_CN",
      avoids: [0],
    }
    try {
      const cyclingResult: navi.RouteResult = await navi.getCyclingRoutes(cyclingParams);
      if (cyclingResult.routes.length == 0) {
        throw new Error("获取骑行信息出现错误. ");
      }

      MapRouterUtils.SetResult(getCyclingResult, cyclingResult.routes[0]);
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`获取骑行信息出现错误. 错误代码:${err.code}, 错误信息: ${err.message}`);
    }
    return getCyclingResult;
  }

计算公共交通路径

还未印证正确性,因为发现途径点坐标会只有一个,已经提工单反馈官方修改,后续应该可以直接使用。

arkts 复制代码
  /**
   * 计算公共交通
   * @param context
   * @param start
   * @param end
   * @returns
   */
  public static async TransitRoute(context: Context, start: mapCommon.LatLng,
    end: mapCommon.LatLng): Promise<GetRouteResult> {
    let transitParams: navi.TransitRouteParams = {
      //起点
      origin: {
        longitude: start.longitude,
        latitude: start.latitude
      },
      //终点
      destination: {
        longitude: end.longitude,
        latitude: end.latitude
      },
      //自选设置
      // 0:推荐(默认值)。
      // 1:地铁优先。
      // 2:少走路。
      // 3:少中转。
      // 4:时间短。
      // 5:不坐地铁。
      preference: 0
    }
    let getTransitResult: GetRouteResult = new GetRouteResult(RouterType.Transit);
    try {
      const transitResult: navi.TransitRouteResult = await navi.getTransitRoutes(context, transitParams);
      if (!transitResult.routes || transitResult.routes.length == 0) {
        throw new Error("获取公交信息出现错误. ");
      }

      if (transitResult.routes) {
        const route: navi.TransitRoute =
          transitResult.routes.sort((a, b) => ((a.busSortInfo as navi.BusSortInfo).totalCost as number) -
            ((b.busSortInfo as navi.BusSortInfo).totalCost as number))[0];
        getTransitResult.Duration = route.busSortInfo?.totalCost;
        getTransitResult.DurationDescription = DateUtils.FormatDuration(route.busSortInfo?.totalCost as number);
        let distance: number = 0;
        route.sections.forEach((section, index) => {
          let wapPoints: mapCommon.LatLng[] = []
          if (section.type == "pedestrian" && section.pedestrianSection) {
            wapPoints = section.pedestrianSection.polyline as mapCommon.LatLng[]
            distance += (section.pedestrianSection.travelSummary as navi.BaseSummary).length;
          } else if (section.type == "transit" && section.transitSection) {
            wapPoints = section.transitSection.polyline as mapCommon.LatLng[]
            distance += (section.transitSection.travelSummary as navi.BaseSummary).length;
          }
          getTransitResult.WayPoint = getTransitResult.WayPoint.concat(wapPoints);
        })
        getTransitResult.Distance = distance;
        getTransitResult.DistanceDescription = `${(distance / 1000).toFixed(2)}公里`

      }
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`获取公交信息出现错误. 错误代码:${err.code}, 错误信息: ${err.message}`);
    }
    return getTransitResult;
  }

完整代码

GetRouteResult.ets

包含了交通规划类型

arkts 复制代码
import { map, mapCommon } from "@kit.MapKit"

/**
 * 路径规划类
 */
export class GetRouteResult {
  /**
   * 路径规划类型
   */
  Type: RouterType = RouterType.Driving
  /**
   * 路径规划类型名字
   */
  TypeName: string = ""
  /**
   * 距离
   */
  Distance?: number = 0
  /**
   * 距离描述
   */
  DistanceDescription?: string = ""
  /**
   * 持续时间。秒
   */
  Duration?: number = 0
  /**
   * 持续时间描述
   */
  DurationDescription?: string = ""
  /**
   * 途径点,包括起终点
   */
  WayPoint: mapCommon.LatLng[] = []

  constructor(routerType: RouterType) {
    this.Type = routerType;
    switch (routerType) {
      case RouterType.Driving:
        this.TypeName = "驾驶";
        break;
      case RouterType.Walking:
        this.TypeName = "步行";
        break;
      case RouterType.Cycling:
        this.TypeName = "骑行";
        break;
      case RouterType.Transit:
        this.TypeName = "公共交通";
        break;
    }
  }
}

/**
 * 交通规划类型
 */
export enum RouterType {
  /**
   * 驾车
   */
  Driving,
  /**
   * 步行
   */
  Walking,
  /**
   * 骑行
   */
  Cycling,
  /**
   * 公共交通
   */
  Transit
}

DateUtils.ets

路线规划返回的时间单位为秒,公共交通的规划结果中没有直接给出时间的格式化文字,因此,需要写一个类来实现时间的转换。

arkts 复制代码
export class DateUtils {
  public static FormatDuration(seconds: number): string {
    if (seconds < 0) {
      return "0秒";
    }
    if (seconds === 0) {
      return "0秒";
    }
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const remainingSeconds = seconds % 60;
    // 根据不同的时间长度选择不同的显示方式
    if (hours > 0) {
      return `${hours}小时${minutes}分${remainingSeconds}秒`;
    } else if (minutes > 0) {
      return `${minutes}分${remainingSeconds}秒`;
    } else {
      return `${remainingSeconds}秒`;
    }
  }
}

MapRouterUtils.ets

路径规划类封装

arkts 复制代码
import { GetRouteResult, RouterType } from "../Models/GetRouterResult";
import { mapCommon, navi } from "@kit.MapKit";
import { BusinessError } from '@kit.BasicServicesKit';
import { DateUtils } from "./DateUsils";

export class MapRouterUtils {
  /**
   * 根据起点和终点数据规划路线
   * @param start
   * @param end
   * @returns
   */
  public static async CalculateRouter(context: Context, start: mapCommon.LatLng,
    end: mapCommon.LatLng): Promise<GetRouteResult[]> {
    let result: GetRouteResult[] = [];
    //驾驶计算
    const drivingResult = await MapRouterUtils.DrivingRoute(start, end, []);
    //步行计算
    const walkingResult = await MapRouterUtils.WalkingRoute(start, end);
    //骑行计算
    const cyclingResult = await MapRouterUtils.CyclingRoute(start, end);
    //计算公交出行时间
    const transitResult = await MapRouterUtils.TransitRoute(context, start, end);
    result = [drivingResult, walkingResult, cyclingResult, transitResult]
    return result;
  }

  /**
   * 根据起点和终点获得一种数据
   * @param context
   * @param start
   * @param end
   * @returns
   */
  public static async SimpleCalculateRouter(context: Context, start: mapCommon.LatLng,
    end: mapCommon.LatLng, routeType: RouterType): Promise<GetRouteResult> {
    let result: GetRouteResult = new GetRouteResult(routeType);
    switch (routeType) {
      case RouterType.Driving:
        result = await MapRouterUtils.DrivingRoute(start, end, []);
        break;
      case RouterType.Walking:
        result = await MapRouterUtils.WalkingRoute(start, end);
        break;
      case RouterType.Cycling:
        result = await MapRouterUtils.CyclingRoute(start, end);
        break;
      case RouterType.Transit:
        result = await MapRouterUtils.TransitRoute(context, start, end);
        break;
    }
    return result;
  }

  /**
   * 计算驾车路径
   * @param start 开始经纬度
   * @param end 结束经纬度
   * @param wayPoint 途径点
   * @returns 驾车路径结果
   */
  public static async DrivingRoute(start: mapCommon.LatLng,
    end: mapCommon.LatLng, wayPoint: mapCommon.LatLng[]): Promise<GetRouteResult> {
    let getDrivingResult: GetRouteResult = new GetRouteResult(RouterType.Driving);

    let drivingParams: navi.DrivingRouteParams = {
      origins: [{
        latitude: start.latitude,
        longitude: start.longitude
      }],
      destination: {
        latitude: end.latitude,
        longitude: end.longitude
      },
      language: "zh_CN",
      avoids: [0],
      //途径点
      waypoints: wayPoint,
      //途径点类型。false:经停点,将分段下发路线信息。true:经过点,将整段下发路线信息。
      isViaType: true,
      //是否对途经点进行优化。true:进行途经点优化
      optimize: true,
      //是否返回多条规划路线结果。false:返回单条规划路线结果
      alternatives: false,
      //预计出发时间。时间戳。必须是当前或者未来时间,不能是过去时间。0为当前时间处理
      departAt: 0,
      //事故华北预估模型。0:智能预测;1:路况差于历史平均水平;2:路况优于历史平均水平。
      trafficMode: 0
    }
    try {
      const drivingResult: navi.RouteResult = await navi.getDrivingRoutes(drivingParams);
      if (drivingResult.routes.length == 0) {
        throw new Error("获取驾驶信息出现错误. ");
      }
      MapRouterUtils.SetResult(getDrivingResult, drivingResult.routes[0]);
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`获取驾驶信息出现错误. 错误代码:${err.code}, 错误信息: ${err.message}`);
    }
    return getDrivingResult;
  }

  /**
   * 计算步行路径
   * @param start 开始经纬度
   * @param end 结束经纬度
   * @returns
   */
  public static async WalkingRoute(start: mapCommon.LatLng,
    end: mapCommon.LatLng): Promise<GetRouteResult> {

    let getWalkingResult: GetRouteResult = new GetRouteResult(RouterType.Walking);
    let walkingParams: navi.RouteParams = {
      origins: [start],
      destination: {
        latitude: end.latitude,
        longitude: end.longitude
      },
      language: "zh_CN",
      avoids: [0],
    }
    try {
      const walkingResult: navi.RouteResult = await navi.getWalkingRoutes(walkingParams);
      if (walkingResult.routes.length == 0) {
        throw new Error("获取步行信息出现错误. ");
      }

      MapRouterUtils.SetResult(getWalkingResult, walkingResult.routes[0]);
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`获取步行信息出现错误. 错误代码:${err.code}, 错误信息: ${err.message}`);
    }
    return getWalkingResult;
  }

  /**
   * 计算骑行路径
   * @param start
   * @param end
   * @returns
   */
  public static async CyclingRoute(start: mapCommon.LatLng,
    end: mapCommon.LatLng): Promise<GetRouteResult> {
    let getCyclingResult: GetRouteResult = new GetRouteResult(RouterType.Cycling);
    let cyclingParams: navi.RouteParams = {
      origins: [{
        latitude: start.latitude,
        longitude: start.longitude
      }],
      destination: {
        latitude: end.latitude,
        longitude: end.longitude
      },
      language: "zh_CN",
      avoids: [0],
    }
    try {
      const cyclingResult: navi.RouteResult = await navi.getCyclingRoutes(cyclingParams);
      if (cyclingResult.routes.length == 0) {
        throw new Error("获取骑行信息出现错误. ");
      }

      MapRouterUtils.SetResult(getCyclingResult, cyclingResult.routes[0]);
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`获取骑行信息出现错误. 错误代码:${err.code}, 错误信息: ${err.message}`);
    }
    return getCyclingResult;
  }

  /**
   * 计算公共交通
   * @param context
   * @param start
   * @param end
   * @returns
   */
  public static async TransitRoute(context: Context, start: mapCommon.LatLng,
    end: mapCommon.LatLng): Promise<GetRouteResult> {
    let transitParams: navi.TransitRouteParams = {
      //起点
      origin: {
        longitude: start.longitude,
        latitude: start.latitude
      },
      //终点
      destination: {
        longitude: end.longitude,
        latitude: end.latitude
      },
      //自选设置
      // 0:推荐(默认值)。
      // 1:地铁优先。
      // 2:少走路。
      // 3:少中转。
      // 4:时间短。
      // 5:不坐地铁。
      preference: 0
    }
    let getTransitResult: GetRouteResult = new GetRouteResult(RouterType.Transit);
    try {
      const transitResult: navi.TransitRouteResult = await navi.getTransitRoutes(context, transitParams);
      if (!transitResult.routes || transitResult.routes.length == 0) {
        throw new Error("获取公交信息出现错误. ");
      }

      if (transitResult.routes) {
        const route: navi.TransitRoute =
          transitResult.routes.sort((a, b) => ((a.busSortInfo as navi.BusSortInfo).totalCost as number) -
            ((b.busSortInfo as navi.BusSortInfo).totalCost as number))[0];
        getTransitResult.Duration = route.busSortInfo?.totalCost;
        getTransitResult.DurationDescription = DateUtils.FormatDuration(route.busSortInfo?.totalCost as number);
        let distance: number = 0;
        route.sections.forEach((section, index) => {
          let wapPoints: mapCommon.LatLng[] = []
          if (section.type == "pedestrian" && section.pedestrianSection) {
            wapPoints = section.pedestrianSection.polyline as mapCommon.LatLng[]
            distance += (section.pedestrianSection.travelSummary as navi.BaseSummary).length;
          } else if (section.type == "transit" && section.transitSection) {
            wapPoints = section.transitSection.polyline as mapCommon.LatLng[]
            distance += (section.transitSection.travelSummary as navi.BaseSummary).length;
          }
          getTransitResult.WayPoint = getTransitResult.WayPoint.concat(wapPoints);
        })
        getTransitResult.Distance = distance;
        getTransitResult.DistanceDescription = `${(distance / 1000).toFixed(2)}公里`

      }
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`获取公交信息出现错误. 错误代码:${err.code}, 错误信息: ${err.message}`);
    }
    return getTransitResult;
  }

  /**
   * 填写路线内容
   * @param getResult
   * @param route
   */
  private static SetResult(getResult: GetRouteResult, route: navi.Route) {
    const routeStep = route.steps[0];
    getResult.Distance = routeStep.distance;
    getResult.DistanceDescription = routeStep.distanceDescription;
    getResult.Duration = routeStep.duration;
    getResult.DurationDescription = routeStep.durationDescription;
    getResult.WayPoint = route.overviewPolyline as mapCommon.LatLng[]
  }
}

SelectRouteParam.ets

路线选择交互类

arkts 复制代码
import { GetRouteResult } from "./GetRouterResult";

@ObservedV2
export class SelectRouteParam {
  /**
   * 交通方式集合
   */
  @Trace RouteResult: GetRouteResult[] = []
  /**
   * 选择交通方式索引
   */
  @Trace SelectIndex: number = 0
  /**
   * 出发时间
   */
  @Trace DepartTime: Date = new Date();
  /**
   * 选择修改
   */
  @Trace SelectChange: (index: number) => void = (i) => {
    this.SelectIndex = i;
  }
}

PromptActionClass.ets

弹窗辅助类

arkts 复制代码
import { promptAction } from "@kit.ArkUI";
import { hilog } from "@kit.PerformanceAnalysisKit";
import { BusinessError } from "@kit.BasicServicesKit";

export class PromptActionClass {
  static ctx: UIContext;
  static ContentNode: ComponentContent<Object>;
  static Options: promptAction.BaseDialogOptions;

  static SetContext(context: UIContext) {
    PromptActionClass.ctx = context;
  }


  static SetContentNode(node: ComponentContent<Object>) {
    PromptActionClass.ContentNode = node;
  }


  static SetOptions(options: promptAction.BaseDialogOptions) {
    PromptActionClass.Options = options;
  }


  static OpenDialog() {
    if (PromptActionClass.ContentNode !== null) {
      PromptActionClass.ctx.getPromptAction()
        .openCustomDialog(PromptActionClass.ContentNode, PromptActionClass.Options)
        .then(() => {
          hilog.info(0xFF00, 'TAG', '打开弹窗已完成');
        })
        .catch((error: BusinessError) => {
          let message = (error as BusinessError).message;
          let code = (error as BusinessError).code;
          hilog.info(0xFF00, 'TAG', `弹窗打开失败,错误代码: ${code}, 错误信息:${message}`);
        })
    }
  }


  static CloseDialog(contentNode?: ComponentContent<Object>) {
    if (PromptActionClass.ContentNode !== null) {
      PromptActionClass.ctx.getPromptAction()
        .closeCustomDialog(contentNode)
        .then(() => {
          hilog.info(0xFF00, 'TAG', '关闭弹窗顺利运行');
        })
        .catch((error: BusinessError) => {
          let message = (error as BusinessError).message;
          let code = (error as BusinessError).code;
          hilog.info(0xFF00, 'TAG', `关闭弹窗错误,错误代码: ${code}, 错误信息:${message}`);
        })
    }
  }


  static UpdateDialog(options: promptAction.BaseDialogOptions) {
    if (PromptActionClass.ContentNode !== null) {
      PromptActionClass.ctx.getPromptAction()
        .updateCustomDialog(PromptActionClass.ContentNode, options)
        .then(() => {
          hilog.info(0xFF00, 'TAG', '更新弹窗顺利运行');
        })
        .catch((error: BusinessError) => {
          let message = (error as BusinessError).message;
          let code = (error as BusinessError).code;
          hilog.info(0xFF00, 'TAG', `更新弹窗出错,错误代码:${code}, 错误信息:${message}`);
        })
    }
  }
}

LocationUtil

用户定位信息辅助类

arkts 复制代码
import { common, Permissions } from "@kit.AbilityKit";
import { geoLocationManager } from "@kit.LocationKit";
import { map, mapCommon, sceneMap } from "@kit.MapKit";

export class LocationUtil {
  /**
   * 当前位置的维度
   */
  public static LocationLatitude: number = 39.9;
  /**
   * 当前位置的经度
   */
  public static LocationLongitude: number = 116.4;
  /**
   * 所需要得权限
   */
  public static MapPermissions: Permissions[] =
    [
      'ohos.permission.INTERNET',
      'ohos.permission.LOCATION',
      'ohos.permission.APPROXIMATELY_LOCATION'
    ]

  /**
   * 更新用户定位
   */
  public static async UpdateLocation() {
    // 获取用户位置坐标
    let location: geoLocationManager.Location = await geoLocationManager.getCurrentLocation();
    const convertPoint = map.convertCoordinateSync(mapCommon.CoordinateType.WGS84,
      mapCommon.CoordinateType.GCJ02, {
        longitude: location.longitude,
        latitude: location.latitude
      });
    LocationUtil.LocationLongitude = convertPoint.longitude
    LocationUtil.LocationLatitude = convertPoint.latitude;
  }

  /**
   * 选择地图
   * @param uiContext
   * @returns
   */
  public static async SelectLocation(uiContext: common.UIAbilityContext): Promise<sceneMap.LocationChoosingResult | undefined> {
    try {
      await LocationUtil.UpdateLocation();
      let locationChoosingOptions: sceneMap.LocationChoosingOptions = {
        // 地图中心点坐标
        location: { latitude: LocationUtil.LocationLatitude, longitude: LocationUtil.LocationLongitude },
        language: 'zh_cn',
        // 展示搜索控件
        searchEnabled: true,
        // 展示附近Poi
        showNearbyPoi: true
      };
      // 拉起地点选取页
      let result: sceneMap.LocationChoosingResult =
        await sceneMap.chooseLocation(uiContext, locationChoosingOptions);
      return result;
    } catch (e) {
      console.info(e)
      return undefined;
    }

  }

  public static async SelectLocationPoint(uiContext: common.UIAbilityContext,
    originLocationPoint: mapCommon.LatLng): Promise<mapCommon.LatLng | undefined> {
    try {
      let locationChoosingOptions: sceneMap.LocationChoosingOptions = {
        // 地图中心点坐标
        location: originLocationPoint,
        language: 'zh_cn',
        // 展示搜索控件
        searchEnabled: true,
        // 展示附近Poi
        showNearbyPoi: true
      };
      // 拉起地点选取页
      let result: sceneMap.LocationChoosingResult =
        await sceneMap.chooseLocation(uiContext, locationChoosingOptions);
      if (result) {
        return result.location;
      }
      return undefined;
    } catch (e) {
      console.error(`地点选择错误,错误代码:${e}`);
      return undefined;
    }
  }
}

PermissionUtils.ets

权限判断辅助类

arkts 复制代码
import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit';

/**
 *权限封装类
 */
export class PermissionUtils {
  /**
   * 检查权限是否授权(完全授权)
   * @param permissionsArr 权限集合
   * @returns true:已经全部授权;false:没有全部授权
   */
  public static CheckPermissions(permissionsArr: Permissions[]): boolean {
    const atManager = abilityAccessCtrl.createAtManager();
    //获取bundle信息
    const bundleInfo =
      bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    // 拿到当前应用的tokenID 标识
    const tokenID = bundleInfo.appInfo.accessTokenId
    //校验应用是否被授予权限
    let result: boolean = true;
    permissionsArr.forEach((x: Permissions, index: number) => {
      if (!(atManager.checkAccessTokenSync(tokenID, x) === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)) {
        result = false;
        return;
      }
    })
    return result;
  }

  /**
   * 申请授权(首次弹窗申请)
   * @param permissionList 权限集合
   * @returns true:权限完全加载;false:有权限没有加载
   */
  public static async RequestPermissions(permissionList: Permissions[]): Promise<boolean> {
    // 程序访问控制管理
    const atManager = abilityAccessCtrl.createAtManager();
    // 拉起弹框请求用户授权
    const grantStatus = await atManager.requestPermissionsFromUser(getContext(), permissionList)
    // 获取请求权限的结果
    const isAuth = grantStatus.authResults.every(v => v === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)
    // 返回 Promise 授权结果
    return isAuth ? Promise.resolve(true) : Promise.reject(false)
  }

  /**
   * 打开系统设置的权限管理页面
   */
  public static OpenPermissionSettingsPage() {
    // 获取上下文
    const context = getContext() as common.UIAbilityContext
    // 获取包信息
    const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
    // 打开系统设置页
    context.startAbility({
      bundleName: 'com.huawei.hmos.settings',
      abilityName: 'com.huawei.hmos.settings.MainAbility',
      uri: 'application_info_entry',
      parameters: {
        // 按照包名打开对应设置页
        pushParams: bundleInfo.name
      }
    })
  }
}

MapRouteViewModel.ets

页面交互类

arkts 复制代码
import { map, mapCommon } from "@kit.MapKit"
import { AsyncCallback } from "@kit.BasicServicesKit"
import { LocationUtil } from "../Utils/LocationUtil"
import { common, Permissions } from "@kit.AbilityKit"
import { geoLocationManager } from "@kit.LocationKit"
import { PermissionUtils } from "../Utils/PermissionUtils"
import { SelectRouteParam } from "../Models/SelectRouteParam"
import { ComponentContent } from "@kit.ArkUI"
import { PromptActionClass } from "../Utils/PromptActionClass"
import { GetRouteResult, RouterType } from "../Models/GetRouterResult"
import { MapRouterUtils } from "../Utils/MapRouterUtils"

@ObservedV2
export class MapRouteViewModel {
  /**
   * 地图初始化参数设置
   */
  MapOption?: mapCommon.MapOptions
  /**
   * 地图回调方法
   */
  MapCallBack?: AsyncCallback<map.MapComponentController>
  /**
   * 地图控制器
   */
  MapController?: map.MapComponentController
  /**
   * 地图监听管理器
   */
  MapEventManager?: map.MapEventManager
  /**
   * 所需要得权限
   */
  MapPermissions: Permissions[] =
    [
      'ohos.permission.INTERNET',
      'ohos.permission.LOCATION',
      'ohos.permission.APPROXIMATELY_LOCATION'
    ]
  /**
   * 当前位置的维度
   */
  @Trace LocationLatitude: number = 39.9;
  /**
   * 当前位置的经度
   */
  @Trace LocationLongitude: number = 116.4;
  MyUIContext?: UIContext
  /**
   * 标记builder
   */
  @Trace MarkerBuilder?: CustomBuilder
  /**
   * 标记集合
   */
  @Trace MarkerList: map.Marker[] = []
  /**
   * 路线规划集合
   */
  @Trace RouteResult?: SelectRouteParam;
  /**
   * 选择路径模拟器
   */
  @Trace SelectRouteType?: ComponentContent<object>

  constructor() {
    this.UpdateLocation()
    this.MapOption = {
      //相机位置
      position: {
        target: {
          latitude: this.LocationLatitude,
          longitude: this.LocationLongitude
        },
        zoom: 15
      },
      //地图类型
      mapType: mapCommon.MapType.STANDARD,
      //地图最小图层,默认值为2
      minZoom: 2,
      //地图最大图层,默认值为20
      maxZoom: 20,
      //是否支持旋转手势
      rotateGesturesEnabled: true,
      //是否支持滑动手势
      scrollGesturesEnabled: true,
      //是否支持缩放手势
      zoomGesturesEnabled: true,
      //是否支持倾斜手势
      tiltGesturesEnabled: true,
      //是否展示缩放控件
      zoomControlsEnabled: true,
      //是否展示定位按钮
      myLocationControlsEnabled: true,
      //是否展示指南针控件
      compassControlsEnabled: false,
      //是否展示比例尺
      scaleControlsEnabled: true,
      //是否一直显示比例尺,只有比例尺启用时该参数才生效。
      alwaysShowScaleEnabled: true
    };
    this.MapCallBack = async (err, mapController) => {
      if (!err) {
        this.MapController = mapController;
        //启用我的位置图层
        mapController.setMyLocationEnabled(true);
        //设置我的位置跟随设备移动
        mapController.setMyLocationStyle({
          displayType: mapCommon.MyLocationDisplayType.LOCATE
        })
        //启用我的位置按钮
        mapController.setMyLocationControlsEnabled(true);
        //地图监听时间管理器
        this.MapEventManager = this.MapController.getEventManager();
        this.MapEventManager.on("markerClick", (marker: map.Marker) => {
          this.MarkerClickCallBack(marker);
        });
      }
    }
  }

  public async Init(uiContext: UIContext, builder: WrappedBuilder<[SelectRouteParam]>,
    markerBuilder: CustomBuilder) {
    this.MyUIContext = uiContext;
    this.SelectRouteType = new ComponentContent(uiContext, builder, new SelectRouteParam());
    this.MarkerBuilder = markerBuilder;
    //识别权限是否赋予
    if (!PermissionUtils.CheckPermissions(this.MapPermissions)) {
      const perResult: boolean = await PermissionUtils.RequestPermissions(this.MapPermissions);
      if (!perResult) {
        console.error("用户未授权");
        return;
      }
    }
  }


  /**
   * 更新用户定位
   */
  public async UpdateLocation() {
    // 获取用户位置坐标
    try {
      let location: geoLocationManager.Location = await geoLocationManager.getCurrentLocation();
      const convertPoint = map.convertCoordinateSync(mapCommon.CoordinateType.WGS84,
        mapCommon.CoordinateType.GCJ02, {
          longitude: location.longitude,
          latitude: location.latitude
        });
      this.LocationLongitude = convertPoint.longitude;
      this.LocationLatitude = convertPoint.latitude;
    } catch (error) {
      console.error("更新用户坐标出错");
    }
  }

  /**
   * 在当前位置添加一个标记
   * @param point
   */
  public async CreateLocationMarker() {
    if (this.MapController) {
      // Marker初始化参数
      let markerOptions: mapCommon.MarkerOptions = {
        //定位点
        position: {
          latitude: this.LocationLatitude,
          longitude: this.LocationLongitude
        },
        clickable: true
      };
      try {
        let marker: map.Marker = await this.MapController.addMarker(markerOptions);
      } catch (error) {
        console.error(error)
      }
    }
  }

  /**
   * 选择地点,并生成标记
   */
  public async SelectedLocationCreateMarker() {
    if (this.MyUIContext && this.MapController) {
      await LocationUtil.UpdateLocation()
      let selectResult =
        await LocationUtil.SelectLocation(this.MyUIContext.getHostContext() as common.UIAbilityContext)
      if (selectResult) {
        // Marker初始化参数
        let markerOptions: mapCommon.MarkerOptions = {
          //定位点
          position: selectResult.location,
          iconBuilder: this.MarkerBuilder,
          clickable: true
        };
        try {
          let marker: map.Marker = await this.MapController.addMarker(markerOptions);
          marker.setTitle(selectResult.name)
          marker.setSnippet(selectResult.address)
          //如果不是第一个,会涉及到计算行程
          if (this.MarkerList.length != 0) {
            await this.SimpleCalculate(this.MarkerList[this.MarkerList.length-1].getPosition(), marker.getPosition());
          }
          this.MarkerList.push(marker);

        } catch (error) {
          console.error(error)
        }
      }
    }
  }

  /**
   * 移动视图相机
   * @param latitude 维度
   * @param longitude 经度
   */
  public async MoveCamera(latitude: number, longitude: number) {
    if (this.MapController) {
      //将视图移动到标点位置
      let nwPosition = map.newCameraPosition({
        target: {
          latitude: latitude,
          longitude: longitude
        },
        zoom: 18
      })
      // 以动画方式移动地图相机
      this.MapController.animateCamera(nwPosition, 1000);
    }
  }

  /**
   * 标记点击事件
   * @param marker
   */
  public async MarkerClickCallBack(marker: map.Marker) {
    let locationPoint: mapCommon.LatLng = marker.getPosition();
    this.MoveCamera(locationPoint.latitude, locationPoint.longitude);
  }

  /**
   * 弹出路径选择页面
   * @param modelIndex
   */
  public async ShowSelectRoute(modelIndex: number) {
    if (this.SelectRouteType && this.MyUIContext) {
      this.SelectRouteType.update(this.RouteResult)
      PromptActionClass.SetContext(this.MyUIContext);
      PromptActionClass.SetOptions({
        alignment: DialogAlignment.Bottom
      })
      PromptActionClass.SetContentNode(this.SelectRouteType);
      PromptActionClass.OpenDialog();
      //说明是之前的简单计算,需要重新计算一下
      if (this.RouteResult && this.RouteResult.RouteResult.length == 1) {
        const param = this.MarkerList[modelIndex];
        const nextModel = this.MarkerList[modelIndex+1];
        await this.Calculate(param.getPosition(), nextModel.getPosition());
        //更新页面
        this.SelectRouteType.update(this.RouteResult);
      }
    }
  }

  /**
   * 计算全部路径
   * @param start
   * @param end
   */
  public async Calculate(start: mapCommon.LatLng, end: mapCommon.LatLng) {
    let selectRoute: SelectRouteParam = new SelectRouteParam();
    if (this.MyUIContext) {
      const routeResults: GetRouteResult[] =
        await MapRouterUtils.CalculateRouter(this.MyUIContext.getHostContext() as common.Context, start, end)
      selectRoute.RouteResult = routeResults;
      selectRoute.SelectIndex = 0;
      selectRoute.SelectChange = (index: number) => {
        this.RouteTypeSelectChange(selectRoute, index);
      }
    }
    this.RouteResult = selectRoute;
  }

  /**
   * 简单计算(用于快速生成)
   * @param start 开始地点
   * @param end 结束地点
   * @returns
   */
  public async SimpleCalculate(start: mapCommon.LatLng, end: mapCommon.LatLng) {
    let selectRoute: SelectRouteParam = new SelectRouteParam();
    if (this.MyUIContext) {
      const routeResult: GetRouteResult =
        await MapRouterUtils.SimpleCalculateRouter(this.MyUIContext.getHostContext() as common.Context,
          start,
          end, RouterType.Driving)
      selectRoute.RouteResult = [routeResult];
      selectRoute.SelectIndex = 0;
      selectRoute.SelectChange = (index: number) => {
        this.RouteTypeSelectChange(selectRoute, index);
      }
    }
    this.RouteResult = selectRoute;
  }

  /**
   * 交通方式选择
   * @param index
   */
  public async RouteTypeSelectChange(selectRoute: SelectRouteParam, index: number) {
    selectRoute.SelectIndex = index;
    this.RouteResult = selectRoute;
    this.SelectRouteType?.update(selectRoute);
    setTimeout(() => {
      PromptActionClass.CloseDialog(this.SelectRouteType)
    }, 50)
  }
}

MapRoutePage.ets

主要显示页面

arkts 复制代码
import { GetRouteResult, RouterType } from '../Models/GetRouterResult'
import { SelectRouteParam } from '../Models/SelectRouteParam'
import { MapRouteViewModel } from '../ViewModels/MapRouteViewModel'
import { map, MapComponent } from '@kit.MapKit';

@Entry
@ComponentV2
struct MapRoutePage {
  @Local VM: MapRouteViewModel = new MapRouteViewModel()

  aboutToAppear(): void {
    this.VM.Init(this.getUIContext(), wrapBuilder(SelectRouteBuilder), () => {
      this.MyMarkerBuilder(this.VM.MarkerList.length);
    });
  }

  build() {
    RelativeContainer() {
      MapComponent({
        mapOptions: this.VM.MapOption,
        mapCallback: this.VM.MapCallBack
      })
        .width("100%")
        .height("350")
        .id("Map")
      Column({ space: 10 }) {
        List({ space: 10 }) {
          ForEach(this.VM.MarkerList, (model: map.Marker, modelIndex: number) => {
            ListItem() {
              Column({ space: 10 }) {
                Column({ space: 8 }) {
                  Row() {
                    Image($r('app.media.startIcon'))
                      .width(53)
                      .height(23)
                    Text(model.getTitle())
                      .fontSize(18)
                      .layoutWeight(1)
                      .maxLines(1)
                      .textOverflow({ overflow: TextOverflow.Ellipsis })
                  }
                  .alignItems(VerticalAlign.Center)
                  .width("100%")

                  Row({ space: 2 }) {
                    SymbolGlyph($r('sys.symbol.local'))
                      .fontSize(14)
                    Text(model.getSnippet())
                      .fontSize(14)
                      .maxLines(1)
                      .textOverflow({ overflow: TextOverflow.Ellipsis })
                      .layoutWeight(1)
                  }
                  .padding({ left: 12 })
                  .alignItems(VerticalAlign.Center)
                  .width("100%")
                }
                .alignItems(HorizontalAlign.Start)
                .padding(10)
                .border({
                  width: 2,
                  color: $r('app.color.Main_Color'),
                  radius: 10
                })
                .backgroundColor($r('app.color.white'))
                .width("100%")

                Row() {
                  Button() {
                    Row({ space: 5 }) {
                      SymbolGlyph($r('sys.symbol.car'))
                        .fontWeight(FontWeight.Medium)
                        .fontSize(14)
                        .fontColor([$r('app.color.Main_Color')])
                      Text(`${this.VM.RouteResult?.RouteResult[this.VM.RouteResult.SelectIndex]?.DurationDescription} / ${this.VM.RouteResult?.RouteResult[this.VM.RouteResult.SelectIndex]?.DistanceDescription} `)
                        .fontSize(14)
                        .fontWeight(FontWeight.Medium)
                        .fontColor($r('app.color.Main_Color'))
                      Text(">> More")
                        .fontWeight(FontWeight.Medium)
                        .fontSize(10)
                        .fontColor($r('app.color.Main_Color'))
                    }
                    .alignItems(VerticalAlign.Center)
                  }
                  .type(ButtonType.Normal)
                  .backgroundColor($r('app.color.white'))
                  .onClick(() => {
                    if (this.VM.RouteResult) {
                      this.VM.ShowSelectRoute(modelIndex)
                    }
                  })
                }
                .padding({ left: 10, right: 10 })
                .width("100%")
                .justifyContent(FlexAlign.Start)
                .visibility(this.VM.RouteResult && this.VM.MarkerList.length - 1 != modelIndex ? Visibility.Visible :
                  Visibility.None)
              }
            }
            .width("100%")
            .backgroundColor(Color.White)
          })
        }
        .width("100%")
        .height("100%")
        .margin({ top: 10, bottom: 10 })
      }
      .alignRules({
        top: { anchor: "Map", align: VerticalAlign.Bottom }
      })
      .padding({ top: 10, left: 10, right: 10 })
      .width("100%")

      Button() {
        SymbolGlyph($r('sys.symbol.plus'))
          .fontColor([Color.White])
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
      }
      .id("PlusButton")
      .type(ButtonType.Circle)
      .width(50)
      .height(50)
      .backgroundColor($r('app.color.Main_Color'))
      .margin({ bottom: 20 })
      .alignRules({
        bottom: { anchor: "__container__", align: VerticalAlign.Bottom },
        middle: { anchor: "__container__", align: HorizontalAlign.Center }
      })
      .bindMenu([
        {
          value: "添加行程",
          action: () => {
            this.VM.SelectedLocationCreateMarker()
          }
        }
      ], {
        enableArrow: true,
        placement: Placement.Top
      })
    }
    .height('100%')
    .width('100%')
  }

  @Builder
  MyMarkerBuilder(index: number) {
    RelativeContainer() {
      Image($r('app.media.app_icon'))
        .width(30)
        .height(30)
        .syncLoad(true)
      Text(`${index}`)
        .fontSize(10)
        .width(15)
        .textAlign(TextAlign.Center)
        .height(15)
        .backgroundColor($r('app.color.Main_Color'))
        .borderRadius(7.5)
        .alignRules({
          bottom: { anchor: "__container__", align: VerticalAlign.Bottom },
          right: { anchor: "__container__", align: HorizontalAlign.End }
        })
    }
    .width(30)
    .height(30)
  }
}

@Builder
function SelectRouteBuilder(param: SelectRouteParam) {
  Column({ space: 10 }) {
    Row() {
      Text("选择交通方式")
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      Text(`${param.DepartTime.getHours().toString().padStart(2, '0')}:${param.DepartTime.getMinutes()
        .toString()
        .padStart(2, '0')} 出发`)
        .fontSize(12)
        .fontColor("#9e9e9e")
        .fontWeight(FontWeight.Bold)
    }
    .justifyContent(FlexAlign.SpaceBetween)
    .width("100%")
    .margin({ top: 10 })

    Column() {
      List() {
        ForEach(param.RouteResult, (item: GetRouteResult, index) => {
          ListItem() {
            RelativeContainer() {
              SymbolGlyph(item.Type == RouterType.Driving ? $r('sys.symbol.car') :
                item.Type == RouterType.Walking ? $r('sys.symbol.figure_run') :
                  item.Type == RouterType.Cycling ? $r('sys.symbol.bicycle') :
                    $r('sys.symbol.bus_fill'))
                .fontSize(20)
                .fontColor([param.SelectIndex == index ? Color.Black : "#9e9e9e"])
                .alignRules({
                  center: { anchor: "__container__", align: VerticalAlign.Center },
                  left: { anchor: "__container__", align: HorizontalAlign.Start }
                })
                .id("Icon")

              Text(`${item.TypeName} ${item.DistanceDescription} | ${item.DurationDescription}`)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor(param.SelectIndex == index ? Color.Black : "#9e9e9e")
                .margin({ left: 10 })
                .alignRules({
                  center: { anchor: "__container__", align: VerticalAlign.Center },
                  left: { anchor: "Icon", align: HorizontalAlign.End }
                })
                .id("Text")

              Button() {
                Row() {
                  SymbolGlyph($r('sys.symbol.paperplane_fill'))
                    .fontSize(20)
                    .fontColor([Color.White])
                }
                .justifyContent(FlexAlign.Center)
                .alignItems(VerticalAlign.Center)
                .width("100%")
                .height("100%")
              }
              .type(ButtonType.Normal)
              .backgroundColor(Color.Black)
              .visibility(param.SelectIndex == index ? Visibility.Visible : Visibility.None)
              .borderRadius(15)
              .width(40)
              .height(30)
              .alignRules({
                center: { anchor: "__container__", align: VerticalAlign.Center },
                right: { anchor: "__container__", align: HorizontalAlign.End }
              })
              .id("JumpButton")
            }
            .padding({ left: 20, right: 20 })
            .border({
              radius: 20,
              width: 2,
              color: param.SelectIndex == index ? Color.Black : Color.Transparent
            })
            .height(54)
            .width("100%")
          }
          .onClick(() => {
            param.SelectChange(index);
          })
        })
      }
      .width("100%")
      .height("100%")
    }
    .width("100%")
    .layoutWeight(1)
    .visibility(param.RouteResult.length == 1 ? Visibility.None : Visibility.Visible)
    .animation({
      duration: 500
    })

    Column() {
      LoadingProgress()
        .color($r('app.color.Main_Color'))
        .width(100)
    }
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .width("100%")
    .layoutWeight(1)
    .visibility(param.RouteResult.length == 1 ? Visibility.Visible : Visibility.None)
    .animation({
      duration: 500
    })
  }
  .borderRadius({ topLeft: 20, topRight: 20 })
  .padding(10)
  .width("100%")
  .height(300)
  .backgroundColor(Color.White)
}

总结

  • 当前代码使用了新版本的弹窗类封装。
  • 为了方便写文章和主要突出两个地点间的交通规划的计算和选择,并没有实现多个地点的规划操作,读者可以自己手动修改。
相关推荐
行者9616 小时前
Flutter FloatingActionButton在OpenHarmony平台的深度适配与优化实践
flutter·harmonyos·鸿蒙
儿歌八万首17 小时前
鸿蒙自定义相机开发:Camera Kit
数码相机·华为·harmonyos·harmonyos app
行者9618 小时前
Flutter跨平台开发:OpenHarmony平台卡片翻转组件的优化实践
flutter·harmonyos·鸿蒙
小雨青年18 小时前
鸿蒙 HarmonyOS 6 | ArkUI (06):表单交互 TextInput、Toggle、Slider 与 Picker 选择器
华为·交互·harmonyos
行者9618 小时前
Flutter适配OpenHarmony:手势交互的深度优化与实战应用
flutter·交互·harmonyos·鸿蒙
行者9618 小时前
Flutter与OpenHarmony深度融合:跨平台日历组件性能优化与适配实践
flutter·harmonyos·鸿蒙
行者9618 小时前
Flutter适配鸿蒙:SnackBar组件实践与优化策略
flutter·harmonyos·鸿蒙
baobao熊18 小时前
【HarmonyOS 6】List之间实现拖拽排序
华为·list·harmonyos
AlbertZein1 天前
HarmonyOS一杯冰美式的时间 -- @Env
harmonyos