根据二维平面坐标生成弓子形航线
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的点坐标从而来更改弓字形航线的形状