第九篇:绑图与测距功能

本篇教程将学习如何在地图上绑制各种图形(折线、多边形、圆形),以及实现测距和面积计算功能。

学习目标

  • 绑制折线(Polyline)
  • 绑制多边形(Polygon)
  • 绑制圆形(Circle)
  • 实现测距功能
  • 实现面积计算功能

1. 覆盖物类型

类型 类名 说明
折线 Polyline 连接多个点的线段
多边形 Polygon 封闭的多边形区域
圆形 Circle 以某点为圆心的圆形区域
弧线 Arc 圆弧
地面覆盖物 GroundOverlay 贴在地面上的图片

2. 完整代码示例

创建文件 entry/src/main/ets/pages/Demo08_Drawing.ets

import {
  AMap,
  MapView,
  MapViewComponent,
  MapViewManager,
  MapViewCreateCallback,
  CameraUpdateFactory,
  LatLng,
  Marker,
  MarkerOptions,
  Polyline,
  PolylineOptions,
  Polygon,
  PolygonOptions,
  Circle,
  CircleOptions,
  BitmapDescriptorFactory,
  AMapUtils
} from '@amap/amap_lbs_map3d';

const MAP_VIEW_NAME = 'DrawingDemo';

/**
 * 绘制模式
 */
type DrawMode = 'none' | 'polyline' | 'polygon' | 'circle';

@Entry
@Component
struct Demo08_Drawing {
  private mapView: MapView | undefined = undefined;
  private aMap: AMap | undefined = undefined;
  
  // 绘制的覆盖物
  private polyline: Polyline | undefined = undefined;
  private polygon: Polygon | undefined = undefined;
  private circle: Circle | undefined = undefined;
  private pointMarkers: Marker[] = [];
  
  // 绘制的点
  private drawPoints: LatLng[] = [];
  private circleCenter: LatLng | undefined = undefined;
  
  @State isMapReady: boolean = false;
  @State drawMode: DrawMode = 'none';
  @State info: string = '选择绘制模式,然后点击地图添加点';
  @State totalDistance: number = 0;
  @State totalArea: number = 0;
  @State circleRadius: number = 0;

  private mapViewCreateCallback: MapViewCreateCallback = 
    (mapview: MapView | undefined, mapViewName: string | undefined) => {
      if (!mapview || mapViewName !== MAP_VIEW_NAME) return;

      this.mapView = mapview;
      this.mapView.onCreate();
      
      this.mapView.getMapAsync((map: AMap) => {
        this.aMap = map;
        this.isMapReady = true;
        
        // 设置初始视野
        const center = new LatLng(39.909187, 116.397451);
        map.moveCamera(CameraUpdateFactory.newLatLngZoom(center, 14));
        
        // 启用控件
        map.getUiSettings()?.setZoomControlsEnabled(true);
        
        // 地图点击事件
        map.setOnMapClickListener((point: LatLng) => {
          this.onMapClick(point);
        });
      });
    };

  /**
   * 地图点击处理
   */
  private onMapClick(point: LatLng): void {
    switch (this.drawMode) {
      case 'polyline':
        this.addPolylinePoint(point);
        break;
      case 'polygon':
        this.addPolygonPoint(point);
        break;
      case 'circle':
        this.handleCircleClick(point);
        break;
      default:
        this.info = '请先选择绘制模式';
    }
  }

  /**
   * ==================== 折线绑制 ====================
   */
  
  /**
   * 添加折线点
   */
  private addPolylinePoint(point: LatLng): void {
    this.drawPoints.push(point);
    
    // 添加点标记
    this.addPointMarker(point, this.drawPoints.length);
    
    // 更新折线
    this.updatePolyline();
    
    // 计算总距离
    this.calculateTotalDistance();
  }

  /**
   * 更新折线显示
   */
  private updatePolyline(): void {
    if (!this.aMap || this.drawPoints.length < 2) return;
    
    // 移除旧折线
    if (this.polyline) {
      this.polyline.remove();
    }
    
    // 创建新折线
    const options = new PolylineOptions();
    options.setPoints(this.drawPoints);
    options.setWidth(8);
    options.setColor(0xFF2196F3);  // 蓝色
    options.setGeodesic(false);     // 是否大地曲线
    
    this.polyline = this.aMap.addPolyline(options);
  }

