在cesium中根据turf生成弓子形(割草机)航线方法

2 阅读2分钟

根据二维平面坐标生成弓子形航线

import * as turf from '@turf/turf';
import type {
  Point,
  LineString,
  Polygon,
  MultiPolygon,
  Feature,
  FeatureCollection,
  BBox
} from 'geojson';
import type { Units } from '@turf/turf';
export interface LatLng {
  lat: number;
  lng: number;
}
export interface SimpleRouteOptions {
  polygon: LatLng[];
  spacing?: number;
  rotate?: number;
  units?: Units;
  margin?: number;
}

export class TurfCoveragePlanner {
  /**
   * 生成弓字形航线
   * @param options 配置项
   * @returns 包含原始多边形、偏移后多边形和航线的对象
   */
  static generate(options: SimpleRouteOptions) {
    const {
      polygon,
      spacing = 20,
      rotate = 0,
      units = 'meters',
      margin = 0
    } = options;
    const originalTurfPolygon = this.toTurfPolygon(polygon);
    let workingPolygon: Feature<Polygon> = originalTurfPolygon;
    let bufferedTurfPolygon: Feature<Polygon | MultiPolygon> | null = null;

    if (margin > 0) {
      const buffered = turf.buffer(originalTurfPolygon, margin, { units });

      if (buffered) {
        bufferedTurfPolygon = buffered;
        if (buffered.geometry.type === 'MultiPolygon') {
          workingPolygon = turf.polygon(buffered.geometry.coordinates[0]);
        } else {
          workingPolygon = buffered as Feature<Polygon>;
        }
      }
    }

    // 3. 计算多边形中心点(用于旋转)
    const center = turf.center(workingPolygon);

    // 4. 将多边形旋转指定角度
    const rotatedPolygon = turf.transformRotate(workingPolygon, -rotate, { pivot: center });

    // 5. 获取旋转后多边形的外接矩形
    const bbox = turf.bbox(rotatedPolygon) as BBox;
    const [minX, minY, maxX, maxY] = bbox;

    // 6. 生成平行航线
    const routes: number[][][] = [];
    const centerLat = center.geometry.coordinates[1];
    const latStep = this.calculateLatStep(spacing, units);
    const lngStep = this.calculateLngStep(spacing, units, centerLat);

    let currentY = minY;

    while (currentY <= maxY) {
      const line: LineString = {
        type: 'LineString',
        coordinates: [
          [minX - lngStep * 0.5, currentY], // 用步长代替硬编码0.01,更准确
          [maxX + lngStep * 0.5, currentY]
        ]
      };

      const intersection = turf.lineIntersect(line, rotatedPolygon) as FeatureCollection<Point>;

      if (intersection.features.length >= 2) {
        const points = intersection.features.map(f => f.geometry.coordinates);

        points.sort((a, b) => a[0] - b[0]);
        routes.push([points[0], points[1]]);
      }

      currentY += latStep;
    }

    // 7. 生成之字形(往返)
    const zigzagRoutes = routes.map((route, index) => {
      return index % 2 === 1 ? [route[1], route[0]] : route;
    });

    // 8. 拼接所有航点
    const rotatedWaypoints: number[][] = [];

    zigzagRoutes.forEach(route => {
      rotatedWaypoints.push(route[0]);
      rotatedWaypoints.push(route[1]);
    });

    // 9. 将所有航点旋转回原始坐标系
    const waypoints: LatLng[] = rotatedWaypoints.map(pos => {
      const point = turf.point(pos);
      const rotatedPoint = turf.transformRotate(point, rotate, { pivot: center });

      return {
        lng: rotatedPoint.geometry.coordinates[0],
        lat: rotatedPoint.geometry.coordinates[1]
      };
    });

    // 10. 将 Turf.js 的多边形转换回 LatLng[]
    const originalPolygonLatLng = this.turfPolygonToLatLng(originalTurfPolygon);
    const bufferedPolygonLatLng = bufferedTurfPolygon ?
      this.turfPolygonToLatLng(bufferedTurfPolygon) :
      null;

    return {
      originalPolygon: originalPolygonLatLng,
      bufferedPolygon: bufferedPolygonLatLng,
      waypoints
    };
  }

  private static toTurfPolygon(points: LatLng[]): Feature<Polygon> {
    const coordinates: number[][][] = [
      points.map(p => [p.lng, p.lat])
    ];

    const first = coordinates[0][0];
    const last = coordinates[0][coordinates[0].length - 1];

    if (first[0] !== last[0] || first[1] !== last[1]) {
      coordinates[0].push(first);
    }

    return turf.polygon(coordinates);
  }

  private static turfPolygonToLatLng(turfPolygon: Feature<Polygon | MultiPolygon>): LatLng[] {
    const coords = turfPolygon.geometry.type === 'MultiPolygon' ?
      turfPolygon.geometry.coordinates[0][0] :
      turfPolygon.geometry.coordinates[0];

    return coords.map(pos => ({
      lng: pos[0],
      lat: pos[1]
    }));
  }

  /**
   * 计算纬度方向的步长(1度纬度≈111320米,全球基本一致)
   */
  private static calculateLatStep(spacing: number, units: Units): number {
    const metersPerDegreeLat = 111320;
    const spacingInMeters = this.convertToMeters(spacing, units);

    return spacingInMeters / metersPerDegreeLat;
  }

  /**
   * 计算经度方向的步长(随纬度变化,赤道最长,两极最短)
   */
  private static calculateLngStep(spacing: number, units: Units, latitude: number): number {
    const metersPerDegreeLng = 111320 * Math.cos(latitude * Math.PI / 180);
    const spacingInMeters = this.convertToMeters(spacing, units);

    return spacingInMeters / metersPerDegreeLng;
  }

  /**
   * 将任意单位转换为米
   */
  private static convertToMeters(value: number, units: Units): number {
    switch (units) {
    case 'kilometers':
    case 'kilometres':
      return value * 1000;
    case 'miles':
      return value * 1609.34;
    case 'feet':
      return value * 0.3048;
    case 'yards':
      return value * 0.9144;
    case 'nauticalmiles':
      return value * 1852;
    default: // meters/metres
      return value;
    }
  }
}

调用方法

const polygon: LatLng[] = [
	{ lat: 39.9075, lng: 116.3972 },
	{ lat: 39.90935, lng: 116.3972 },
	{ lat: 39.91019494, lng: 116.399753 },
	{ lat: 39.908991, lng: 116.401943 },
	// { lat: 39.906943, lng: 116.402071 },
	{ lat: 39.9057662, lng: 116.399448 },
	// { lat: 39.9075, lng: 116.3972 }
];
const polygonOptions = {
	polygon,
	spacing: 30,
	rotate: 0,
	margin: 0,
};
const polylineFn = () => {
    if (viewer) {
       viewer.entities.add({
          name: '弓字形航线',
          polyline: {
            positions:  new CallbackProperty(() => {
              const result = TurfCoveragePlanner.generate(polygonOptions);
              const positions = Cartesian3.fromDegreesArrayHeights(
                result.waypoints.map(p => [p.lng, p.lat, 10]).flat()
              );
              return positions;
            }, false),
            width: 2,
            material: Color.LIME,
            clampToGround: false
          }
        });
   }
};

如果想看如何动态更改平面形状,参照这个平移方法,通过实时更新polygonOptions的点坐标从而来更改弓字形航线的形状