You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Qwik滑翔伞任务地图:如何绘制多圆间的最短路径

实现思路:相邻圆的最短路径(外公切线切点连线)

你需要解决的核心是计算两个相邻圆的外公切线切点,用这些切点连线代替圆心连线,这就是依次经过所有圆的最短路径。以下是具体实现步骤:

1. 核心数学逻辑(球面坐标系下)

因为你的坐标是经纬度(球面坐标),不能直接用平面几何计算,需要基于球面三角函数推导:

  • 第一步:计算两圆心的距离与方位角
    用Haversine公式计算两圆心(tp1, tp2)的球面距离d,再计算从tp1圆心指向tp2圆心的方位角α(即正北方向到圆心连线的夹角,范围0-360°)。
  • 第二步:计算切线夹角
    设两圆半径分别为r1、r2,计算外公切线与圆心连线的夹角θ:
    // 注意:d的单位要和r1、r2一致(比如都转成米)
    const theta = Math.acos((r1 - r2) / d);
    
    如果两圆半径相等(r1 === r2),则θ = Math.PI / 2(90°),切线与圆心连线垂直。
  • 第三步:计算切点的方位角
    从tp1圆心出发,切点的方位角为α ± θ;对应tp2圆心的切点方位角为α ± θ + Math.PI(180°)。这里要根据任务前进方向选择正确的符号(比如取符合整体路径走向的那一组)。
  • 第四步:计算切点的经纬度
    基于圆心坐标、半径、切点方位角,用球面坐标偏移公式计算出两个切点的经纬度。

2. 代码实现步骤

(1)封装切点计算函数

先写一个工具函数,输入两个turnpoint对象,返回两个切点的坐标数组:

// 地球半径(米)
const EARTH_RADIUS = 6371000;

// 计算两点的球面距离(米)和方位角(弧度)
function getDistanceAndBearing(lat1, lng1, lat2, lng2) {
  const φ1 = lat1 * Math.PI / 180;
  const φ2 = lat2 * Math.PI / 180;
  const Δφ = (lat2 - lat1) * Math.PI / 180;
  const Δλ = (lng2 - lng1) * Math.PI / 180;

  // Haversine公式计算距离
  const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
            Math.cos(φ1) * Math.cos(φ2) *
            Math.sin(Δλ/2) * Math.sin(Δλ/2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
  const distance = EARTH_RADIUS * c;

  // 计算方位角(从点1到点2)
  const bearing = Math.atan2(Math.sin(Δλ)*Math.cos(φ2),
                            Math.cos(φ1)*Math.sin(φ2) -
                            Math.sin(φ1)*Math.cos(φ2)*Math.cos(Δλ));

  return { distance, bearing };
}

// 从圆心、方位角、半径计算切点坐标
function getPointFromBearing(lat, lng, bearing, distance) {
  const φ1 = lat * Math.PI / 180;
  const λ1 = lng * Math.PI / 180;
  const θ = bearing;
  const d = distance / EARTH_RADIUS;

  const φ2 = Math.asin( Math.sin(φ1)*Math.cos(d) +
                        Math.cos(φ1)*Math.sin(d)*Math.cos(θ) );
  const λ2 = λ1 + Math.atan2( Math.sin(θ)*Math.sin(d)*Math.cos(φ1),
                              Math.cos(d) - Math.sin(φ1)*Math.sin(φ2) );

  return [φ2 * 180 / Math.PI, λ2 * 180 / Math.PI];
}

// 获取两个圆的外公切线切点(从tp1到tp2的路径)
function getTangentPoints(tp1, tp2) {
  const { lat: lat1, lon: lng1 } = tp1.waypoint;
  const { lat: lat2, lon: lng2 } = tp2.waypoint;
  const r1 = tp1.radius;
  const r2 = tp2.radius;

  const { distance, bearing } = getDistanceAndBearing(lat1, lng1, lat2, lng2);
  
  // 计算切线夹角(弧度)
  let theta;
  if (r1 === r2) {
    theta = Math.PI / 2;
  } else {
    theta = Math.acos((r1 - r2) / distance);
  }

  // 取其中一组切线(如果方向不对,把+theta改成-theta)
  const bearing1 = bearing + theta;
  const bearing2 = bearing + theta + Math.PI;

  // 计算两个切点
  const point1 = getPointFromBearing(lat1, lng1, bearing1, r1);
  const point2 = getPointFromBearing(lat2, lng2, bearing2, r2);

  return [point1, point2];
}

(2)修改原有绘制代码

把原来连接圆心的polyline替换成连接切点的线段:

task?.turnpoints.forEach((tp, index) => {
  // 绘制圆的代码不变
  circle([tp.waypoint.lat, tp.waypoint.lon], {
    radius: tp.radius,
    color: "orange",
  }).addTo(mapContainer$.value!);

  if (index < task.turnpoints.length - 1) {
    const nextTp = task.turnpoints[index + 1];
    // 获取两个切点
    const [startPoint, endPoint] = getTangentPoints(tp, nextTp);
    // 绘制切点连线
    polyline(
      [startPoint, endPoint],
      { color: "black" },
    ).addTo(mapContainer$.value!);
  }
});

3. 边界情况处理

  • 如果两个圆相交:此时最短路径是两圆的相交弦(直接穿过交点),可以判断两圆心距离d < r1 + r2时,计算相交点代替切点。
  • 如果后一个圆包含前一个圆:这种情况在滑翔伞任务中很少见,但如果出现,最短路径是从第一个圆的切点指向第二个圆的圆心方向的切点。

内容的提问来源于stack exchange,提问作者jules sang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 09:35:16