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
相关产品推荐
相关产品推荐

