Turf.js多边形如何生成航线?GIS自动规划实战技巧(含代码)

编程与开发
Dr.GIS
wowwwai GIS研习社 · 工具流程与项目排障

引言:如果你正在搜索“Turf.js多边形如何生成航线?GIS自动规划实战技巧(含代码)”,大概率是想把一个农田、湖面、巡检区域或无人机作业区的多边形,自动转换成一组平行的航线。这个需求在 WebGIS、无人机巡检、测绘外业规划和自动化空间分析中很常见,核心不是简单画几条线,而是要让航线覆盖多边形内部,并能控制间距、角度和边界裁剪。

本文用 Turf.js 演示一个可落地的方案:输入一个 GeoJSON 多边形,按指定方向生成平行扫描线,再用多边形裁剪得到内部航线。你可以把它接入 Leaflet、OpenLayers 或前端任务规划系统中。

Turf.js多边形生成航线与Turf.js航线规划示意图
Turf.js 多边形生成航线的基本思路:先生成覆盖范围内的平行线,再按多边形边界裁剪。

背景:为什么 Turf.js 多边形生成航线不能只靠画线

在很多 GIS 项目中,多边形代表一个作业区域,例如:

  • 无人机航测的农田地块。
  • 水域巡检的湖面边界。
  • 林区、矿区、园区的巡查范围。
  • 机器人或船舶的自动扫描区域。

用户通常希望输入一个多边形后,系统自动输出类似“割草机路径”的航线。也就是多条平行线按照固定间距覆盖整个区域。这类路径在 GIS 中常被称为扫描线、覆盖路径或作业航线。

但直接在地图上按经纬度画平行线会遇到几个问题:

  • 经纬度不是米,直接用度作为距离单位会导致间距不准。
  • 多边形可能是斜的,默认东西向或南北向航线不一定合理。
  • 航线必须被裁剪在多边形内部,不能飞出作业区。
  • 凹多边形、窄边、洞、多部件区域会产生断裂线段。
  • 前端 Turf.js 适合轻量规划,但不适合超大范围和高精度测量替代。

所以,Turf.js 多边形生成航线的正确思路是:先确定坐标与单位,再生成足够长的平行线,最后使用多边形裁剪得到有效航线。

原理:Turf.js 航线规划的核心流程

Turf.js 是一个常用的 JavaScript 空间分析库,可以在浏览器或 Node.js 中处理 GeoJSON 数据。它适合做缓冲区、距离、相交、裁剪、网格、面积等前端空间计算。

本文的 Turf.js 航线规划流程如下:

  1. 读取作业区 Polygon 或 MultiPolygon。
  2. 计算多边形的外包矩形 bbox。
  3. 根据 bbox 生成一组覆盖范围足够大的平行线。
  4. 将平行线按指定角度旋转。
  5. 用多边形裁剪平行线,只保留多边形内部线段。
  6. 输出 GeoJSON LineString 或 MultiLineString 作为航线。

这里有一个重要概念:航线间距必须使用真实距离单位,例如米。Turf.js 的很多距离函数支持 kilometers、meters、miles 等单位,但 GeoJSON 坐标通常仍然是 WGS84 经纬度。因此,中小范围项目可以使用 Turf.js 的测地计算;如果是大范围或高精度工程项目,建议先投影到本地米制坐标系后再生成航线。

简单理解:Turf.js 负责“空间计算”,GeoJSON 负责“数据表达”,而航线规划的关键是把距离、角度、裁剪三件事处理一致。

步骤:用 Turf.js 多边形生成航线的完整代码

1. 安装 Turf.js

如果你在前端项目中使用 npm,可以安装 Turf.js:

npm install @turf/turf

如果你只是做一个简单 HTML 页面,也可以通过 CDN 引入:

<script src="https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js"></script>

下面的示例采用模块写法,更适合 Vue、React、Vite 或 Node.js 环境。

2. 准备一个 GeoJSON 多边形

示例多边形如下,坐标为 WGS84 经纬度。实际项目中,它可以来自用户在 WebGIS 上绘制的面,也可以来自后端接口、数据库或 GeoJSON 文件。

import * as turf from "@turf/turf";

const polygon = turf.polygon([[
  [116.3800, 39.9000],
  [116.3860, 39.9010],
  [116.3880, 39.9060],
  [116.3820, 39.9080],
  [116.3770, 39.9040],
  [116.3800, 39.9000]
]]);

