云计算百科
云计算领域专业知识百科平台

HarmonyOS智慧农业管理应用开发教程--高高种地--第15篇:地图导航与路线规划

第15篇:地图导航与路线规划

📚 本篇导读

在智慧农业管理中,农户经常需要从当前位置前往农田、查找周边的农资店、农机租赁点等服务设施。本篇教程将带你实现完整的地图导航和POI(兴趣点)搜索功能,让应用更加实用。

本篇将实现:

  • 🚗 驾车路线规划(从当前位置到目标地块)
  • 🗺️ 路线可视化展示(在地图上绘制导航路线)
  • 📍 POI周边搜索(农资店、农机租赁、加油站等)
  • 🎯 搜索结果展示(列表+地图标记)
  • 🌤️ 天气查询功能(基于当前位置)

🎯 学习目标

完成本篇教程后,你将掌握:

  • 如何集成高德地图导航SDK
  • 如何实现驾车路线规划功能
  • 如何在地图上绘制路线
  • 如何实现POI搜索功能
  • 如何处理搜索结果并在地图上展示

  • 📐 功能架构

    导航与搜索功能架构

    地图导航与搜索模块
    ├── 路线规划
    │ ├── RouteSearch(路线搜索服务)
    │ ├── 驾车路线计算
    │ ├── 路线结果处理
    │ └── 路线可视化(Polyline绘制)

    ├── POI搜索
    │ ├── PoiSearch(POI搜索服务)
    │ ├── 周边圆形搜索
    │ ├── 城市关键词搜索
    │ └── 搜索结果标记

    ├── 天气查询
    │ ├── WeatherSearch(天气搜索服务)
    │ ├── 实时天气查询
    │ └── 天气信息展示

    └── 交互功能
    ├── 点击地块触发导航
    ├── 搜索半径调整
    └── 搜索结果点击定位


    一、高德导航SDK集成

    1.1 依赖配置

    在项目的 oh-package.json5 中,我们已经添加了导航和搜索SDK:

    {
    "dependencies": {
    "@amap/amap_lbs_map3d": "^1.0.0", // 地图SDK
    "@amap/amap_lbs_search": "^1.0.0", // 搜索SDK(包含路线规划)
    "@amap/amap_lbs_navi": "^1.0.0" // 导航SDK
    }
    }

    说明:

    • @amap/amap_lbs_search:提供路线规划、POI搜索、天气查询等功能
    • @amap/amap_lbs_navi:提供实时导航功能(本篇重点是路线规划)

    1.2 导入必要的模块

    在 FieldMapPage.ets 中导入搜索相关的类:

    // 搜索SDK导入
    import {
    RouteSearch, // 路线搜索
    DriveRouteQuery, // 驾车路线查询
    FromAndTo, // 起点终点
    LatLonPoint, // 经纬度点
    OnRouteSearchListener, // 路线搜索监听器
    DriveRouteResult, // 驾车路线结果
    AMapException, // 异常码
    PoiSearch, // POI搜索
    PoiQuery, // POI查询
    OnPoiSearchListener, // POI搜索监听器
    PoiResult, // POI结果
    PoiItem, // POI项
    PoiSearchBound, // POI搜索范围
    WeatherSearch, // 天气搜索
    WeatherSearchQuery, // 天气查询
    LocalWeatherLive, // 实时天气
    LocalWeatherLiveResult,// 天气结果
    OnWeatherSearchListener // 天气监听器
    } from '@amap/amap_lbs_search';


    二、路线规划功能实现

    2.1 初始化路线搜索服务

    在页面的 aboutToAppear() 生命周期中初始化搜索服务:

    @Component
    export struct FieldMapPage {
    // 路线搜索相关
    private routeSearch: RouteSearch | null = null;
    private routePolyline: Polyline | null = null;
    @State showRouteDialog: boolean = false;
    @State routeDistance: string = '';
    @State routeTime: string = '';
    @State isRouteSearching: boolean = false;

    aboutToAppear(): void {
    // 初始化路线搜索服务
    try {
    const context = getContext(this) as Context;

    // 创建RouteSearch实例
    this.routeSearch = new RouteSearch(context);
    console.info('[FieldMapPage] RouteSearch初始化成功');

    } catch (error) {
    console.error('[FieldMapPage] 搜索服务初始化失败:', error);
    promptAction.showToast({
    message: '路线规划功能初始化失败',
    duration: 2000
    });
    }
    }
    }

    关键点:

    • RouteSearch 需要传入 Context 对象
    • 初始化失败时要给用户友好的提示

    2.2 实现路线规划方法

    创建一个方法来计算从当前位置到目标地块的路线:

    /**
    * 规划到指定地块的路线
    * @param fieldId 地块ID
    */

    private planRouteToField(fieldId: string): void {
    // 1. 检查是否有当前位置
    if (!this.currentLocationInfo ||
    !this.currentLocationInfo.latitude ||
    !this.currentLocationInfo.longitude) {
    promptAction.showToast({
    message: '无法获取当前位置,请确保已开启定位权限',
    duration: 2000
    });
    return;
    }

    // 2. 查找目标地块
    const field = this.fields.find(f => f.id === fieldId);
    if (!field || !field.latitude || !field.longitude) {
    promptAction.showToast({
    message: '地块位置信息不完整',
    duration: 2000
    });
    return;
    }

    // 3. 获取起点和终点坐标
    const startLat = this.currentLocationInfo.latitude;
    const startLon = this.currentLocationInfo.longitude;
    const endLat = field.latitude;
    const endLon = field.longitude;

    console.info(`[FieldMapPage] 规划路线: (${startLat},${startLon}) -> (${endLat},${endLon})`);

    // 4. 调用路线搜索
    this.searchDriveRoute(startLat, startLon, endLat, endLon);
    }

    /**
    * 搜索驾车路线
    */

    private searchDriveRoute(
    startLat: number,
    startLon: number,
    endLat: number,
    endLon: number
    ): void {
    if (!this.routeSearch || !globalAMap) {
    promptAction.showToast({
    message: '路线搜索服务不可用',
    duration: 2000
    });
    return;
    }

    try {
    console.info('[FieldMapPage] 开始路线搜索…');
    this.isRouteSearching = true;

    // 1. 创建起点和终点
    const startPoint = new LatLonPoint(startLat, startLon);
    const endPoint = new LatLonPoint(endLat, endLon);
    const fromAndTo = new FromAndTo(startPoint, endPoint);

    // 2. 创建驾车路线查询
    // mode参数: 0=速度优先, 1=费用优先, 2=距离优先, 3=不走高速
    const passPoints = new ArrayList<LatLonPoint>(); // 途经点(空)
    const avoidPolygons = new ArrayList<ArrayList<LatLonPoint>>(); // 避让区域(空)
    const query = new DriveRouteQuery(
    fromAndTo, // 起点终点
    0, // 模式:速度优先
    passPoints, // 途经点
    avoidPolygons, // 避让区域
    '' // 避让道路
    );

    // 3. 设置监听器
    const listener: OnRouteSearchListener = {
    // 驾车路线搜索回调
    onDriveRouteSearched: (result: DriveRouteResult | undefined, errorCode: number): void => {
    this.isRouteSearching = false;
    this.handleRouteResult(result, errorCode);
    },
    // 其他路线类型(不使用)
    onWalkRouteSearched: (): void => {},
    onRideRouteSearched: (): void => {},
    onBusRouteSearched: (): void => {}
    };

    // 4. 设置监听器并开始搜索
    this.routeSearch.setRouteSearchListener(listener);

    // 延时执行,确保监听器设置完成
    setTimeout(() => {
    if (this.routeSearch) {
    this.routeSearch.calculateDriveRouteAsyn(query);
    console.info('[FieldMapPage] 路线计算已启动');
    }
    }, 100);

    promptAction.showToast({
    message: '正在规划路线…',
    duration: 1500
    });

    } catch (error) {
    this.isRouteSearching = false;
    console.error('[FieldMapPage] 路线搜索失败:', error);
    promptAction.showToast({
    message: '路线规划失败',
    duration: 2000
    });
    }
    }

    代码说明:

    步骤说明
    1. 参数验证 检查当前位置和目标地块的坐标是否有效
    2. 创建查询对象 使用 DriveRouteQuery 创建驾车路线查询
    3. 设置监听器 通过 OnRouteSearchListener 接收搜索结果
    4. 异步搜索 调用 calculateDriveRouteAsyn() 开始搜索

    DriveRouteQuery参数说明:

    • mode=0:速度优先(推荐用于农业场景)
    • mode=1:费用优先(避开收费路段)
    • mode=2:距离优先(最短路径)
    • mode=3:不走高速(适合农村道路)

    2.3 处理路线搜索结果

    /**
    * 处理路线搜索结果
    */

    private handleRouteResult(result: DriveRouteResult | undefined, errorCode: number): void {
    console.info('[FieldMapPage] 路线搜索回调, errorCode:', errorCode);

    if (!globalAMap) {
    console.error('[FieldMapPage] 地图对象为空');
    return;
    }

    // 检查是否成功
    if (errorCode === AMapException.CODE_AMAP_SUCCESS && result) {
    console.info('[FieldMapPage] ✅ 路线搜索成功!');

    // 获取路线列表(通常返回多条路线,取第一条)
    const paths = result.getPaths();

    if (paths && paths.length > 0) {
    const path = paths[0]; // 取第一条路线

    // 获取路线信息
    const distance = path.getDistance(); // 距离(米)
    const duration = path.getDuration(); // 时长(秒)

    console.info(`[FieldMapPage] 路线信息 – 距离:${distance}米, 时长:${duration}`);

    // 格式化距离显示
    this.routeDistance = distance > 1000 ?
    `${(distance / 1000).toFixed(2)}公里` :
    `${distance.toFixed(0)}`;

    // 格式化时间显示
    this.routeTime = duration > 3600 ?
    `${Math.floor(duration / 3600)}小时${Math.floor((duration % 3600) / 60)}分钟` :
    `${Math.floor(duration / 60)}分钟`;

    // 绘制路线
    const steps = path.getSteps(); // 获取路线分段
    this.drawRoute(steps);

    // 显示路线信息对话框
    this.showRouteDialog = true;

    promptAction.showToast({
    message: `路线规划成功: ${this.routeDistance},用时约${this.routeTime}`,
    duration: 3000
    });
    } else {
    console.warn('[FieldMapPage] 未找到路线');
    promptAction.showToast({
    message: '未找到合适的路线',
    duration: 2000
    });
    }
    } else {
    // 搜索失败
    console.error('[FieldMapPage] 路线搜索失败, errorCode:', errorCode);

    let errorMessage = '路线搜索失败';
    switch (errorCode) {
    case 1000: errorMessage = '请求超时,请重试'; break;
    case 1001: errorMessage = '网络错误,请检查网络连接'; break;
    case 1002: errorMessage = '无效的参数'; break;
    case 2000: errorMessage = '无效的用户Key'; break;
    case 3000: errorMessage = '服务请求超出限制'; break;
    default: errorMessage = `路线搜索失败 (错误码: ${errorCode})`;
    }

    promptAction.showToast({
    message: errorMessage,
    duration: 3000
    });
    }
    }

    关键点:

    • result.getPaths() 返回多条路线方案,通常取第一条
    • path.getDistance() 返回距离(单位:米)
    • path.getDuration() 返回预计时长(单位:秒)
    • path.getSteps() 返回路线分段信息,用于绘制路线

    2.4 在地图上绘制路线

    /**
    * 绘制路线到地图上
    * @param steps 路线分段信息
    */

    private drawRoute(steps: ESObject): void {
    console.info('[FieldMapPage] 开始绘制路线…');

    if (!globalAMap) {
    console.error('[FieldMapPage] 地图对象为空,无法绘制路线');
    return;
    }

    if (!steps) {
    console.error('[FieldMapPage] 路线分段信息为空');
    return;
    }

    // 1. 清除旧路线
    if (this.routePolyline) {
    console.info('[FieldMapPage] 清除旧路线');
    this.routePolyline.remove();
    this.routePolyline = null;
    }

    try {
    // 2. 收集所有路线点
    const polylineOptions: PolylineOptions = new PolylineOptions();
    let pointCount = 0;

    // 遍历所有分段
    const stepsArray: ESObject = steps;
    const length: number = stepsArray.length as number;
    console.info(`[FieldMapPage] 处理 ${length} 个路线分段`);

    for (let i = 0; i < length; i++) {
    const step: ESObject = stepsArray[i] as ESObject;
    const polyline: ESObject = step.getPolyline?.() as ESObject;

    if (polyline) {
    const polylineLength: number = polyline.length as number;
    // 遍历分段中的所有点
    for (let j = 0; j < polylineLength; j++) {
    const point: ESObject = polyline[j] as ESObject;
    const lat: number = point.getLatitude?.() as number;
    const lon: number = point.getLongitude?.() as number;

    if (lat && lon) {
    polylineOptions.add(new LatLng(lat, lon));
    pointCount++;
    }
    }
    }
    }

    console.info(`[FieldMapPage] 收集到 ${pointCount} 个路线点`);

    // 3. 绘制路线
    if (pointCount > 0) {
    polylineOptions.setWidth(12); // 线条宽度
    polylineOptions.setColor(0xFF2196F3); // 蓝色
    polylineOptions.setZIndex(50); // 层级(确保在地块标记之上)

    const polyline = globalAMap.addPolyline(polylineOptions);
    this.routePolyline = polyline ? polyline : null;

    if (this.routePolyline) {
    console.info('[FieldMapPage] ✅ 路线绘制成功');
    } else {
    console.error('[FieldMapPage] ❌ 添加路线到地图失败');
    }
    } else {
    console.warn('[FieldMapPage] 没有有效的路线点');
    promptAction.showToast({
    message: '路线数据无效',
    duration: 2000
    });
    }
    } catch (error) {
    console.error('[FieldMapPage] 绘制路线失败:', error);
    promptAction.showToast({
    message: '绘制路线失败',
    duration: 2000
    });
    }
    }

    /**
    * 清除路线
    */

    private clearRoute(): void {
    if (this.routePolyline) {
    this.routePolyline.remove();
    this.routePolyline = null;
    }
    this.showRouteDialog = false;
    this.routeDistance = '';
    this.routeTime = '';
    }

    绘制路线的关键步骤:

    步骤说明
    1. 清除旧路线 避免多条路线重叠显示
    2. 遍历分段 路线由多个分段(step)组成
    3. 收集坐标点 每个分段包含多个坐标点
    4. 创建Polyline 使用 PolylineOptions 配置样式
    5. 添加到地图 调用 addPolyline() 显示路线

    Polyline样式配置:

    • setWidth(12):线条宽度(像素)
    • setColor(0xFF2196F3):颜色(ARGB格式,蓝色)
    • setZIndex(50):层级,数值越大越在上层

    三、POI搜索功能实现

    3.1 初始化POI搜索服务

    @Component
    export struct FieldMapPage {
    // POI搜索相关
    private poiSearch: PoiSearch | null = null;
    private poiMarkers: Marker[] = [];
    private searchCircle: MapCircle | null = null;

    @State showPoiSearch: boolean = false;
    @State poiKeyword: string = '农资店';
    @State poiResults: PoiItem[] = [];
    @State searchRadius: number = 5000; // 搜索半径(米)

    aboutToAppear(): void {
    try {
    const context = getContext(this) as Context;

    // 初始化POI搜索(需要context和undefined参数)
    this.poiSearch = new PoiSearch(context, undefined);
    console.info('[FieldMapPage] PoiSearch初始化成功');

    } catch (error) {
    console.error('[FieldMapPage] POI搜索初始化失败:', error);
    }
    }
    }

    3.2 实现周边POI搜索

    /**
    * 搜索周边POI
    * @param keyword 搜索关键词(如:农资店、加油站)
    */

    private searchNearbyPoi(keyword: string): void {
    // 1. 检查当前位置
    if (!this.poiSearch || !this.currentLocationInfo ||
    !this.currentLocationInfo.latitude || !this.currentLocationInfo.longitude) {
    promptAction.showToast({
    message: '无法获取当前位置',
    duration: 2000
    });
    return;
    }

    try {
    // 2. 创建中心点
    const centerPoint = new LatLonPoint(
    this.currentLocationInfo.latitude,
    this.currentLocationInfo.longitude
    );

    // 3. 创建圆形搜索区域
    const searchBound = PoiSearchBound.createCircleSearchBound(
    centerPoint, // 中心点
    this.searchRadius // 半径(米)
    );

    // 4. 创建POI查询
    const query = new PoiQuery(
    keyword, // 关键词
    '', // 类型(空表示所有类型)
    '' // 城市(空表示全国)
    );
    query.setPageSize(20); // 每页结果数
    query.setPageNum(0); // 页码(从0开始)
    query.setExtensions('all'); // 获取完整信息

    console.info(`[FieldMapPage] POI搜索: ${keyword}, 半径: ${this.searchRadius}`);

    // 5. 设置监听器
    const listener: OnPoiSearchListener = {
    onPoiSearched: (result: PoiResult | undefined, errorCode: number): void => {
    this.handlePoiResult(result, errorCode);
    },
    onPoiItemSearched: () => {}
    };

    // 6. 执行搜索
    this.poiSearch.setOnPoiSearchListener(listener);
    this.poiSearch.setBound(searchBound);
    this.poiSearch.setQuery(query);
    this.poiSearch.searchPOIAsyn();

    promptAction.showToast({
    message: `正在搜索${keyword}`,
    duration: 1500
    });
    } catch (error) {
    console.error('[FieldMapPage] POI搜索失败:', error);
    promptAction.showToast({
    message: 'POI搜索失败',
    duration: 2000
    });
    }
    }

    POI搜索参数说明:

    参数说明示例
    keyword 搜索关键词 ‘农资店’、‘加油站’、‘农机租赁’
    category POI类型码 留空表示所有类型
    city 城市名称 留空表示全国范围
    searchRadius 搜索半径 5000(5公里)
    pageSize 每页结果数 20(最大50)

    3.3 处理POI搜索结果

    /**
    * 处理POI搜索结果
    */

    private handlePoiResult(result: PoiResult | undefined, errorCode: number): void {
    console.info(`[FieldMapPage] POI搜索回调, errorCode: ${errorCode}`);

    if (!globalAMap) {
    console.error('[FieldMapPage] 地图对象为空');
    return;
    }

    // 清除旧结果
    this.clearPoiResults();

    if (errorCode === AMapException.CODE_AMAP_SUCCESS && result) {
    console.info('[FieldMapPage] ✅ POI搜索成功!');

    const pois = result.getPois();
    console.info(`[FieldMapPage] 找到 ${pois ? pois.length : 0} 个POI`);

    if (pois && pois.length > 0) {
    // 1. 将ArrayList转换为数组
    this.poiResults = [];
    for (let i = 0; i < pois.length; i++) {
    this.poiResults.push(pois[i]);
    }

    // 2. 在地图上添加POI标记
    for (const poi of this.poiResults) {
    const latLon = poi.getLatLonPoint();
    if (latLon) {
    const markerOptions: MarkerOptions = new MarkerOptions();
    markerOptions.setPosition(
    new LatLng(latLon.getLatitude(), latLon.getLongitude())
    );
    markerOptions.setTitle(poi.getTitle());
    markerOptions.setSnippet(poi.getSnippet());
    markerOptions.setZIndex(15); // 层级高于地块标记
    markerOptions.setAnchor(0.5, 1);
    // 使用紫红色标记,区别于地块标记
    markerOptions.setIcon(
    BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_MAGENTA)
    );

    const marker = globalAMap.addMarker(markerOptions);
    if (marker) {
    this.poiMarkers.push(marker);
    }
    }
    }

    // 3. 绘制搜索范围圆圈
    if (this.currentLocationInfo?.latitude && this.currentLocationInfo?.longitude) {
    const circleOptions: CircleOptions = new CircleOptions();
    circleOptions.setCenter(
    new LatLng(
    this.currentLocationInfo.latitude,
    this.currentLocationInfo.longitude
    )
    );
    circleOptions.setRadius(this.searchRadius);
    circleOptions.setStrokeColor(0x88FF5722); // 半透明橙色边框
    circleOptions.setStrokeWidth(2);
    circleOptions.setFillColor(0x22FF5722); // 半透明橙色填充

    this.searchCircle = globalAMap.addCircle(circleOptions);
    }

    promptAction.showToast({
    message: `找到 ${this.poiResults.length} 个结果`,
    duration: 2000
    });
    } else {
    promptAction.showToast({
    message: '未找到相关POI',
    duration: 2000
    });
    }
    } else {
    console.error('[FieldMapPage] POI搜索失败, errorCode:', errorCode);
    promptAction.showToast({
    message: 'POI搜索失败',
    duration: 2000
    });
    }
    }

    /**
    * 清除POI搜索结果
    */

    private clearPoiResults(): void {
    // 清除POI标记
    for (const marker of this.poiMarkers) {
    marker.remove();
    }
    this.poiMarkers = [];

    // 清除搜索范围圆圈
    if (this.searchCircle) {
    this.searchCircle.remove();
    this.searchCircle = null;
    }

    // 清除结果列表
    this.poiResults = [];
    }

    POI结果处理要点:

    步骤说明
    1. 清除旧结果 避免多次搜索结果叠加
    2. 转换数据 将ArrayList转为TypeScript数组
    3. 添加标记 使用紫红色标记区分POI和地块
    4. 绘制范围 用圆圈显示搜索范围

    3.4 POI搜索UI实现

    在地图页面添加POI搜索面板:

    build() {
    Stack({ alignContent: Alignment.TopStart }) {
    // 地图组件
    MapViewComponent({ mapOptions: this.mapOptions })
    .width('100%')
    .height('100%')

    // POI搜索面板
    if (this.showPoiSearch) {
    Column() {
    // 搜索关键词选择
    Row() {
    Text('搜索类型:')
    .fontSize(14)
    .fontColor('#333333')

    // 快捷关键词按钮
    Button('农资店')
    .fontSize(12)
    .padding({ left: 12, right: 12, top: 6, bottom: 6 })
    .backgroundColor(this.poiKeyword === '农资店' ? '#4CAF50' : '#E0E0E0')
    .fontColor(this.poiKeyword === '农资店' ? '#FFFFFF' : '#666666')
    .onClick(() => {
    this.poiKeyword = '农资店';
    this.searchNearbyPoi(this.poiKeyword);
    })

    Button('农机租赁')
    .fontSize(12)
    .padding({ left: 12, right: 12, top: 6, bottom: 6 })
    .backgroundColor(this.poiKeyword === '农机租赁' ? '#4CAF50' : '#E0E0E0')
    .fontColor(this.poiKeyword === '农机租赁' ? '#FFFFFF' : '#666666')
    .onClick(() => {
    this.poiKeyword = '农机租赁';
    this.searchNearbyPoi(this.poiKeyword);
    })

    Button('加油站')
    .fontSize(12)
    .padding({ left: 12, right: 12, top: 6, bottom: 6 })
    .backgroundColor(this.poiKeyword === '加油站' ? '#4CAF50' : '#E0E0E0')
    .fontColor(this.poiKeyword === '加油站' ? '#FFFFFF' : '#666666')
    .onClick(() => {
    this.poiKeyword = '加油站';
    this.searchNearbyPoi(this.poiKeyword);
    })
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceBetween)
    .padding(12)

    // 搜索半径调整
    Row() {
    Text('搜索半径:')
    .fontSize(14)
    .fontColor('#333333')

    Slider({
    value: this.searchRadius,
    min: 1000,
    max: 50000,
    step: 1000
    })
    .width(150)
    .onChange((value: number) => {
    this.searchRadius = value;
    })

    Text(`${(this.searchRadius / 1000).toFixed(1)}公里`)
    .fontSize(12)
    .fontColor('#666666')
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceBetween)
    .padding({ left: 12, right: 12, bottom: 12 })

    // 搜索结果列表
    if (this.poiResults.length > 0) {
    List() {
    ForEach(this.poiResults, (poi: PoiItem, index: number) => {
    ListItem() {
    Row() {
    Column() {
    Text(poi.getTitle())
    .fontSize(14)
    .fontWeight(FontWeight.Medium)
    .fontColor('#333333')

    Text(poi.getSnippet())
    .fontSize(12)
    .fontColor('#999999')
    .margin({ top: 4 })

    // 距离信息
    if (poi.getDistance()) {
    Text(`距离: ${(poi.getDistance() / 1000).toFixed(2)}公里`)
    .fontSize(11)
    .fontColor('#4CAF50')
    .margin({ top: 4 })
    }
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)

    // 导航按钮
    Button('导航')
    .fontSize(12)
    .padding({ left: 12, right: 12, top: 6, bottom: 6 })
    .backgroundColor('#2196F3')
    .onClick(() => {
    const latLon = poi.getLatLonPoint();
    if (latLon && this.currentLocationInfo) {
    this.searchDriveRoute(
    this.currentLocationInfo.latitude!,
    this.currentLocationInfo.longitude!,
    latLon.getLatitude(),
    latLon.getLongitude()
    );
    }
    })
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#FFFFFF')
    }
    }, (poi: PoiItem, index: number) => index.toString())
    }
    .width('100%')
    .maxHeight(300)
    .backgroundColor('#F5F5F5')
    }

    // 关闭按钮
    Button('关闭')
    .width('100%')
    .fontSize(14)
    .backgroundColor('#FF5722')
    .onClick(() => {
    this.showPoiSearch = false;
    this.clearPoiResults();
    })
    .margin({ top: 8 })
    }
    .width('90%')
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .shadow({ radius: 8, color: '#33000000' })
    .padding(16)
    .margin({ top: 80, left: '5%' })
    }
    }
    .width('100%')
    .height('100%')
    }

    UI设计要点:

    • 提供快捷关键词按钮(农资店、农机租赁、加油站)
    • 可调节搜索半径(1-50公里)
    • 列表展示搜索结果,包含距离信息
    • 每个结果提供导航按钮,可直接规划路线

    四、天气查询功能

    4.1 实现天气查询

    /**
    * 查询当前城市天气
    * @param cityName 城市名称
    */

    private searchWeather(cityName: string): void {
    if (!cityName) {
    console.warn('[FieldMapPage] 城市名称为空,无法查询天气');
    return;
    }

    try {
    const context = getContext(this) as Context;

    // 创建天气搜索监听器
    const weatherSearchListener: OnWeatherSearchListener = {
    onWeatherLiveSearched: (
    weatherLiveResult: LocalWeatherLiveResult | undefined,
    errorCode: number
    ): void => {
    if (errorCode === AMapException.CODE_AMAP_SUCCESS) {
    if (weatherLiveResult && weatherLiveResult.getLiveResult()) {
    const liveWeather: LocalWeatherLive = weatherLiveResult.getLiveResult();

    // 更新天气信息
    this.currentWeather = liveWeather.getWeather() || '';
    this.currentTemperature = liveWeather.getTemperature() || '';
    this.windDirection = liveWeather.getWindDirection() || '';
    this.windPower = liveWeather.getWindPower() || '';
    this.humidity = liveWeather.getHumidity() || '';
    this.reportTime = liveWeather.getReportTime() || '';

    console.info(`[FieldMapPage] ✅ 天气查询成功: ${this.currentWeather} ${this.currentTemperature}°C`);
    }
    } else {
    console.error(`[FieldMapPage] ❌ 天气查询失败,错误码: ${errorCode}`);
    }
    },
    onWeatherForecastSearched: (): void => {
    // 不处理预报天气
    }
    };

    // 创建天气查询
    const query = new WeatherSearchQuery(
    cityName,
    WeatherSearchQuery.WEATHER_TYPE_LIVE // 实时天气
    );

    const weatherSearch = new WeatherSearch(context);
    weatherSearch.setOnWeatherSearchListener(weatherSearchListener);
    weatherSearch.setQuery(query);
    weatherSearch.searchWeatherAsyn();

    console.info(`[FieldMapPage] 开始查询天气: ${cityName}`);
    } catch (error) {
    console.error('[FieldMapPage] 天气查询异常:', error);
    }
    }

    /**
    * 根据天气描述返回对应图标
    */

    private getWeatherIcon(weather: string): string {
    if (weather.includes('晴')) return '☀️';
    if (weather.includes('云') || weather.includes('阴')) return '☁️';
    if (weather.includes('雨')) return '🌧️';
    if (weather.includes('雪')) return '❄️';
    if (weather.includes('雾') || weather.includes('霾')) return '🌫️';
    if (weather.includes('雷')) return '⛈️';
    if (weather.includes('风')) return '💨';
    return '🌤️';
    }

    4.2 天气信息展示

    在地图页面添加天气信息卡片:

    // 天气信息卡片
    if (this.currentWeather) {
    Row() {
    Text(this.getWeatherIcon(this.currentWeather))
    .fontSize(24)

    Column() {
    Text(`${this.currentWeather} ${this.currentTemperature}°C`)
    .fontSize(14)
    .fontWeight(FontWeight.Medium)
    .fontColor('#333333')

    Text(`${this.windDirection}${this.windPower}级 | 湿度${this.humidity}%`)
    .fontSize(11)
    .fontColor('#999999')
    .margin({ top: 4 })
    }
    .alignItems(HorizontalAlign.Start)
    .margin({ left: 8 })
    }
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    .shadow({ radius: 4, color: '#22000000' })
    .margin({ top: 80, left: 16 })
    }


    五、完整功能集成

    5.1 地图工具栏

    在地图页面添加工具栏,集成所有功能:

    // 地图工具栏
    Row() {
    // POI搜索按钮
    Button() {
    Image($r('app.media.ic_search'))
    .width(24)
    .height(24)
    .fillColor('#FFFFFF')
    }
    .width(48)
    .height(48)
    .backgroundColor('#4CAF50')
    .borderRadius(24)
    .onClick(() => {
    this.showPoiSearch = !this.showPoiSearch;
    })

    // 定位按钮
    Button() {
    Image($r('app.media.ic_location'))
    .width(24)
    .height(24)
    .fillColor('#FFFFFF')
    }
    .width(48)
    .height(48)
    .backgroundColor('#2196F3')
    .borderRadius(24)
    .margin({ left: 12 })
    .onClick(() => {
    this.moveToCurrentLocation();
    })

    // 清除路线按钮
    if (this.routePolyline) {
    Button() {
    Image($r('app.media.ic_close'))
    .width(24)
    .height(24)
    .fillColor('#FFFFFF')
    }
    .width(48)
    .height(48)
    .backgroundColor('#FF5722')
    .borderRadius(24)
    .margin({ left: 12 })
    .onClick(() => {
    this.clearRoute();
    })
    }
    }
    .position({ x: 16, y: '50%' })

    5.2 地块标记点击触发导航

    修改地块标记的点击事件,添加导航功能:

    // 在addFieldMarkers方法中
    globalAMap.setOnMarkerClickListener({
    onMarkerClick: (marker: Marker): boolean => {
    const title = marker.getTitle();
    console.info('[FieldMapPage] 标记被点击:', title);

    // 查找对应的地块
    const field = this.fields.find(f => f.name === title);
    if (field) {
    // 显示地块详情对话框
    AlertDialog.show({
    title: field.name,
    message: `面积: ${field.area}亩\\n${field.currentCrop ? '作物: ' + field.currentCrop.cropName : '状态: 闲置'}`,
    primaryButton: {
    value: '导航',
    action: () => {
    // 规划到该地块的路线
    this.planRouteToField(field.id);
    }
    },
    secondaryButton: {
    value: '查看详情',
    action: () => {
    // 跳转到地块详情页
    router.pushUrl({
    url: 'pages/Field/FieldDetailPage',
    params: { fieldId: field.id }
    });
    }
    }
    });
    }

    return true;
    }
    });


    六、实操练习

    练习1:实现路线规划

    任务:点击地块标记,规划从当前位置到该地块的路线

    步骤:

  • 确保已开启定位权限
  • 点击地图上的任意地块标记
  • 在弹出的对话框中点击"导航"按钮
  • 观察地图上绘制的蓝色路线
  • 查看路线信息(距离、时长)
  • 预期结果:

    • 地图上显示蓝色路线
    • 弹出提示显示路线距离和时长
    • 路线从当前位置连接到目标地块

    练习2:搜索周边POI

    任务:搜索周边的农资店

    步骤:

  • 点击地图工具栏的搜索按钮
  • 在POI搜索面板中点击"农资店"按钮
  • 调整搜索半径(如5公里)
  • 观察地图上的紫红色POI标记
  • 查看搜索结果列表
  • 点击某个结果的"导航"按钮
  • 预期结果:

    • 地图上显示紫红色POI标记
    • 显示搜索范围的橙色圆圈
    • 列表展示POI名称、地址、距离
    • 可以规划到POI的路线

    练习3:调整搜索半径

    任务:尝试不同的搜索半径

    步骤:

  • 打开POI搜索面板
  • 拖动搜索半径滑块(1-50公里)
  • 点击搜索按钮
  • 观察搜索结果数量的变化
  • 观察地图上圆圈范围的变化
  • 预期结果:

    • 半径越大,搜索结果越多
    • 地图上的圆圈范围相应变化
    • 远距离的POI也会被搜索到

    七、常见问题与解决方案

    问题1:路线规划失败

    现象:点击导航后提示"路线搜索失败"

    可能原因:

  • 网络连接问题
  • 起点或终点坐标无效
  • API Key配置错误
  • 搜索服务未正确初始化
  • 解决方案:

    // 1. 检查网络权限
    // module.json5中添加:
    "requestPermissions": [
    {
    "name": "ohos.permission.INTERNET"
    }
    ]

    // 2. 验证坐标有效性
    if (!startLat || !startLon || !endLat || !endLon) {
    console.error('坐标无效');
    return;
    }

    // 3. 检查API Key
    console.info('API Key:', MapConstants.AMAP_API_KEY);

    // 4. 添加错误处理
    try {
    this.routeSearch.calculateDriveRouteAsyn(query);
    } catch (error) {
    console.error('路线搜索异常:', error);
    }

    问题2:POI搜索无结果

    现象:搜索后提示"未找到相关POI"

    可能原因:

  • 搜索半径太小
  • 关键词不准确
  • 当前位置偏远,周边确实没有相关POI
  • 解决方案:

    // 1. 增大搜索半径
    this.searchRadius = 10000; // 10公里

    // 2. 使用更通用的关键词
    this.poiKeyword = '商店'; // 而不是'农资店'

    // 3. 添加城市范围搜索
    const query = new PoiQuery(keyword, '', '武汉市');

    问题3:路线不显示在地图上

    现象:路线规划成功,但地图上看不到路线

    可能原因:

  • Polyline的ZIndex太低,被其他图层遮挡
  • 路线颜色与地图背景相近
  • 地图缩放级别不合适
  • 解决方案:

    // 1. 提高ZIndex
    polylineOptions.setZIndex(100);

    // 2. 使用更鲜艳的颜色
    polylineOptions.setColor(0xFFFF0000); // 红色
    polylineOptions.setWidth(15); // 增加宽度

    // 3. 调整地图视角
    const centerLat = (startLat + endLat) / 2;
    const centerLon = (startLon + endLon) / 2;
    const cameraUpdate = CameraUpdateFactory.newLatLngZoom(
    new LatLng(centerLat, centerLon),
    12
    );
    globalAMap.animateCamera(cameraUpdate);

    问题4:定位权限未授权

    现象:无法获取当前位置

    解决方案:

    // 在EntryAbility.ets的onCreate中请求权限
    async requestPermissions() {
    const permissions: Permissions[] = [
    'ohos.permission.APPROXIMATELY_LOCATION',
    'ohos.permission.LOCATION'
    ];

    const atManager = abilityAccessCtrl.createAtManager();
    const result = await atManager.requestPermissionsFromUser(
    this.context,
    permissions
    );

    console.info('权限申请结果:', result.authResults);
    }


    八、功能扩展建议

    8.1 多种出行方式

    除了驾车路线,还可以添加步行、骑行路线:

    // 步行路线
    private searchWalkRoute(startLat: number, startLon: number, endLat: number, endLon: number): void {
    const startPoint = new LatLonPoint(startLat, startLon);
    const endPoint = new LatLonPoint(endLat, endLon);
    const fromAndTo = new FromAndTo(startPoint, endPoint);

    const query = new WalkRouteQuery(fromAndTo);

    const listener: OnRouteSearchListener = {
    onWalkRouteSearched: (result: WalkRouteResult | undefined, errorCode: number): void => {
    // 处理步行路线结果
    },
    onDriveRouteSearched: (): void => {},
    onRideRouteSearched: (): void => {},
    onBusRouteSearched: (): void => {}
    };

    this.routeSearch.setRouteSearchListener(listener);
    this.routeSearch.calculateWalkRouteAsyn(query);
    }

    8.2 路线对比

    显示多条路线方案供用户选择:

    private handleRouteResult(result: DriveRouteResult | undefined, errorCode: number): void {
    if (errorCode === AMapException.CODE_AMAP_SUCCESS && result) {
    const paths = result.getPaths();

    // 显示所有路线方案
    for (let i = 0; i < paths.length; i++) {
    const path = paths[i];
    console.info(`方案${i + 1}: ${path.getDistance()}米, ${path.getDuration()}`);

    // 可以用不同颜色绘制多条路线
    this.drawRouteWithColor(path.getSteps(), i === 0 ? 0xFF2196F3 : 0xFF9E9E9E);
    }
    }
    }

    8.3 实时导航

    集成导航SDK实现实时导航:

    // 初始化导航实例
    private naviInstance: AMapNavi | null = null;

    // 开始导航
    private startNavigation(endLat: number, endLon: number): void {
    if (!this.naviInstance) {
    const context = getContext(this) as Context;
    this.naviInstance = AMapNaviFactory.getAMapNaviInstance(
    context,
    MapConstants.AMAP_API_KEY
    );
    }

    // 设置终点
    const endPoint = new NaviLatLng(endLat, endLon);
    const endList = new ArrayList<NaviLatLng>();
    endList.add(endPoint);

    // 计算路线
    this.naviInstance.calculateDriveRoute(
    new ArrayList<NaviLatLng>(), // 起点(空表示当前位置)
    endList, // 终点
    new ArrayList<NaviLatLng>(), // 途经点
    AMapNaviStrategy.DRIVING_DEFAULT
    );

    // 开始导航
    this.naviInstance.startNavi(AMapNaviMode.GPS);
    }

    8.4 收藏常用地点

    保存常用POI,方便快速导航:

    interface FavoritePoi {
    id: string;
    name: string;
    latitude: number;
    longitude: number;
    category: string;
    }

    private favoritePois: FavoritePoi[] = [];

    // 添加收藏
    private addFavorite(poi: PoiItem): void {
    const latLon = poi.getLatLonPoint();
    if (latLon) {
    const favorite: FavoritePoi = {
    id: Date.now().toString(),
    name: poi.getTitle(),
    latitude: latLon.getLatitude(),
    longitude: latLon.getLongitude(),
    category: this.poiKeyword
    };

    this.favoritePois.push(favorite);

    // 保存到本地存储
    StorageUtil.set('favoritePois', JSON.stringify(this.favoritePois));

    promptAction.showToast({
    message: '已添加到收藏',
    duration: 2000
    });
    }
    }


    九、性能优化建议

    9.1 路线缓存

    缓存已计算的路线,避免重复计算:

    private routeCache: Map<string, DriveRouteResult> = new Map();

    private getCacheKey(startLat: number, startLon: number, endLat: number, endLon: number): string {
    return `${startLat.toFixed(4)},${startLon.toFixed(4)}${endLat.toFixed(4)},${endLon.toFixed(4)}`;
    }

    private searchDriveRoute(startLat: number, startLon: number, endLat: number, endLon: number): void {
    const cacheKey = this.getCacheKey(startLat, startLon, endLat, endLon);

    // 检查缓存
    if (this.routeCache.has(cacheKey)) {
    console.info('[FieldMapPage] 使用缓存的路线');
    const cachedResult = this.routeCache.get(cacheKey);
    this.handleRouteResult(cachedResult, AMapException.CODE_AMAP_SUCCESS);
    return;
    }

    // 正常搜索…
    }

    9.2 POI标记优化

    当POI数量较多时,使用聚合显示:

    // 根据地图缩放级别决定是否显示标记
    private updatePoiMarkers(zoom: number): void {
    if (zoom < 12) {
    // 缩放级别较小时,隐藏标记或显示聚合
    for (const marker of this.poiMarkers) {
    marker.setVisible(false);
    }
    } else {
    // 缩放级别较大时,显示所有标记
    for (const marker of this.poiMarkers) {
    marker.setVisible(true);
    }
    }
    }

    9.3 搜索防抖

    避免频繁搜索:

    private searchTimer: number = 1;

    private debouncedSearch(keyword: string): void {
    // 清除之前的定时器
    if (this.searchTimer !== 1) {
    clearTimeout(this.searchTimer);
    }

    // 设置新的定时器
    this.searchTimer = setTimeout(() => {
    this.searchNearbyPoi(keyword);
    }, 500); // 500ms延迟
    }


    十、总结

    本篇教程完成了地图导航与路线规划功能的实现,主要包括:

    ✅ 已实现功能

    功能说明
    驾车路线规划 从当前位置到目标地块的路线计算
    路线可视化 在地图上绘制蓝色路线
    POI周边搜索 搜索农资店、农机租赁、加油站等
    搜索结果展示 列表+地图标记双重展示
    天气查询 基于当前位置的实时天气
    交互优化 点击地块触发导航、POI导航

    🎯 核心技术点

  • RouteSearch:路线搜索服务
  • DriveRouteQuery:驾车路线查询配置
  • Polyline:路线绘制
  • PoiSearch:POI搜索服务
  • PoiSearchBound:圆形搜索范围
  • WeatherSearch:天气查询服务
  • 📊 数据流程

    用户操作 → 搜索服务 → 异步回调 → 结果处理 → UI更新

    点击地块

    获取坐标 → RouteSearch → 路线结果 → 绘制Polyline

    点击搜索

    输入关键词 → PoiSearch → POI结果 → 添加Marker

    🚀 下一步

    在下一篇教程中,我们将学习:

    • HarmonyOS AI能力集成
    • Vision Kit图像识别
    • 病虫害识别功能
    • 作物健康检测

    教程版本:v1.0 更新日期:2024-01 作者:高高种地开发团队

    相关资源:

    • 高德地图HarmonyOS SDK文档
    • HarmonyOS开发者文档
    • 项目源码

    附录:完整代码示例

    A. 路线规划完整代码

    /**
    * 地图页面 – 路线规划功能
    */

    @Component
    export struct FieldMapPage {
    // 路线搜索相关
    private routeSearch: RouteSearch | null = null;
    private routePolyline: Polyline | null = null;
    @State showRouteDialog: boolean = false;
    @State routeDistance: string = '';
    @State routeTime: string = '';

    aboutToAppear(): void {
    const context = getContext(this) as Context;
    this.routeSearch = new RouteSearch(context);
    }

    // 规划路线
    private planRouteToField(fieldId: string): void {
    const field = this.fields.find(f => f.id === fieldId);
    if (field && this.currentLocationInfo) {
    this.searchDriveRoute(
    this.currentLocationInfo.latitude!,
    this.currentLocationInfo.longitude!,
    field.latitude!,
    field.longitude!
    );
    }
    }

    // 搜索驾车路线
    private searchDriveRoute(
    startLat: number, startLon: number,
    endLat: number, endLon: number
    ): void {
    if (!this.routeSearch) return;

    const startPoint = new LatLonPoint(startLat, startLon);
    const endPoint = new LatLonPoint(endLat, endLon);
    const fromAndTo = new FromAndTo(startPoint, endPoint);

    const query = new DriveRouteQuery(
    fromAndTo, 0,
    new ArrayList<LatLonPoint>(),
    new ArrayList<ArrayList<LatLonPoint>>(),
    ''
    );

    const listener: OnRouteSearchListener = {
    onDriveRouteSearched: (result, errorCode) => {
    this.handleRouteResult(result, errorCode);
    },
    onWalkRouteSearched: () => {},
    onRideRouteSearched: () => {},
    onBusRouteSearched: () => {}
    };

    this.routeSearch.setRouteSearchListener(listener);
    this.routeSearch.calculateDriveRouteAsyn(query);
    }

    // 处理路线结果
    private handleRouteResult(result: DriveRouteResult | undefined, errorCode: number): void {
    if (errorCode === AMapException.CODE_AMAP_SUCCESS && result) {
    const paths = result.getPaths();
    if (paths && paths.length > 0) {
    const path = paths[0];
    this.routeDistance = `${(path.getDistance() / 1000).toFixed(2)}公里`;
    this.routeTime = `${Math.floor(path.getDuration() / 60)}分钟`;
    this.drawRoute(path.getSteps());
    this.showRouteDialog = true;
    }
    }
    }

    // 绘制路线
    private drawRoute(steps: ESObject): void {
    if (!globalAMap) return;

    if (this.routePolyline) {
    this.routePolyline.remove();
    }

    const polylineOptions = new PolylineOptions();
    const stepsArray: ESObject = steps;
    const length: number = stepsArray.length as number;

    for (let i = 0; i < length; i++) {
    const step: ESObject = stepsArray[i] as ESObject;
    const polyline: ESObject = step.getPolyline?.() as ESObject;
    if (polyline) {
    const polylineLength: number = polyline.length as number;
    for (let j = 0; j < polylineLength; j++) {
    const point: ESObject = polyline[j] as ESObject;
    const lat: number = point.getLatitude?.() as number;
    const lon: number = point.getLongitude?.() as number;
    if (lat && lon) {
    polylineOptions.add(new LatLng(lat, lon));
    }
    }
    }
    }

    polylineOptions.setWidth(12);
    polylineOptions.setColor(0xFF2196F3);
    polylineOptions.setZIndex(50);

    this.routePolyline = globalAMap.addPolyline(polylineOptions);
    }
    }

    B. POI搜索完整代码

    /**
    * 地图页面 – POI搜索功能
    */

    @Component
    export struct FieldMapPage {
    // POI搜索相关
    private poiSearch: PoiSearch | null = null;
    private poiMarkers: Marker[] = [];
    @State poiResults: PoiItem[] = [];
    @State searchRadius: number = 5000;

    aboutToAppear(): void {
    const context = getContext(this) as Context;
    this.poiSearch = new PoiSearch(context, undefined);
    }

    // 搜索周边POI
    private searchNearbyPoi(keyword: string): void {
    if (!this.poiSearch || !this.currentLocationInfo) return;

    const centerPoint = new LatLonPoint(
    this.currentLocationInfo.latitude!,
    this.currentLocationInfo.longitude!
    );

    const searchBound = PoiSearchBound.createCircleSearchBound(
    centerPoint,
    this.searchRadius
    );

    const query = new PoiQuery(keyword, '', '');
    query.setPageSize(20);
    query.setPageNum(0);

    const listener: OnPoiSearchListener = {
    onPoiSearched: (result, errorCode) => {
    this.handlePoiResult(result, errorCode);
    },
    onPoiItemSearched: () => {}
    };

    this.poiSearch.setOnPoiSearchListener(listener);
    this.poiSearch.setBound(searchBound);
    this.poiSearch.setQuery(query);
    this.poiSearch.searchPOIAsyn();
    }

    // 处理POI结果
    private handlePoiResult(result: PoiResult | undefined, errorCode: number): void {
    if (!globalAMap) return;

    this.clearPoiResults();

    if (errorCode === AMapException.CODE_AMAP_SUCCESS && result) {
    const pois = result.getPois();
    if (pois && pois.length > 0) {
    this.poiResults = [];
    for (let i = 0; i < pois.length; i++) {
    this.poiResults.push(pois[i]);
    }

    // 添加标记
    for (const poi of this.poiResults) {
    const latLon = poi.getLatLonPoint();
    if (latLon) {
    const markerOptions = new MarkerOptions();
    markerOptions.setPosition(
    new LatLng(latLon.getLatitude(), latLon.getLongitude())
    );
    markerOptions.setTitle(poi.getTitle());
    markerOptions.setIcon(
    BitmapDescriptorFactory.defaultMarker(
    BitmapDescriptorFactory.HUE_MAGENTA
    )
    );

    const marker = globalAMap.addMarker(markerOptions);
    if (marker) {
    this.poiMarkers.push(marker);
    }
    }
    }
    }
    }
    }

    // 清除POI结果
    private clearPoiResults(): void {
    for (const marker of this.poiMarkers) {
    marker.remove();
    }
    this.poiMarkers = [];
    this.poiResults = [];
    }
    }


    恭喜! 🎉 你已经完成了地图导航与路线规划功能的学习。现在你的应用具备了完整的导航能力,可以帮助农户快速找到农田和周边服务设施。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » HarmonyOS智慧农业管理应用开发教程--高高种地--第15篇:地图导航与路线规划
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!