前言
这篇文章主要是为了解决社区里的一个具体问题:用户在地图上长按拿到经纬度后,调用getAddressesFromLocation做逆地理编码,结果data一直是空数组,err还是undefined。问题地址在这里:https://developer.huawei.com/consumer/cn/forum/topic/0208222825485900638?fid=0109140870620153026。
这种问题最烦的地方在于:它不报错。回调正常进,代码逻辑看着也没毛病,但就是查不到省市区。
我排查下来,核心原因不是权限,也不是getAddressesFromLocation不能用,而是坐标系没对上。Map Kit 在中国大陆场景下拿到的通常是 GCJ02 坐标,而geoLocationManager.getAddressesFromLocation需要的是 WGS84。两套坐标差个几百米,服务端自然可能查不到地址。
先说结论
如果你也遇到getAddressesFromLocation返回空数组,可以按这个顺序查:
- 确认位置权限已经声明并动态申请
- 确认
geoLocationManager.isGeocoderAvailable()返回true - 确认设备联网、位置开关打开,尽量用真机测试
- 如果坐标来自 Map Kit,先把 GCJ02 转成 WGS84,再传给
getAddressesFromLocation - 国内展示建议加上
locale: 'zh'
这几个点里,最容易被忽略的就是坐标系。
效果预览
注意的是我写的案例中 用的是mock 数据,你可以换成你自己的真实数据跑一下,完整代码在文末,同时我也添加了相关函数的封装
两套坐标系,各管各的
HarmonyOS 上涉及坐标的 SDK 有两套体系:
| 来源 | 坐标系 | 说明 |
|---|---|---|
| Map Kit 回调(地图点击、标注) | GCJ02 | 国测局加密坐标,中国大陆强制使用 |
geoLocationManager逆地理编码 API | WGS84 | GPS 原始坐标,国际通用 |
GCJ02 是在 WGS84 基础上做了非线性加密偏移,平均偏移 300-500 米。你拿 GCJ02 的坐标去调只认 WGS84 的接口,等于拿着北京的地址去上海的地图上找,找不到才是正常的。
这就是为什么回调进了、没报错、但data是空数组——请求本身合法,只是位置偏了,服务端查无结果。
别一上来就怀疑权限
很多人看到定位相关接口没结果,第一反应就是权限没给。这个方向没错,但在这个问题里,权限不是唯一变量。
逆地理编码正常工作前,至少要先过三关:权限声明、运行时授权、服务可用性检查。
module.json5里声明权限:
{"module":{"requestPermissions":[{"name":"ohos.permission.APPROXIMATELY_LOCATION","reason":"$string:reason_for_location","usedScene":{"abilities":["EntryAbility"],"when":"inuse"}},{"name":"ohos.permission.LOCATION","reason":"$string:reason_for_location","usedScene":{"abilities":["EntryAbility"],"when":"inuse"}}]}}页面里动态申请:
import{abilityAccessCtrl,common,Permissions}from'@kit.AbilityKit';privateasyncrequestPermission():Promise<void>{letperms:Array<Permissions>=['ohos.permission.APPROXIMATELY_LOCATION','ohos.permission.LOCATION'];letcontext=getContext(this)ascommon.UIAbilityContext;letatManager=abilityAccessCtrl.createAtManager();awaitatManager.requestPermissionsFromUser(context,perms);}调逆地理编码前,先检查服务是否可用:
if(!geoLocationManager.isGeocoderAvailable()){// 服务不可用,可能是没网络、模拟器不支持、位置开关没开return;}isGeocoderAvailable()返回false的话,后面调了也是白调。模拟器上这个值大概率是false,逆地理编码需要联网访问后端服务,真机 + 网络 + 位置开关打开才行。
如果这些都没问题,但data还是空数组,就该把注意力放到坐标本身了。尤其是坐标来自地图点击、长按、Marker 回调这类 Map Kit 场景时,坐标系问题非常常见。
核心:GCJ02 转 WGS84
这是解决问题的关键。地图回调给的 GCJ02 坐标,必须先转成 WGS84,再传给getAddressesFromLocation。
这里有个判断思路:如果接口能正常返回 Promise、没有抛BusinessError,但结果数组为空,说明请求大概率发出去了,只是服务端按你给的经纬度没有查到合适地址。这个时候继续改权限意义不大,应该先校准坐标。
转换算法:
functiongcj02ToWgs84(lat:number,lon:number):number[]{leta=6378245.0;letee=0.00669342162296594323;// 海外坐标不需要转换if(lon<72.004||lon>137.8347||lat<0.8293||lat>55.8271){return[lat,lon];}letdLat=transformLat(lon-105.0,lat-35.0);letdLon=transformLon(lon-105.0,lat-35.0);letradLat=lat/180.0*Math.PI;letmagic=Math.sin(radLat);magic=1-ee*magic*magic;letsqrtMagic=Math.sqrt(magic);dLat=(dLat*180.0)/((a*(1-ee))/(magic*sqrtMagic)*Math.PI);dLon=(dLon*180.0)/(a/sqrtMagic*Math.cos(radLat)*Math.PI);// 用原坐标减去偏移量,得到 WGS84return[lat-dLat,lon-dLon];}functiontransformLat(x:number,y:number):number{letret=-100.0+2.0*x+3.0*y+0.2*y*y+0.1*x*y+0.2*Math.sqrt(Math.abs(x));ret+=(20.0*Math.sin(6.0*x*Math.PI)+20.0*Math.sin(2.0*x*Math.PI))*2.0/3.0;ret+=(20.0*Math.sin(y*Math.PI)+40.0*Math.sin(y/3.0*Math.PI))*2.0/3.0;ret+=(160.0*Math.sin(y/12.0*Math.PI)+320*Math.sin(y*Math.PI/30.0))*2.0/3.0;returnret;}functiontransformLon(x:number,y:number):number{letret=300.0+x+2.0*y+0.1*x*x+0.1*x*y+0.1*Math.sqrt(Math.abs(x));ret+=(20.0*Math.sin(6.0*x*Math.PI)+20.0*Math.sin(2.0*x*Math.PI))*2.0/3.0;ret+=(20.0*Math.sin(x*Math.PI)+40.0*Math.sin(x/3.0*Math.PI))*2.0/3.0;ret+=(150.0*Math.sin(x/12.0*Math.PI)+300.0*Math.sin(x/30.0*Math.PI))*2.0/3.0;returnret;}这段算法做了两件事:先算出 GCJ02 相对 WGS84 的偏移量dLat和dLon,然后用原坐标减去偏移量,还原出 WGS84 坐标。
transformLat和transformLon是偏移量计算的核心,用一系列三角函数模拟国测局的非线性加密。海外坐标直接跳过转换,因为 GCJ02 加密只在中国大陆生效。
逆地理编码调用
坐标转换完,调接口就简单了:
privatereverseGeocode(lat:number,lon:number):void{// 先转 WGS84letwgs84:number[]=gcj02ToWgs84(lat,lon);letrequest:geoLocationManager.ReverseGeoCodeRequest={latitude:wgs84[0],longitude:wgs84[1],maxItems:1,locale:'zh'// 关键:指定中文返回};geoLocationManager.getAddressesFromLocation(request).then((data:Array<geoLocationManager.GeoAddress>)=>{if(data.length===0){// 还是空?检查网络和位置开关return;}letaddr:geoLocationManager.GeoAddress=data[0];letprovince:string=addr.administrativeArea??'';letcity:string=addr.locality??'';letdistrict:string=addr.subLocality??'';this.bubbleText=`${province}${city}${district}`;}).catch((error:BusinessError)=>{// 处理错误});}有两个参数容易漏:
locale: 'zh'— 不设的话,某些设备默认语言可能不是中文,返回的地址描述可能是英文或者直接查不到。做国内应用,这个值建议写死。maxItems— 控制返回条数,气泡展示只需要 1 条就够了。
GeoAddress里省市区对应的字段是administrativeArea(省)、locality(市)、subLocality(区)。三个拼起来就是气泡里要显示的文字。
Mock 数据模拟地图长按
实际场景里经纬度来自地图长按回调,这里用 mock 数据模拟,方便在没有地图的环境里验证:
interfaceMockLocation{label:string;lat:number;lon:number;}constMOCK_LOCATIONS:MockLocation[]=[{label:'北京天安门',lat:39.9098,lon:116.4044},{label:'上海东方明珠',lat:31.2413,lon:121.5064},{label:'广州塔',lat:23.1080,lon:113.3311},{label:'成都天府广场',lat:30.6598,lon:104.0633},{label:'杭州西湖',lat:30.2420,lon:120.1485},];这些坐标都是 GCJ02 的(从地图 SDK 拿到的就是 GCJ02),点一下就触发逆地理编码。
完整源码
import { geoLocationManager } from '@kit.LocationKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit'; function gcj02ToWgs84(lat: number, lon: number): number[] { let a = 6378245.0; let ee = 0.00669342162296594323; if (lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271) { return [lat, lon]; } let dLat = transformLat(lon - 105.0, lat - 35.0); let dLon = transformLon(lon - 105.0, lat - 35.0); let radLat = lat / 180.0 * Math.PI; let magic = Math.sin(radLat); magic = 1 - ee * magic * magic; let sqrtMagic = Math.sqrt(magic); dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * Math.PI); dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * Math.PI); return [lat - dLat, lon - dLon]; } function transformLat(x: number, y: number): number { let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)); ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin(y / 3.0 * Math.PI)) * 2.0 / 3.0; ret += (160.0 * Math.sin(y / 12.0 * Math.PI) + 320 * Math.sin(y * Math.PI / 30.0)) * 2.0 / 3.0; return ret; } function transformLon(x: number, y: number): number { let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)); ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin(x / 3.0 * Math.PI)) * 2.0 / 3.0; ret += (150.0 * Math.sin(x / 12.0 * Math.PI) + 300.0 * Math.sin(x / 30.0 * Math.PI)) * 2.0 / 3.0; return ret; } interface MockLocation { label: string; lat: number; lon: number; } const MOCK_LOCATIONS: MockLocation[] = [ { label: '北京天安门', lat: 39.9098, lon: 116.4044 }, { label: '上海东方明珠', lat: 31.2413, lon: 121.5064 }, { label: '广州塔', lat: 23.1080, lon: 113.3311 }, { label: '成都天府广场', lat: 30.6598, lon: 104.0633 }, { label: '杭州西湖', lat: 30.2420, lon: 120.1485 }, ]; @Entry @Component struct Qa10 { @State bubbleText: string = ''; @State selectedIndex: number = 0; private async requestPermission(): Promise<void> { let perms: Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION', 'ohos.permission.LOCATION']; try { let context = getContext(this) as common.UIAbilityContext; let atManager = abilityAccessCtrl.createAtManager(); await atManager.requestPermissionsFromUser(context, perms); } catch (_e) { } } private reverseGeocode(lat: number, lon: number): void { this.bubbleText = '查询中...'; if (!geoLocationManager.isGeocoderAvailable()) { this.bubbleText = '服务不可用'; return; } let wgs84: number[] = gcj02ToWgs84(lat, lon); let request: geoLocationManager.ReverseGeoCodeRequest = { latitude: wgs84[0], longitude: wgs84[1], maxItems: 1, locale: 'zh' }; try { geoLocationManager.getAddressesFromLocation(request).then((data: Array<geoLocationManager.GeoAddress>) => { if (data.length === 0) { this.bubbleText = '未查到地址'; return; } let addr: geoLocationManager.GeoAddress = data[0]; let province: string = addr.administrativeArea ?? ''; let city: string = addr.locality ?? ''; let district: string = addr.subLocality ?? ''; this.bubbleText = `${province} ${city} ${district}`; }).catch((error: BusinessError) => { this.bubbleText = `错误: ${error.code}`; }); } catch (err) { let e: BusinessError = err as BusinessError; this.bubbleText = `异常: ${e.code}`; } } build() { Column() { Column() { ForEach(MOCK_LOCATIONS, (loc: MockLocation, index: number) => { Row() { Text(loc.label) .fontSize(14) .fontColor('#CCCCCC') .layoutWeight(1) Text(`${loc.lat}, ${loc.lon}`) .fontSize(11) .fontColor('#888888') } .width('100%') .padding(12) .backgroundColor(this.selectedIndex === index ? '#2A2A4A' : '#1A1A2E') .borderRadius(8) .margin({ bottom: 6 }) .onClick(() => { this.selectedIndex = index; this.reverseGeocode(loc.lat, loc.lon); }) }, (loc: MockLocation, index: number) => `${index}`) } .width('100%') .margin({ bottom: 20 }) if (this.bubbleText !== '') { Column() { Text(this.bubbleText) .fontSize(16) .fontColor('#FFFFFF') .textAlign(TextAlign.Center) } .padding({ left: 20, right: 20, top: 10, bottom: 10 }) .backgroundColor('#4A90D9') .borderRadius(20) .margin({ bottom: 12 }) } Text('点击上方位置模拟地图长按选点,GCJ02坐标自动转WGS84后逆地理编码') .fontSize(11) .fontColor('#666666') } .width('100%') .height('100%') .padding(16) .backgroundColor('#0D0D1A') .onAppear(() => { this.requestPermission(); }) } }写在最后
getAddressesFromLocation返回空数组但不报错,最容易把人带偏。因为它不像权限异常那样直接抛错,也不像服务不可用那样有明显信号,它只是安静地给你一个空结果。
这次社区问题的关键就在这里:坐标来自 Map Kit,逆地理编码走的是geoLocationManager,两边默认使用的坐标系不一样。Map Kit 给的是 GCJ02,getAddressesFromLocation要的是 WGS84,中间差了几百米,地址自然查不到。
我建议按这个顺序处理:先检查isGeocoderAvailable(),再确认权限和真机网络环境,最后重点看坐标系。如果坐标来自 Map Kit,就先转 WGS84;如果你直接用 Map Kit 自带的site.reverseGeocode,它内部已经处理了坐标系转换,就不用再手动转一次。
这个坑不难,但很隐蔽。下次看到data是空数组、err是undefined,先别急着怀疑接口坏了,看看经纬度是不是拿错了坐标系。