注意:GeoJSON 多边形的首尾坐标必须闭合,也就是第一个点和最后一个点相同。如果没有闭合,很多空间分析函数会出现异常或结果不稳定。

3. 生成覆盖 bbox 的基础平行线

航线要覆盖整个多边形,所以不能只在多边形内部直接画线。更稳妥的方法是先用 bbox 扩大范围,生成一批足够长的线,再统一裁剪。

function createBaseLinesByBbox(polygon, spacingMeters) {
  const bbox = turf.bbox(polygon);
  const [minX, minY, maxX, maxY] = bbox;

  const center = turf.center(polygon);
  const widthKm = turf.distance([minX, minY], [maxX, minY], { units: "kilometers" });
  const heightKm = turf.distance([minX, minY], [minX, maxY], { units: "kilometers" });

  const diagonalKm = Math.sqrt(widthKm * widthKm + heightKm * heightKm);
  const lineLengthKm = diagonalKm * 3;
  const spacingKm = spacingMeters / 1000;

  const lines = [];
  const lineCount = Math.ceil((diagonalKm * 2) / spacingKm);

  for (let i = -lineCount; i <= lineCount; i++) {
    const offsetKm = i * spacingKm;

    const start = turf.destination(center, lineLengthKm / 2, 270, { units: "kilometers" });
    const end = turf.destination(center, lineLengthKm / 2, 90, { units: "kilometers" });

    const movedStart = turf.destination(start, offsetKm, 0, { units: "kilometers" });
    const movedEnd = turf.destination(end, offsetKm, 0, { units: "kilometers" });

    lines.push(turf.lineString([
      movedStart.geometry.coordinates,
      movedEnd.geometry.coordinates
    ]));
  }

  return lines;
}

这段代码默认生成东西向线,再通过南北方向偏移形成多条平行线。后面我们会根据航线角度进行旋转。

4. 根据航线角度旋转平行线

很多作业区不是正南正北方向。比如一块农田整体倾斜 30 度,如果航线仍然水平生成,会产生更多短线段和转弯点。因此需要支持 angle 参数。

function rotateLines(lines, angle, pivot) {
  return lines.map(line => turf.transformRotate(line, angle, {
    pivot: pivot.geometry.coordinates
  }));
}

这里的 angle 单位是度。通常可以让用户在界面输入航线角度,例如 0、30、45、90。也可以通过多边形最长边自动估算一个推荐角度,但这属于进一步优化。

5. 裁剪航线,只保留多边形内部部分

Turf.js 本身没有一个在所有复杂场景下都完美的“线按面裁剪”单函数。一个实用方法是:将每条线与多边形边界求交点,再按线段切分,判断每个小线段的中点是否在多边形内。

下面给出一个可读性较好的实现。它适合普通 Polygon、多数凹多边形和常见前端航线预规划场景。

function clipLineByPolygon(line, polygon) {
  const boundary = turf.polygonToLine(polygon);
  const intersections = turf.lineIntersect(line, boundary);

  const coords = line.geometry.coordinates;
  const points = [
    turf.point(coords[0]),
    turf.point(coords[coords.length - 1])
  ];

  intersections.features.forEach(pt => points.push(pt));

  const sortedPoints = points
    .map(pt => {
      const snapped = turf.nearestPointOnLine(line, pt, { units: "kilometers" });
      return {
        point: snapped,
        location: snapped.properties.location
      };
    })
    .sort((a, b) => a.location - b.location);

  const segments = [];

  for (let i = 0; i < sortedPoints.length - 1; i++) {
    const p1 = sortedPoints[i].point;
    const p2 = sortedPoints[i + 1].point;

    const segment = turf.lineString([
      p1.geometry.coordinates,
      p2.geometry.coordinates
    ]);

    const length = turf.length(segment, { units: "meters" });
    if (length < 0.5) {
      continue;
    }

    const midpoint = turf.along(segment, length / 2, { units: "meters" });

    if (turf.booleanPointInPolygon(midpoint, polygon)) {
      segments.push(segment);
    }
  }

  return segments;
}

这一步是 Turf.js 多边形生成航线最容易出问题的地方。因为凹多边形可能让一条扫描线被切成多段,所以输出不是一条完整 LineString,而是一组航线线段。

6. 封装完整的航线生成函数

把上面的步骤组合起来,就可以得到一个完整函数:

function generateFlightLines(polygon, options = {}) {
  const spacingMeters = options.spacingMeters || 30;
  const angle = options.angle || 0;
  const minLengthMeters = options.minLengthMeters || 2;

  const center = turf.center(polygon);

  const baseLines = createBaseLinesByBbox(polygon, spacingMeters);
  const rotatedLines = rotateLines(baseLines, angle, center);

  const clippedSegments = [];

  rotatedLines.forEach(line => {
    const segments = clipLineByPolygon(line, polygon);
    segments.forEach(seg => {
      const len = turf.length(seg, { units: "meters" });
      if (len >= minLengthMeters) {
        seg.properties = {
          type: "flight-line",
          length_m: Number(len.toFixed(2)),
          spacing_m: spacingMeters,
          angle: angle
        };
        clippedSegments.push(seg);
      }
    });
  });

  return turf.featureCollection(clippedSegments);
}

const flightLines = generateFlightLines(polygon, {
  spacingMeters: 40,
  angle: 25,
  minLengthMeters: 5
});

console.log(JSON.stringify(flightLines, null, 2));

输出结果是一个 FeatureCollection,每个 Feature 是一条 LineString 航线。你可以直接把它添加到 Leaflet、OpenLayers 或 Mapbox GL JS 图层中。

7. 在 Leaflet 中显示航线

如果你使用 Leaflet,可以这样把结果加载到地图上:

L.geoJSON(polygon, {
  style: {
    color: "#2563eb",
    weight: 2,
    fillOpacity: 0.1
  }
}).addTo(map);

L.geoJSON(flightLines, {
  style: {
    color: "#f97316",
    weight: 2
  },
  onEachFeature: function (feature, layer) {
    layer.bindPopup(
      "长度:" + feature.properties.length_m + " 米<br>" +
      "间距:" + feature.properties.spacing_m + " 米<br>" +
      "角度:" + feature.properties.angle + "°"
    );
  }
}).addTo(map);

这样就完成了一个基础的 Turf.js 航线规划流程:多边形输入、航线生成、属性输出、地图展示。

常见坑:Turf.js 自动规划航线容易出错的地方

1. 经纬度坐标下直接按度计算间距

很多初学者会用 0.0001 度当作固定间距,这在 GIS 中不可靠。因为经度对应的实际距离会随纬度变化,同样的度数在北京和海南代表的米数不同。

建议使用 Turf.js 的 distance、destination、length 等函数,并明确设置 units 为 meters 或 kilometers。对于工程精度要求高的项目,建议后端使用 PostGIS、GDAL、GeoPandas 或 Shapely 在投影坐标系中处理。

2. 多边形没有闭合或几何无效

GeoJSON Polygon 的外环必须闭合。如果坐标顺序混乱、自相交或存在重复点,Turf.js 多边形生成航线可能得到空结果、断裂结果或异常结果。

处理前建议检查:

  • 首尾点是否一致。
  • 多边形是否自相交。
  • 是否存在过短边和重复点。
  • Polygon 与 MultiPolygon 是否被正确区分。

3. 线裁剪结果出现很多短碎线

凹多边形、锯齿状边界和复杂边界会切出很多短线。可以通过 minLengthMeters 过滤过短线段,也可以先对边界做适度简化。

const simplifiedPolygon = turf.simplify(polygon, {
  tolerance: 0.00001,
  highQuality: true
});

但简化会改变边界形状,不能用于权属边界、精确测绘成果等严肃场景。是否简化,要根据业务允许误差决定。

4. 航线角度和地图显示方向理解不一致

在 Turf.js 中,方位角通常以北为 0 度,顺时针增加。地图屏幕上的“水平线”“竖直线”与真实地理方向不一定完全一致,尤其在 Web Mercator 投影下更明显。

如果用户在界面上拖动方向线,建议把方向线的起点和终点转换成方位角,再传入航线生成函数。

5. MultiPolygon 没有拆分处理

如果作业区是 MultiPolygon,比如多个不相连地块,建议先拆分成多个 Polygon 分别生成航线,再合并结果。否则裁剪和排序可能变得复杂。

方法比较:前端 Turf.js、后端 PostGIS 与 Python GIS 怎么选

方法 适合场景 优点 限制
Turf.js 前端生成 WebGIS 交互预览、小范围航线规划、用户即时编辑 部署简单,反馈快,直接处理 GeoJSON 复杂几何和大数据量时性能有限
PostGIS 后端生成 生产系统、批量地块、数据库内空间计算 适合批处理,可结合空间索引和投影坐标系 开发和部署成本更高
Python GeoPandas 或 Shapely 离线处理、科研分析、批量任务规划 几何处理能力强,便于自动化脚本 不适合纯前端即时交互
专业无人机航线软件 真实飞行任务、相机参数、重叠率、航高控制 考虑飞行安全和设备参数 二次开发和 WebGIS 集成不一定方便