  /**
   * 计算总距离
   */
  private calculateTotalDistance(): void {
    if (this.drawPoints.length < 2) {
      this.totalDistance = 0;
      this.info = `已添加 ${this.drawPoints.length} 个点`;
      return;
    }
    
    let distance = 0;
    for (let i = 0; i < this.drawPoints.length - 1; i++) {
      const p1 = this.drawPoints[i];
      const p2 = this.drawPoints[i + 1];
      
      // 使用高德工具类计算两点距离
      distance += AMapUtils.calculateLineDistance(p1, p2);
    }
    
    this.totalDistance = distance;
    this.info = `总距离: ${this.formatDistance(distance)}\n已添加 ${this.drawPoints.length} 个点`;
  }

  /**
   * ==================== 多边形绑制 ====================
   */
  
  /**
   * 添加多边形点
   */
  private addPolygonPoint(point: LatLng): void {
    this.drawPoints.push(point);
    
    // 添加点标记
    this.addPointMarker(point, this.drawPoints.length);
    
    // 更新多边形
    this.updatePolygon();
    
    // 计算面积和周长
    this.calculatePolygonMetrics();
  }

  /**
   * 更新多边形显示
   */
  private updatePolygon(): void {
    if (!this.aMap || this.drawPoints.length < 3) {
      // 少于3个点时显示折线
      if (this.drawPoints.length >= 2) {
        if (this.polyline) this.polyline.remove();
        const options = new PolylineOptions();
        options.setPoints(this.drawPoints);
        options.setWidth(6);
        options.setColor(0xFF4CAF50);
        this.polyline = this.aMap?.addPolyline(options);
      }
      return;
    }
    
    // 移除临时折线
    if (this.polyline) {
      this.polyline.remove();
      this.polyline = undefined;
    }
    
    // 移除旧多边形
    if (this.polygon) {
      this.polygon.remove();
    }
    
    // 创建新多边形
    const options = new PolygonOptions();
    options.setPoints(this.drawPoints);
    options.setStrokeWidth(6);
    options.setStrokeColor(0xFF4CAF50);   // 绿色边框
    options.setFillColor(0x304CAF50);     // 半透明绿色填充
    
    this.polygon = this.aMap.addPolygon(options);
  }

  /**
   * 计算多边形面积和周长
   */
  private calculatePolygonMetrics(): void {
    if (this.drawPoints.length < 3) {
      this.totalArea = 0;
      this.info = `已添加 ${this.drawPoints.length} 个点(至少需要3个点)`;
      return;
    }
    
    // 计算面积
    this.totalArea = AMapUtils.calculateArea(this.drawPoints);
    
    // 计算周长
    let perimeter = 0;
    for (let i = 0; i < this.drawPoints.length; i++) {
      const p1 = this.drawPoints[i];
      const p2 = this.drawPoints[(i + 1) % this.drawPoints.length];
      perimeter += AMapUtils.calculateLineDistance(p1, p2);
    }
    
    this.totalDistance = perimeter;
    this.info = `面积: ${this.formatArea(this.totalArea)}\n周长: ${this.formatDistance(perimeter)}\n已添加 ${this.drawPoints.length} 个点`;
  }

  /**
   * ==================== 圆形绑制 ====================
   */
  
  /**
   * 处理圆形点击
   */
  private handleCircleClick(point: LatLng): void {
    if (!this.circleCenter) {
      // 第一次点击设置圆心
      this.circleCenter = point;
      this.addPointMarker(point, 1);
      this.info = '圆心已设置,再次点击设置半径';
    } else {
      // 第二次点击设置半径
      const radius = AMapUtils.calculateLineDistance(this.circleCenter, point);
      this.circleRadius = radius;
      
      // 绘制圆形
      this.drawCircle(this.circleCenter, radius);
      
      // 计算面积
      const area = Math.PI * radius * radius;
      this.info = `圆形半径: ${this.formatDistance(radius)}\n面积: ${this.formatArea(area)}`;
    }
  }

  /**
   * 绘制圆形
   */
  private drawCircle(center: LatLng, radius: number): void {
    if (!this.aMap) return;
    
    // 移除旧圆形
    if (this.circle) {
      this.circle.remove();
    }
    
    // 创建新圆形
    const options = new CircleOptions();
    options.setCenter(center);
    options.setRadius(radius);
    options.setStrokeWidth(6);
    options.setStrokeColor(0xFFFF9800);   // 橙色边框
    options.setFillColor(0x30FF9800);     // 半透明橙色填充
    
    this.circle = this.aMap.addCircle(options);
  }

  /**
   * ==================== 通用方法 ====================
   */
  
