第08章 坐标换算算法详解
8.1 坐标系统概述
map-360-demo 涉及两套坐标系统:
- 全景球形坐标:yaw(偏航角,水平方向)和 pitch(俯仰角,垂直方向),单位弧度
- GPS 经纬度坐标:经度(lng)和纬度(lat),单位度
实现全景与地图联动,核心就是在这两套坐标之间进行换算。换算逻辑封装在 src/utils/geo.ts(共 94 行)。
8.2 辅助函数
// geo.ts:3-9
function round6(n: number): number {
return Math.round(n * 1e6) / 1e6 // 6 位小数
}
function clamp(n: number, min: number, max: number): number {
return Math.min(max, Math.max(min, n))
}
round6:保留 6 位小数,用于坐标精度控制clamp:数值钳制
8.3 方位角解析
// geo.ts:12-15
function parseBearing(bearing: string): number {
return parseFloat(bearing) // 解析 '120deg' 为 120
}
场景的 bearing 字段是字符串形式(如 '120deg'),需要解析为角度数。
8.4 米/度换算
// geo.ts:18-23
const M_PER_DEG_LAT = 110574 // 每度纬度米数
function mPerDegLng(lat: number): number {
return 111320 * Math.cos(lat * Math.PI / 180) // 每度经度米数随纬度收缩
}
关键概念:
- 每度纬度对应的地面距离基本恒定(约 110574 米)
- 每度经度对应的地面距离随纬度收缩(
111320 * cos(lat))
重要:经度/纬度每度地面距离不同(相差 cos(纬度) 因子),必须先在”米”空间按方位角分解再换算回度数,否则标记落在地图上的实际方向会偏转(中纬度可达 10°)。
8.5 距离与方位角计算
8.5.1 两点距离
// geo.ts:26-30
function distanceMeters(a: [number, number], b: [number, number]): number {
const [lng1, lat1] = a
const [lng2, lat2] = b
const east = (lng2 - lng1) * mPerDegLng(lat1)
const north = (lat2 - lat1) * M_PER_DEG_LAT
return Math.hypot(east, north) // 平面近似
}
将经纬度差换算为米,再用 Math.hypot 计算平面距离。
8.5.2 方位角
// geo.ts:33-37
function bearingBetween(a: [number, number], b: [number, number]): number {
const [lng1, lat1] = a
const [lng2, lat2] = b
const east = (lng2 - lng1) * mPerDegLng(lat1)
const north = (lat2 - lat1) * M_PER_DEG_LAT
return (Math.atan2(east, north) * 180) / Math.PI // 顺时针自北
}
a→b 地理方位角(度,顺时针自北),atan2(east, north)。
8.5.3 目标点
// geo.ts:40-51
function destination(a: [number, number], bearingDeg: number, distM: number): [number, number] {
const [lng, lat] = a
const bearingRad = (bearingDeg * Math.PI) / 180
const east = distM * Math.sin(bearingRad)
const north = distM * Math.cos(bearingRad)
return [
round6(lng + east / mPerDegLng(lat)),
round6(lat + north / M_PER_DEG_LAT),
]
}
从起点沿方位角移动指定距离后的目标点,先分解到米空间再换算回度数。
8.6 全景 yaw 与地理方位角
// geo.ts:57-59
function geoBearingOf(scene: Scene, yaw: number): number {
return parseBearing(scene.bearing) + (yaw * 180) / Math.PI
}
全景 yaw 对应的地理方位角 = scene.bearing + yaw(与 PlanPlugin 镜头指向公式一致)。
8.7 全景点击 → 经纬度(estimateGps)
// geo.ts:70-79
export function estimateGps(scene: Scene, yaw: number, pitch: number): [number, number] {
const bearingRad = (geoBearingOf(scene, yaw) * Math.PI) / 180
const distM = clamp(450 + pitch * 800, 120, 1100) // 典型 pitch -0.1~0.32 → 370m~710m
const [lng, lat] = scene.coordinates
return [
round6(lng + (distM * Math.sin(bearingRad)) / mPerDegLng(lat)),
round6(lat + (distM * Math.cos(bearingRad)) / M_PER_DEG_LAT),
]
}
算法流程:
- 计算 yaw 对应的地理方位角
- 根据 pitch 估算距离(
450 + pitch * 800,钳制到 [120, 1100] 米) - 在米空间按方位角分解距离
- 换算回经纬度
距离估算:典型 pitch -0.1~0.32 → 370m~710m,模拟了”点击位置离拍摄点越远,pitch 越小”的透视关系。
8.8 地图坐标 → 全景 yaw(yawFromGps)
// geo.ts:85-94
export function yawFromGps(scene: Scene, coords: [number, number]): number {
const [lng, lat] = scene.coordinates
const east = (coords[0] - lng) * mPerDegLng(lat)
const north = (coords[1] - lat) * M_PER_DEG_LAT
const angleDeg = (Math.atan2(east, north) * 180) / Math.PI
const yawDeg = (angleDeg - parseBearing(scene.bearing) + 360) % 360
const yawRad = (yawDeg * Math.PI) / 180
return yawRad > Math.PI ? yawRad - 2 * Math.PI : yawRad // 归一到 (-π, π]
}
算法流程:
- 换算米求真实方位角
- 扣除场景 bearing
- 归一到 PSV 的 yaw 习惯范围
(-π, π]
8.9 换算关系总结
全景球形坐标 (yaw, pitch)
│
├── estimateGps(scene, yaw, pitch) ──→ GPS 经纬度
│
└── yawFromGps(scene, coords) ──→ 全景 yaw
| 方向 | 函数 | 关键逻辑 |
|---|---|---|
| 全景 → 地图 | estimateGps | 方位角 + 距离估算 + 米空间分解 |
| 地图 → 全景 | yawFromGps | 米空间方位角 - 场景 bearing + 归一化 |
核心要点:
- 米空间分解:经度/纬度每度地面距离不同,必须先在米空间分解
- 场景 bearing 偏移:全景 yaw 与地理方位角相差场景的 bearing
- 归一化:yaw 归一到 PSV 习惯的
(-π, π]范围
8.10 下一步
理解了坐标换算后,进入 第09章 样式与交互细节 了解样式与交互的实现细节。