如果你的目标是“用户在网页上画一个面,立即看到航线效果”,Turf.js 是很合适的起点。如果目标是生产级无人机任务,还需要加入航高、相机视场角、旁向重叠率、航向重叠率、禁飞区、起降点和转弯半径等约束。

检查清单:上线前验证 Turf.js 多边形航线结果

  • 输入几何:确认 Polygon 闭合、无明显自相交、坐标顺序正确。
  • 坐标单位:确认间距、长度、偏移使用 meters 或 kilometers,而不是直接使用经纬度差值。
  • 航线角度:测试 0、45、90、135 度等典型角度,确认方向符合业务理解。
  • 边界裁剪:检查航线是否全部位于多边形内部,没有明显飞出边界。
  • 短线过滤:设置合理的 minLengthMeters,避免输出无意义碎线。
  • 复杂多边形:测试凹多边形、带窄边多边形、多地块场景。
  • 性能:测试最大作业区面积和最小航线间距,避免浏览器卡死。
  • 结果导出:确认输出 GeoJSON 属性中包含 spacing_m、angle、length_m 等必要字段。
  • 地图展示:在 Leaflet、OpenLayers 或 Mapbox GL JS 中检查样式、顺序和弹窗属性。
  • 业务复核:如果用于真实无人机飞行,必须交由专业飞控软件或安全规则再次校验。

FAQ:Turf.js 多边形生成航线常见问题

Turf.js 多边形如何生成航线最简单?

最简单的方法是:计算多边形 bbox,生成一组等间距平行线,按指定角度旋转,然后用多边形裁剪线段。本文代码就是这个思路,适合 WebGIS 中的航线预览和轻量自动规划。

Turf.js 航线规划能直接用于无人机飞行吗?

不建议直接用于真实飞行。Turf.js 可以生成空间几何意义上的航线,但真实无人机任务还需要考虑航高、速度、相机参数、重叠率、电池、地形、禁飞区、返航点和飞控格式。本文方案更适合作为 GIS 自动规划的基础模块。

为什么生成的航线间距看起来不均匀?

常见原因是直接用经纬度差值做间距,或者地图投影显示造成视觉差异。建议使用 Turf.js 的 destination 和 distance 按米计算。对于较大范围或高精度任务,应先转换到合适的投影坐标系。

凹多边形生成航线为什么会断成多段?

这是正常现象。一条扫描线穿过凹多边形时,可能进入、离开、再次进入多边形,因此会被裁剪成多条 LineString。后续如果要形成连续飞行路径,还需要做航线排序和连接。

如何让航线按照多边形长边方向生成?

可以提取多边形外环边界,计算每条边的长度和方位角,选择最长边的方位角作为航线角度。更高级的做法是计算最小外接矩形,并使用矩形长轴方向作为推荐航线方向。

Turf.js 可以处理带洞多边形吗?

可以处理 GeoJSON 中带内环的 Polygon,但线裁剪逻辑需要特别测试。对于带洞区域,航线不能穿过洞内区域。本文的中点判断方法在很多场景下可用,但复杂洞、多洞、多部件区域建议做更严格的几何验证。

生成航线后如何排序成连续作业路径?

本文重点是从多边形生成航线线段。若要形成连续路径,可以按照线段中心点在垂直方向上的位置排序,并采用蛇形顺序连接:第一条从左到右,第二条从右到左,依次交替。真实飞行还要考虑转弯半径和安全距离。

结论:Turf.js 适合做 WebGIS 航线自动规划的第一步

Turf.js 多边形生成航线的关键,不是简单把线画出来,而是处理好距离单位、航线角度、边界裁剪和结果验证。对于 WebGIS 中的前端交互预览,Turf.js 可以快速实现“画一个面,自动出航线”的效果。

实战中建议采用本文流程:先基于 bbox 生成足够长的平行线,再按角度旋转,最后按多边形裁剪并过滤短线。这样得到的 GeoJSON 航线结果结构清晰,便于在 Leaflet、OpenLayers 或后端服务中继续使用。

如果项目进入生产级无人机作业阶段,还需要把 Turf.js 航线规划结果与飞控参数、安全规则和专业航线格式结合起来。把 GIS 几何规划和飞行任务规划分开处理,系统会更可靠。