  /**
   * 添加点标记
   */
  private addPointMarker(point: LatLng, index: number): void {
    if (!this.aMap) return;
    
    const options = new MarkerOptions();
    options.setPosition(point);
    options.setTitle(`${index}`);
    
    // 根据绘制模式设置不同颜色
    let hue = BitmapDescriptorFactory.HUE_BLUE;
    if (this.drawMode === 'polygon') {
      hue = BitmapDescriptorFactory.HUE_GREEN;
    } else if (this.drawMode === 'circle') {
      hue = BitmapDescriptorFactory.HUE_ORANGE;
    }
    
    options.setIcon(BitmapDescriptorFactory.defaultMarker(hue));
    options.setAnchor(0.5, 1.0);
    options.setZIndex(20);
    
    const marker = this.aMap.addMarker(options);
    if (marker) {
      this.pointMarkers.push(marker);
    }
  }

  /**
   * 清除所有绘制
   */
  private clearAll(): void {
    // 清除覆盖物
    if (this.polyline) {
      this.polyline.remove();
      this.polyline = undefined;
    }
    if (this.polygon) {
      this.polygon.remove();
      this.polygon = undefined;
    }
    if (this.circle) {
      this.circle.remove();
      this.circle = undefined;
    }
    
    // 清除标记
    for (const marker of this.pointMarkers) {
      marker.remove();
    }
    this.pointMarkers = [];
    
    // 重置数据
    this.drawPoints = [];
    this.circleCenter = undefined;
    this.totalDistance = 0;
    this.totalArea = 0;
    this.circleRadius = 0;
    this.info = '已清除,选择模式开始绘制';
  }

  /**
   * 撤销最后一个点
   */
  private undoLastPoint(): void {
    if (this.drawPoints.length === 0) return;
    
    // 移除最后一个点
    this.drawPoints.pop();
    
    // 移除最后一个标记
    const lastMarker = this.pointMarkers.pop();
    if (lastMarker) {
      lastMarker.remove();
    }
    
    // 更新图形
    if (this.drawMode === 'polyline') {
      this.updatePolyline();
      this.calculateTotalDistance();
    } else if (this.drawMode === 'polygon') {
      this.updatePolygon();
      this.calculatePolygonMetrics();
    }
  }

  /**
   * 切换绘制模式
   */
  private setDrawMode(mode: DrawMode): void {
    this.clearAll();
    this.drawMode = mode;
    
    switch (mode) {
      case 'polyline':
        this.info = '点击地图添加折线点,测量距离';
        break;
      case 'polygon':
        this.info = '点击地图添加多边形顶点,测量面积';
        break;
      case 'circle':
        this.info = '第一次点击设置圆心,第二次点击设置半径';
        break;
      default:
        this.info = '选择绘制模式';
    }
  }

  /**
   * 格式化距离
   */
  private formatDistance(meters: number): string {
    if (meters < 1000) {
      return `${meters.toFixed(1)}`;
    }
    return `${(meters / 1000).toFixed(2)}公里`;
  }

  /**
   * 格式化面积
   */
  private formatArea(sqMeters: number): string {
    if (sqMeters < 10000) {
      return `${sqMeters.toFixed(1)}平方米`;
    }
    if (sqMeters < 1000000) {
      return `${(sqMeters / 10000).toFixed(2)}公顷`;
    }
    return `${(sqMeters / 1000000).toFixed(2)}平方公里`;
  }

  aboutToAppear(): void {
    MapViewManager.getInstance()
      .registerMapViewCreatedCallback(this.mapViewCreateCallback);
  }

  aboutToDisappear(): void {
    this.clearAll();
    
    MapViewManager.getInstance()
      .unregisterMapViewCreatedCallback(this.mapViewCreateCallback);
    
    if (this.mapView) {
      this.mapView.onDestroy();
      this.mapView = undefined;
      this.aMap = undefined;
    }
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('绘图与测距')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
      }
      .width('100%')
      .height(50)
      .padding({ left: 16 })
      .backgroundColor('#E91E63')

      // 模式选择
      Row() {
        Button('折线测距')
          .fontSize(12)
          .height(36)
          .layoutWeight(1)
          .margin({ right: 6 })
          .backgroundColor(this.drawMode === 'polyline' ? '#2196F3' : '#e0e0e0')
          .fontColor(this.drawMode === 'polyline' ? Color.White : '#333')
          .onClick(() => this.setDrawMode('polyline'))
        
        Button('多边形面积')
          .fontSize(12)
          .height(36)
          .layoutWeight(1)
          .margin({ right: 6 })
          .backgroundColor(this.drawMode === 'polygon' ? '#4CAF50' : '#e0e0e0')
          .fontColor(this.drawMode === 'polygon' ? Color.White : '#333')
          .onClick(() => this.setDrawMode('polygon'))
        
        Button('圆形')
          .fontSize(12)
          .height(36)
          .layoutWeight(1)
          .backgroundColor(this.drawMode === 'circle' ? '#FF9800' : '#e0e0e0')
          .fontColor(this.drawMode === 'circle' ? Color.White : '#333')
          .onClick(() => this.setDrawMode('circle'))
      }
      .width('100%')
      .padding({ left: 12, right: 12, top: 8, bottom: 8 })
      .backgroundColor('#f5f5f5')

