HarmonyOS6 map.calculateDistance vs Haversine:两种距离计算方案对比
文章目录
- 前言
- 一、为什么经纬度不能直接算距离?
- 1.1 平面距离的错误示范
- 1.2 经纬度与实际距离的关系
- 二、Haversine 公式:球面距离的标准算法
- 2.1 公式推导(简化版)
- 2.2 TypeScript 实现
- 三、本项目实现:map.calculateDistance
- 3.1 源码分析
- 3.2 map.calculateDistance vs 自实现 Haversine 对比
- 四、距离相关的实用工具函数
- 4.1 排序、过滤、格式化
- 五、Haversine 精度说明
- 总结
前言
本项目用CalculateUtil.getDistance()计算用户与加油站的距离,内部调用了map.calculateDistance()。你有没有想过:为什么不直接用Math.sqrt((lat2-lat1)² + (lng2-lng1)²)来算?
答案是:地球是球体,不是平面。在球面上,经纬度的度数并不直接等于距离,必须用专门的球面距离公式——Haversine 公式。本篇从数学原理到代码实现,带你彻底搞懂地理距离计算。
一、为什么经纬度不能直接算距离?
1.1 平面距离的错误示范
// ❌ 错误:经纬度差不是实际距离(单位也不对)functionwrongDistance(lat1:number,lng1:number,lat2:number,lng2:number):number{constdlat=lat2-lat1;constdlng=lng2-lng1;returnMath.sqrt(dlat*dlat+dlng*dlng);// 这只是"度数差",不是公里}wrongDistance(39.9,116.4,40.0,116.5);// 结果约 0.141,但实际距离约 15km!// 为什么错?// 问题1:纬度1度 ≈ 111km(在地球任意位置都差不多)// 问题2:经度1度的实际距离随纬度变化!// 赤道(0°):经度1度 ≈ 111km// 北京(40°N):经度1度 ≈ 85km// 北极(90°N):经度1度 ≈ 0km// 地球不是平面,而是球体,需要球面几何1.2 经纬度与实际距离的关系
| 纬度(°N) | 纬度1°≈km | 经度1°≈km |
|---|---|---|
| 0(赤道) | 110.6 | 111.3 |
| 30(上海) | 110.9 | 96.4 |
| 40(北京) | 111.0 | 85.4 |
| 60 | 111.2 | 55.8 |
| 90(北极) | 111.7 | 0 |
二、Haversine 公式:球面距离的标准算法
2.1 公式推导(简化版)
Haversine 公式基于球面三角学,计算球面上两点之间的大圆距离(最短路径):
设: φ1, φ2 = 两点纬度(弧度) λ1, λ2 = 两点经度(弧度) R = 地球半径(6371km) Δφ = φ2 - φ1(纬度差) Δλ = λ2 - λ1(经度差) a = sin²(Δφ/2) + cos(φ1) × cos(φ2) × sin²(Δλ/2) c = 2 × atan2(√a, √(1-a)) d = R × c (单位:km)2.2 TypeScript 实现
/** * Haversine 公式计算两点球面距离 * @param lat1 起点纬度(度) * @param lng1 起点经度(度) * @param lat2 终点纬度(度) * @param lng2 终点经度(度) * @returns 距离(公里) */functionhaversineDistance(lat1:number,lng1:number,lat2:number,lng2:number):number{constR=6371;// 地球平均半径(km)// 将度转换为弧度consttoRad=(deg:number):number=>deg*Math.PI/180;constφ1=toRad(lat1);constφ2=toRad(lat2);constΔφ=toRad(lat2-lat1);constΔλ=toRad(lng2-lng1);// Haversine 公式核心consta=Math.sin(Δφ/2)*Math.sin(Δφ/2)+Math.cos(φ1)*Math.cos(φ2)*Math.sin(Δλ/2)*Math.sin(Δλ/2);constc=2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));returnR*c;// 返回距离(km)}// 测试:北京天安门 → 上海外滩constdistance=haversineDistance(39.9087,116.3975,31.2365,121.4897);console.log(`北京到上海直线距离:${distance.toFixed(0)}km`);// 约 1066km(实际约1067km)// 测试:同一地点距离为0console.log(haversineDistance(39.9,116.4,39.9,116.4));// 0// 测试:非常近的距离(1km以内)constnearDist=haversineDistance(39.9042,116.4074,39.9092,116.4124);console.log(`附近距离:${(nearDist*1000).toFixed(0)}m`);// 约 671m三、本项目实现:map.calculateDistance
3.1 源码分析
// CalculateUtil.etsimport{map,mapCommon}from'@kit.MapKit';exportclassCalculateUtil{publicstaticgetDistance(lat1:number,long1:number,lat2:number,long2:number):string{letlan1:mapCommon.LatLng={latitude:lat1,longitude:long1};letlan2:mapCommon.LatLng={latitude:lat2,longitude:long2};// MapKit 提供的距离计算函数(内部也使用球面距离算法)// 返回值单位:米letdistance:number=map.calculateDistance(lan1,lan2)/1000;// 转换为公里returndistance.toFixed(1);// 保留一位小数}}3.2 map.calculateDistance vs 自实现 Haversine 对比
// 对比两种方法的结果constuserLat=39.9042,userLng=116.4074;// 天安门conststationLat=40.0046,stationLng=116.4823;// 望京// 方案1:map.calculateDistance(项目实际使用)import{map,mapCommon}from'@kit.MapKit';constp1:mapCommon.LatLng={latitude:userLat,longitude:userLng};constp2:mapCommon.LatLng={latitude:stationLat,longitude:stationLng};constsdkDist=map.calculateDistance(p1,p2)/1000;console.log(`SDK 计算:${sdkDist.toFixed(2)}km`);// 方案2:自实现 HaversineconstmyDist=haversineDistance(userLat,userLng,stationLat,stationLng);console.log(`Haversine:${myDist.toFixed(2)}km`);// 两者结果几乎一致(差异 < 0.1%)| 方案 | 依赖 | 精度 | 适用场景 |
|---|---|---|---|
map.calculateDistance | MapKit SDK | 高(考虑地球扁率) | HarmonyOS 项目(推荐) |
| Haversine 自实现 | 无依赖 | 高(±0.5%) | 通用场景、离线计算 |
| 平面勾股定理 | 无 | 极低(误差大) | ❌ 不要用 |
四、距离相关的实用工具函数
4.1 排序、过滤、格式化
interfacePointData{id:string;name:string;latitude:number;longitude:number;distance?:number;// 可选,动态计算}classGeoUtils{// 计算距离(返回km)staticdistance(lat1:number,lng1:number,lat2:number,lng2:number):number{constR=6371;consttoRad=(d:number)=>d*Math.PI/180;constdLat=toRad(lat2-lat1);constdLng=toRad(lng2-lng1);consta=Math.sin(dLat/2)**2+Math.cos(toRad(lat1))*Math.cos(toRad(lat2))*Math.sin(dLng/2)**2;returnR*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));}// 格式化距离显示staticformatDistance(km:number):string{if(km<0.1)return`${(km*1000).toFixed(0)}m`;if(km<1)return`${(km*1000).toFixed(0)}m`;if(km<10)return`${km.toFixed(1)}km`;return`${km.toFixed(0)}km`;}// 给点列表添加距离并排序staticsortByDistance<TextendsPointData>(points:T[],userLat:number,userLng:number):(T&{distance:number;distanceStr:string})[]{returnpoints.map(p=>({...p,distance:GeoUtils.distance(userLat,userLng,p.latitude,p.longitude),distanceStr:GeoUtils.formatDistance(GeoUtils.distance(userLat,userLng,p.latitude,p.longitude))})).sort((a,b)=>a.distance-b.distance);}// 过滤指定范围内的点staticfilterByRadius<TextendsPointData>(points:T[],userLat:number,userLng:number,radiusKm:number):T[]{returnpoints.filter(p=>GeoUtils.distance(userLat,userLng,p.latitude,p.longitude)<=radiusKm);}// 判断两点是否在同一区域(快速判断,用于粗筛)staticisNearby(lat1:number,lng1:number,lat2:number,lng2:number,radiusDeg:number=0.1):boolean{returnMath.abs(lat2-lat1)<=radiusDeg&&Math.abs(lng2-lng1)<=radiusDeg;}}// 使用示例@Entry@Componentstruct GeoUtilsDemoPage{@StateuserLat:number=39.9042;@StateuserLng:number=116.4074;@StatesortedStations:Array<PointData&{distance:number;distanceStr:string}>=[];privaterawStations:PointData[]=[{id:'1',name:'望京石化',latitude:40.0046,longitude:116.4823},{id:'2',name:'朝阳石油',latitude:39.9219,longitude:116.4386},{id:'3',name:'国贸壳牌',latitude:39.9108,longitude:116.4551},{id:'4',name:'通州加油站',latitude:39.8947,longitude:116.6561},// 远处];aboutToAppear():void{// 筛选5km内 + 按距离排序constnearby=GeoUtils.filterByRadius(this.rawStations,this.userLat,this.userLng,5);this.sortedStations=GeoUtils.sortByDistance(nearby,this.userLat,this.userLng);}build(){Column({space:12}){Text(`我的位置:${this.userLat.toFixed(4)},${this.userLng.toFixed(4)}`).fontSize(13).fontColor('#999999')Text('5km内加油站(按距离排序)').fontSize(16).fontWeight(FontWeight.Bold)ForEach(this.sortedStations,(station:PointData&{distanceStr:string})=>{Row({space:12}){Text('⛽').fontSize(20)Text(station.name).fontSize(15).layoutWeight(1)Text(station.distanceStr).fontSize(14).fontColor('#1A6FF5').fontWeight(FontWeight.Bold)}.padding(16).width('100%').backgroundColor('#FFFFFF').borderRadius(12)},(s:PointData)=>s.id)}.padding(20).width('100%').height('100%').backgroundColor('#F5F7FA')}}五、Haversine 精度说明
// Haversine 的精度误差来源:// 1. 地球不是完美球体,而是椭球体(赤道半径 6378km,极地半径 6357km)// 2. Haversine 使用平均半径 6371km,在赤道误差约 0.3%,在极地误差约 0.5%// 3. 对于城市级(<500km)的距离计算,误差可以忽略不计// 高精度需求(如导航)推荐:Vincenty 公式(考虑地球扁率)// 一般应用(如找附近加油站):Haversine 完全够用// 快速估算(不需要精确,只是排序用):functionroughDistance(lat1:number,lng1:number,lat2:number,lng2:number):number{// 在 40°N 左右的中国大多数城市constLAT_TO_KM=111.0;// 纬度1度 ≈ 111kmconstLNG_TO_KM=85.0;// 经度1度 ≈ 85km(40°N 附近)constdLat=(lat2-lat1)*LAT_TO_KM;constdLng=(lng2-lng1)*LNG_TO_KM;returnMath.sqrt(dLat*dLat+dLng*dLng);// 这里的平面计算是局部近似,精度约5%}总结
地理距离计算必须用球面几何,因为地球不是平面。Haversine公式是最广泛使用的球面距离算法:将经纬度差转换为弧度,通过三角函数计算球面角度,再乘以地球半径得到实际距离。本项目使用map.calculateDistance()是最佳选择(MapKit 内置,精度更高);如果需要离线或跨平台计算,Haversine 手写实现也能满足城市级距离计算的精度要求。
