前言

这篇文章主要是为了解决社区里的一个具体问题:用户在地图上长按拿到经纬度后,调用 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 逆地理编码 APIWGS84GPS 原始坐标,国际通用

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,先别急着怀疑接口坏了,看看经纬度是不是拿错了坐标系。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