      // 地图区域
      Stack() {
        MapViewComponent({ mapViewName: MAP_VIEW_NAME })
          .width('100%')
          .height('100%')
        
        // 信息面板
        Column() {
          Text(this.info)
            .fontSize(12)
            .fontColor('#333')
        }
        .padding(10)
        .backgroundColor('rgba(255,255,255,0.95)')
        .borderRadius(8)
        .position({ x: 10, y: 10 })
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')
      .layoutWeight(1)

      // 操作按钮
      Row() {
        Button('撤销')
          .fontSize(13)
          .height(40)
          .layoutWeight(1)
          .margin({ right: 8 })
          .backgroundColor('#FF9800')
          .enabled(this.drawPoints.length > 0)
          .onClick(() => this.undoLastPoint())
        
        Button('清除')
          .fontSize(13)
          .height(40)
          .layoutWeight(1)
          .backgroundColor('#F44336')
          .onClick(() => this.clearAll())
      }
      .width('100%')
      .padding(12)
      .backgroundColor('#f5f5f5')
    }
    .width('100%')
    .height('100%')
  }
}

3. AMapUtils 工具类

import { AMapUtils } from '@amap/amap_lbs_map3d';

// 计算两点距离(米)
const distance = AMapUtils.calculateLineDistance(point1, point2);

// 计算多边形面积(平方米)
const area = AMapUtils.calculateArea(points);

// 判断点是否在多边形内
const isInside = AMapUtils.isPointInPolygon(point, polygon);

// 判断点是否在圆内
const isInCircle = AMapUtils.isPointInCircle(point, center, radius);

4. PolylineOptions 完整配置

const options = new PolylineOptions();

// 基本属性
options.setPoints(points);          // 坐标点数组
options.setWidth(10);               // 线宽(像素)
options.setColor(0xFF2196F3);       // 颜色(ARGB)

// 线型设置
options.setGeodesic(true);          // 是否大地曲线
options.setDottedLine(false);       // 是否虚线

// 可见性
options.setVisible(true);           // 是否可见
options.setZIndex(10);              // 层级

// 纹理设置
options.setUseTexture(true);        // 使用纹理
options.setLineTexture(texture);    // 纹理图片

5. PolygonOptions 完整配置

const options = new PolygonOptions();

// 顶点
options.setPoints(points);

// 边框
options.setStrokeWidth(5);
options.setStrokeColor(0xFF4CAF50);

// 填充
options.setFillColor(0x304CAF50);

// 其他
options.setVisible(true);
options.setZIndex(10);

6. CircleOptions 完整配置

const options = new CircleOptions();

// 基本属性
options.setCenter(center);          // 圆心
options.setRadius(1000);            // 半径(米)

// 边框
options.setStrokeWidth(5);
options.setStrokeColor(0xFFFF9800);

// 填充
options.setFillColor(0x30FF9800);

// 其他
options.setVisible(true);
options.setZIndex(10);

7. 实用技巧

7.1 绘制带箭头的折线

// 设置箭头纹理
const arrowTexture = await BitmapDescriptorFactory.fromRawfilePath(
  context,
  'arrow.png'
);
options.setLineTexture(arrowTexture);
options.setUseTexture(true);

7.2 绘制渐变色折线

// 设置分段颜色
const colors = [0xFF4CAF50, 0xFFFFEB3B, 0xFFF44336];
options.setColorValues(colors);

7.3 编辑已绘制的图形

// 更新折线点
polyline.setPoints(newPoints);

// 更新多边形点
polygon.setPoints(newPoints);

// 更新圆形
circle.setCenter(newCenter);
circle.setRadius(newRadius);

本篇小结

本篇教程我们学习了:

  • ✅ 折线的绘制和测距功能
  • ✅ 多边形的绘制和面积计算
  • ✅ 圆形的绘制
  • ✅ AMapUtils工具类的使用
  • ✅ 覆盖物的样式配置

下一篇我们将整合所有功能,实现一个完整的地图应用。
班级
https://developer.huawei.com/consumer/cn/training/classDetail/fd34ff9286174e848d34cde7f512ce22?type=1%3Fha_source%3Dhmosclass&ha_sourceId=89000248

源码地址
https://gitcode.com/daleishen/gaodehmjiaocheng.git


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