几何计算
SDK 提供了基础的几何计算功能,同时推荐使用专业的几何计算库(如 turf.js)来处理复杂的几何操作。
重要说明:航图SDK支持二维(平面投影)和三维(球面投影)两种模式。在航图应用中,所有涉及距离、方位、路径的计算都应使用大圆(Great Circle)计算,以确保计算结果的准确性。本文档提供的计算方法均基于球面几何学。
基础计算
计算两点距离(大圆距离)
在航图应用中,两点之间的距离应使用大圆距离(Great Circle Distance)计算,这是球面上两点之间的最短距离。
重要说明:
- 不是直线距离:这里计算的不是欧几里得空间中的直线距离(如穿过地球内部的直线),而是沿着地球表面的最短路径
- 大圆路径:大圆距离是球面上两点之间大圆弧的长度,这是实际飞行路径的最短距离
- 与投影无关:无论地图是2D(墨卡托投影)还是3D(Globe View)模式,大圆距离的计算结果都是一样的,因为计算直接基于WGS84经纬度坐标,不依赖于地图的投影方式
javascript
/**
* 计算两点之间的大圆距离(Haversine公式)
* @param {Array<number>} point1 - [经度, 纬度]
* @param {Array<number>} point2 - [经度, 纬度]
* @param {string} unit - 单位:"km"(公里)或 "nm"(海里),默认 "km"
* @returns {number} 距离(指定单位)
*/
function calculateDistance(point1, point2, unit = "km") {
const [lng1, lat1] = point1;
const [lng2, lat2] = point2;
// WGS84椭球体平均半径(公里)
// 注意:对于高精度应用,可以使用更精确的椭球体半径
const R_KM = 6371.0088; // 平均半径,约6371公里
const R_NM = R_KM / 1.852; // 海里 = 公里 / 1.852
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
// Haversine公式(基于球体模型)
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLng / 2) * Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distanceKm = R_KM * c;
// 单位转换:1 公里 = 0.539957 海里
if (unit === "nm" || unit === "NM") {
return distanceKm / 1.852; // 返回海里
}
return distanceKm; // 返回公里
}
// 使用示例
const distanceKm = calculateDistance(
[116.3974, 39.9093], // 北京
[121.4737, 31.2304], // 上海
"km"
);
console.log("距离:", distanceKm.toFixed(2), "公里");
// 航图应用推荐:使用海里(NM)作为标准单位
const distanceNm = calculateDistance(
[116.3974, 39.9093],
[121.4737, 31.2304],
"nm"
);
console.log("距离:", distanceNm.toFixed(2), "海里 (NM)");计算方位角(大圆方位角)
在航图应用中,方位角应使用大圆方位角(Great Circle Bearing)计算,这是从起点到终点的初始方向。
javascript
/**
* 计算两点之间的大圆方位角(初始方位角)
* @param {Array<number>} point1 - 起点 [经度, 纬度]
* @param {Array<number>} point2 - 终点 [经度, 纬度]
* @returns {number} 真方位角(度,0-360,0度为正北,顺时针增加)
*
* 注意:此函数返回的是真方位角(True Bearing),相对于地理正北。
* 如需磁方位角(Magnetic Bearing),需要根据当前位置的磁偏角(Magnetic Variation)进行修正。
* 磁偏角可通过 IGRF(国际地磁参考场)模型获取。
*/
function calculateBearing(point1, point2) {
const [lng1, lat1] = point1;
const [lng2, lat2] = point2;
const dLng = (lng2 - lng1) * Math.PI / 180;
const lat1Rad = lat1 * Math.PI / 180;
const lat2Rad = lat2 * Math.PI / 180;
// 大圆方位角计算公式
const y = Math.sin(dLng) * Math.cos(lat2Rad);
const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) -
Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLng);
const bearing = Math.atan2(y, x) * 180 / Math.PI;
return (bearing + 360) % 360; // 转换为 0-360 度
}
/**
* 将真方位角转换为磁方位角
* @param {number} trueBearing - 真方位角(度)
* @param {number} magneticVariation - 磁偏角(度,东偏为正,西偏为负)
* @returns {number} 磁方位角(度)
*/
function trueToMagnetic(trueBearing, magneticVariation) {
return (trueBearing - magneticVariation + 360) % 360;
}
// 注意:大圆方位角会随着位置变化而变化(除了沿赤道或经线)
// 如果需要计算终点的反方位角,需要交换起点和终点
// 使用示例
const trueBearing = calculateBearing(
[116.3974, 39.9093],
[121.4737, 31.2304]
);
console.log("真方位角:", trueBearing.toFixed(2), "度");
// 示例:转换为磁方位角(假设磁偏角为 -5.5度,即西偏)
const magneticVariation = -5.5; // 需要从 IGRF 模型获取实际值
const magneticBearing = trueToMagnetic(trueBearing, magneticVariation);
console.log("磁方位角:", magneticBearing.toFixed(2), "度");计算中点(大圆中点)
在球面上,简单计算经纬度平均值得到的中点不是真正的大圆中点。对于航图应用,应使用大圆中点计算。
javascript
/**
* 计算两点之间的大圆中点
* 注意:这不是简单的经纬度平均值,而是球面上大圆弧的中点
* @param {Array<number>} point1 - [经度, 纬度]
* @param {Array<number>} point2 - [经度, 纬度]
* @returns {Array<number>|null} 中点坐标 [经度, 纬度],如果两点为对跖点则返回 null
*
* 警告:如果两点恰好在地球的正对面(对跖点,antipodal points),
* 大圆路径有无数条,此时函数可能产生数学上的不确定值。
*/
function calculateMidpoint(point1, point2) {
const [lng1, lat1] = point1;
const [lng2, lat2] = point2;
// 检查是否为对跖点(两点相距约180度)
const distance = calculateDistance(point1, point2, "km");
if (distance > 20000) { // 约等于地球周长的一半
console.warn("警告:两点可能为对跖点,大圆中点计算可能不准确");
}
// 转换为弧度
const lat1Rad = lat1 * Math.PI / 180;
const lng1Rad = lng1 * Math.PI / 180;
const lat2Rad = lat2 * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
// 计算大圆中点
const Bx = Math.cos(lat2Rad) * Math.cos(dLng);
const By = Math.cos(lat2Rad) * Math.sin(dLng);
const midLat = Math.atan2(
Math.sin(lat1Rad) + Math.sin(lat2Rad),
Math.sqrt((Math.cos(lat1Rad) + Bx) ** 2 + By ** 2)
);
const midLng = lng1Rad + Math.atan2(By, Math.cos(lat1Rad) + Bx);
return [
midLng * 180 / Math.PI,
midLat * 180 / Math.PI,
];
}
// 使用示例
const midpoint = calculateMidpoint(
[116.3974, 39.9093],
[121.4737, 31.2304]
);
console.log("大圆中点:", midpoint);
// 注意:对于短距离(<100公里),简单平均值误差较小
// 但对于长距离或高精度应用,必须使用大圆中点
## 使用 Turf.js
推荐使用 [Turf.js](https://turfjs.org/) 进行复杂的几何计算。
### 安装 Turf.js
```bash
npm install @turf/turf距离计算
javascript
import * as turf from "@turf/turf";
const point1 = turf.point([116.3974, 39.9093]);
const point2 = turf.point([121.4737, 31.2304]);
// 计算距离(公里)
const distanceKm = turf.distance(point1, point2, { units: "kilometers" });
console.log("距离:", distanceKm.toFixed(2), "公里");
// 航图应用推荐:使用海里(NM)作为标准单位
// Turf.js 支持 "nauticalmiles" 单位
const distanceNm = turf.distance(point1, point2, { units: "nauticalmiles" });
console.log("距离:", distanceNm.toFixed(2), "海里 (NM)");重要说明:
turf.distance在2D和3D模式下表现一致:turf.distance直接基于 WGS84 经纬度坐标进行计算,不依赖于地图的投影模式(2D墨卡托投影或3D Globe View)- 计算原理:Turf.js 内部使用 Haversine 公式或类似算法,直接处理地理坐标,与地图如何显示(投影)无关
- 实际应用:无论地图当前是2D模式还是3D模式,
turf.distance的计算结果都是相同的,因为计算的是实际的地理距离(大圆距离),而不是屏幕上的视觉距离
面积计算
javascript
import * as turf from "@turf/turf";
const polygon = turf.polygon([
[
[116.3974, 39.9093],
[121.4737, 39.9093],
[121.4737, 31.2304],
[116.3974, 31.2304],
[116.3974, 39.9093],
],
]);
// 计算面积(平方米)
const area = turf.area(polygon);
console.log("面积:", area, "平方米");
console.log("面积:", area / 1000000, "平方公里");中心点计算
javascript
import * as turf from "@turf/turf";
const polygon = turf.polygon([...]);
const centroid = turf.centroid(polygon);
console.log("中心点:", centroid.geometry.coordinates);边界框计算
javascript
import * as turf from "@turf/turf";
const points = turf.featureCollection([
turf.point([116.3974, 39.9093]),
turf.point([121.4737, 31.2304]),
turf.point([113.2644, 23.1291]),
]);
const bbox = turf.bbox(points);
console.log("边界框:", bbox); // [minLng, minLat, maxLng, maxLat]
// 转换为 GeoJSON 边界框
const bboxPolygon = turf.bboxPolygon(bbox);点在多边形内判断
javascript
import * as turf from "@turf/turf";
const point = turf.point([116.3974, 39.9093]);
const polygon = turf.polygon([...]);
const isInside = turf.booleanPointInPolygon(point, polygon);
console.log("点是否在多边形内:", isInside);缓冲区计算
javascript
import * as turf from "@turf/turf";
const point = turf.point([116.3974, 39.9093]);
// 创建缓冲区(半径10公里)
const buffered = turf.buffer(point, 10, { units: "kilometers" });
// 添加到地图
sdk.addGeoJSON("buffer-source", buffered);
sdk.addLayer({
id: "buffer-layer",
source: "buffer-source",
type: "fill",
paint: {
"fill-color": "#007bff",
"fill-opacity": 0.3,
},
});线长度计算
javascript
import * as turf from "@turf/turf";
const line = turf.lineString([
[116.3974, 39.9093],
[121.4737, 31.2304],
[113.2644, 23.1291],
]);
// 计算长度(公里)
const lengthKm = turf.length(line, { units: "kilometers" });
console.log("长度:", lengthKm.toFixed(2), "公里");
// 航图应用推荐:使用海里
const lengthNm = turf.length(line, { units: "nauticalmiles" });
console.log("长度:", lengthNm.toFixed(2), "海里 (NM)");简化几何
javascript
import * as turf from "@turf/turf";
const line = turf.lineString([...]);
// 简化线(减少点数)
const simplified = turf.simplify(line, { tolerance: 0.01, highQuality: false });⚠️ 重要警告:在航图业务中,对限制区(Restricted Area)、管制区(Controlled Airspace)或飞行程序(Procedure)的边界进行简化是非常危险的,可能导致:
- 视觉上的侵入判定错误
- 飞行安全风险
- 法规合规问题
建议:仅在非关键业务场景(如可视化展示、性能优化)中使用几何简化,且必须经过业务验证。
测量工具
使用 MeasurePlugin
SDK 提供了内置的测量插件:
javascript
// 创建测量插件
const measurePlugin = new navMap.MeasurePlugin();
sdk.use(measurePlugin);
// 启用距离测量
measurePlugin.enable("distance");
// 启用面积测量
measurePlugin.enable("area");
// 禁用测量
measurePlugin.disable();自定义测量工具
javascript
class CustomMeasureTool {
constructor(sdk) {
this.sdk = sdk;
this.points = [];
this.setupEvents();
}
setupEvents() {
this.sdk.on("click", (e) => {
if (this.isActive) {
this.addPoint(e.lngLat);
}
});
}
addPoint(lngLat) {
this.points.push([lngLat.lng, lngLat.lat]);
// 添加点标记
this.sdk.addPoint(
[lngLat.lng, lngLat.lat],
{},
{
paint: {
"circle-radius": 6,
"circle-color": "#ff0000",
},
}
);
// 如果有多个点,绘制连线
if (this.points.length > 1) {
this.sdk.addLine(
this.points.slice(-2),
{},
{
paint: {
"line-color": "#ff0000",
"line-width": 2,
},
}
);
}
// 计算距离
if (this.points.length >= 2) {
const distance = this.calculateTotalDistance();
this.updateDistanceDisplay(distance);
}
}
calculateTotalDistance() {
let totalDistance = 0;
for (let i = 1; i < this.points.length; i++) {
totalDistance += calculateDistance(this.points[i - 1], this.points[i]);
}
return totalDistance;
}
updateDistanceDisplay(distance) {
// 更新UI显示距离
document.getElementById("distance-display").textContent =
`总距离: ${distance.toFixed(2)} 公里`;
}
clear() {
this.points.forEach((point, index) => {
this.sdk.removeLayer(`measure-point-${index}`);
if (index > 0) {
this.sdk.removeLayer(`measure-line-${index - 1}`);
}
});
this.points = [];
}
enable() {
this.isActive = true;
}
disable() {
this.isActive = false;
this.clear();
}
}
// 使用
const measureTool = new CustomMeasureTool(sdk);
measureTool.enable();坐标转换
屏幕坐标转地理坐标
注意:坐标转换的结果取决于当前地图的投影模式:
- 二维模式(平面投影):使用墨卡托投影或其他平面投影
- 三维模式(球面投影):使用球面投影,坐标转换会考虑地球曲率
javascript
/**
* 屏幕像素坐标转地理坐标
* @param {Object} pixel - { x: number, y: number }
* @returns {Object} { lng: number, lat: number }
*/
function pixelToLngLat(pixel) {
return sdk.map.unproject(pixel);
}
// 使用示例
const lngLat = pixelToLngLat({ x: 100, y: 200 });
console.log("地理坐标:", lngLat);
// 检查当前投影模式
function getProjectionType() {
const projection = sdk.map.getProjection();
return projection && projection.type === "globe" ? "3D" : "2D";
}地理坐标转屏幕坐标
javascript
/**
* 地理坐标转屏幕像素坐标
* @param {Array<number>} lngLat - [经度, 纬度]
* @returns {Object} { x: number, y: number }
*/
function lngLatToPixel(lngLat) {
return sdk.map.project(lngLat);
}
// 使用示例
const pixel = lngLatToPixel([116.3974, 39.9093]);
console.log("屏幕坐标:", pixel);
// 注意:在3D模式下,投影结果会因视角(pitch、bearing)而变化
// 在2D模式下,投影结果相对稳定完整示例
javascript
import * as turf from "@turf/turf";
let sdk;
async function initMap() {
sdk = new navMap.MapSDK({
container: "map",
center: [116.39, 39.9],
zoom: 10,
});
sdk.on("loadComplete", () => {
setupGeometryTools();
});
}
function setupGeometryTools() {
// 1. 计算两点距离
const distance = calculateDistance(
[116.3974, 39.9093],
[121.4737, 31.2304]
);
console.log("北京到上海距离:", distance, "公里");
// 2. 使用 Turf.js 计算面积
const polygon = turf.polygon([
[
[116.3974, 39.9093],
[121.4737, 39.9093],
[121.4737, 31.2304],
[116.3974, 31.2304],
[116.3974, 39.9093],
],
]);
const area = turf.area(polygon) / 1000000; // 转换为平方公里
console.log("多边形面积:", area, "平方公里");
// 3. 创建缓冲区
const point = turf.point([116.3974, 39.9093]);
const buffer = turf.buffer(point, 50, { units: "kilometers" });
sdk.addGeoJSON("buffer-source", buffer);
sdk.addLayer({
id: "buffer-layer",
source: "buffer-source",
type: "fill",
paint: {
"fill-color": "#007bff",
"fill-opacity": 0.3,
},
});
// 4. 判断点是否在多边形内
const testPoint = turf.point([118.0, 35.0]);
const isInside = turf.booleanPointInPolygon(testPoint, polygon);
console.log("点是否在多边形内:", isInside);
// 5. 使用测量插件
const measurePlugin = new navMap.MeasurePlugin();
sdk.use(measurePlugin);
measurePlugin.enable("distance");
}
initMap();二维与三维模式下的计算差异
投影模式影响
SDK支持两种投影模式:
- 二维模式(平面投影):使用平面投影(如墨卡托投影),适合局部区域显示
- 三维模式(Globe View):使用球面投影(Globe View),适合全球范围显示
术语说明:在 MapLibre/Mapbox 中,"Globe View" 指的是球面投影视图模式,与"带地形高度的3D视图"(Terrain/Pitch)是不同的概念。本文档中的"三维模式"特指 Globe View。
计算注意事项
距离计算:
- 无论哪种模式,都应使用大圆距离计算,因为地理坐标始终基于球面
turf.distance和自定义calculateDistance函数在2D和3D模式下计算结果完全相同- 这些函数直接基于 WGS84 经纬度坐标计算,不依赖于地图投影方式
- 计算的是实际地理距离(沿地球表面的最短路径),不是屏幕上的视觉距离
方位角计算:应使用大圆方位角,这是航图应用的标准
中点计算:应使用大圆中点,简单平均值在球面上不准确
坐标转换:屏幕坐标与地理坐标的转换会因投影模式而异
- 2D模式(墨卡托投影):坐标转换基于平面投影
- 3D模式(Globe View):坐标转换基于球面投影,会考虑视角(pitch、bearing)
面积计算:在球面上计算面积时,应考虑地球曲率的影响
turf.area在2D和3D模式下计算结果相同,因为它直接基于地理坐标计算
模式检测
javascript
// 检测当前是否为3D模式
function is3DMode() {
const projection = sdk.map.getProjection();
return projection && projection.type === "globe";
}
// 根据模式选择计算方法
function calculateDistanceAdaptive(point1, point2) {
// 无论2D还是3D模式,都使用大圆距离
// 因为地理坐标始终基于球面
return calculateDistance(point1, point2);
}注意事项
坐标系统与单位标准
- 坐标系统: 严格遵循 WGS84 坐标系(World Geodetic System 1984)
- 单位标准:
- 距离单位:推荐使用海里(Nautical Mile, NM),这是航图领域的法定单位
- 面积单位:推荐使用平方海里(Square Nautical Mile)
- 单位换算:1 公里 = 0.539957 海里,1 海里 = 1.852 公里
精度算法模型
- 球体 vs 椭球体模型:
- Haversine公式(本文档示例):基于球体模型,适用于一般 EFB 或 H5 应用
- 精度限制:Haversine 在长距离(如跨洋航线)上的误差可达 0.5%
- 高精度需求:对于需要极高精度(厘米级)的航向计算,建议:
- 集成 Vincenty 公式(基于 WGS84 椭球体)
- 使用 Turf.js 的内置方法(Turf 内部处理了部分椭球体差异)
- 对于 PBN(性能导航)相关的精密计算,请勿仅依赖前端 Haversine 函数,应以服务端 ARINC 424 数据处理结果为准
方位角与磁偏角
- 真方位角 vs 磁方位角:
- 本文档计算的是真方位角(True Bearing),相对于地理正北
- 飞行员在驾驶舱看到的是磁方位角(Magnetic Bearing)
- 磁偏角修正:实际飞行作业需考虑 IGRF(国际地磁参考场)模型提供的磁偏角修正
- 磁偏角公式:磁方位角 = 真方位角 - 磁偏角(东偏为正,西偏为负)
大圆计算
- 大圆计算: 航图应用中的所有距离、方位、路径计算都应使用大圆计算
投影形变
- 投影形变:
- 在 2D 墨卡托投影下,高纬度地区的视觉距离会变长(投影形变)
- 但
calculateDistance和turf.distance计算的是实际地理距离(大圆距离),不受投影影响 - 重要:无论地图是2D还是3D模式,距离计算结果都是一样的,因为计算直接基于地理坐标,与投影无关
地球半径
- 地球半径:
- 平均半径:6371.0088 公里(适用于大多数情况)
- 对于高精度应用,可以使用 WGS84 椭球体的精确半径
- Turf.js 默认使用 6371008.8 米(约 6371.0088 公里)
精度考虑
- 精度考虑:
- 大范围计算时注意精度问题
- Haversine 公式在短距离(<100公里)精度较高
- 对于极长距离,考虑使用更精确的 Vincenty 公式
性能与库选择
- 性能优化: 复杂几何计算考虑使用 Web Worker
- 库选择: 推荐使用 Turf.js 等专业库处理复杂几何操作,它们已实现正确的大圆计算
投影模式
- 投影模式: 注意 2D 和 3D(Globe View)模式下坐标转换的差异,但几何计算本身应始终基于球面
