HarmonyOS7 逆地理编码返回空数组?先别查权限,可能是坐标系没对上
前言
这篇文章主要是为了解决社区里的一个具体问题:用户在地图上长按拿到经纬度后,调用 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';
private async requestPermission(): Promise<void> {
let perms: Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION', 'ohos.permission.LOCATION'];
let context = getContext(this) as common.UIAbilityContext;
let atManager = abilityAccessCtrl.createAtManager();
await atManager.requestPermissionsFromUser(context, perms);
}
调逆地理编码前,先检查服务是否可用:
if (!geoLocationManager.isGeocoderAvailable()) {
// 服务不可用,可能是没网络、模拟器不支持、位置开关没开
return;
}
isGeocoderAvailable() 返回 false 的话,后面调了也是白调。模拟器上这个值大概率是 false,逆地理编码需要联网访问后端服务,真机 + 网络 + 位置开关打开才行。
如果这些都没问题,但 data 还是空数组,就该把注意力放到坐标本身了。尤其是坐标来自地图点击、长按、Marker 回调这类 Map Kit 场景时,坐标系问题非常常见。
核心:GCJ02 转 WGS84
这是解决问题的关键。地图回调给的 GCJ02 坐标,必须先转成 WGS84,再传给 getAddressesFromLocation。
这里有个判断思路:如果接口能正常返回 Promise、没有抛 BusinessError,但结果数组为空,说明请求大概率发出去了,只是服务端按你给的经纬度没有查到合适地址。这个时候继续改权限意义不大,应该先校准坐标。
转换算法:
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);
// 用原坐标减去偏移量,得到 WGS84
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;
}
这段算法做了两件事:先算出 GCJ02 相对 WGS84 的偏移量 dLat 和 dLon,然后用原坐标减去偏移量,还原出 WGS84 坐标。
transformLat 和 transformLon 是偏移量计算的核心,用一系列三角函数模拟国测局的非线性加密。海外坐标直接跳过转换,因为 GCJ02 加密只在中国大陆生效。
逆地理编码调用
坐标转换完,调接口就简单了:
private reverseGeocode(lat: number, lon: number): void {
// 先转 WGS84
let wgs84: number[] = gcj02ToWgs84(lat, lon);
let request: 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;
}
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) => {
// 处理错误
});
}
有两个参数容易漏:
locale: 'zh'— 不设的话,某些设备默认语言可能不是中文,返回的地址描述可能是英文或者直接查不到。做国内应用,这个值建议写死。maxItems— 控制返回条数,气泡展示只需要 1 条就够了。
GeoAddress 里省市区对应的字段是 administrativeArea(省)、locality(市)、subLocality(区)。三个拼起来就是气泡里要显示的文字。
Mock 数据模拟地图长按
实际场景里经纬度来自地图长按回调,这里用 mock 数据模拟,方便在没有地图的环境里验证:
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 },
];
这些坐标都是 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,先别急着怀疑接口坏了,看看经纬度是不是拿错了坐标系。
更多推荐


所有评论(0)