把 GPS 轨迹变成探索面积:25m 网格 + 行政区匹配的 iOS 实践

3 阅读5分钟

一个让我纠结很久的问题

跑步 App 告诉我今天跑了 5 公里,运动手表告诉我消耗了 300 大卡。但我更想知道的是:我在这座城市住了三年,到底走过了百分之几的地方?

轨迹线能告诉你去过哪条路,但它没法回答"覆盖了多少"这个问题。我想要一个面积数字。

所以我做了一个叫「雁过留痕」的 iOS App,核心就干一件事:把你走过的 GPS 轨迹转换成探索面积(km²),告诉你这片土地你到底踩过多少。

怎么把一条线变成一个面

GPS 给的是一串经纬度坐标点,连起来是一条线。但我需要面积。

方案是把地图切成 25m × 25m 的网格。GPS 点落入哪个格子,那个格子就被点亮。探索面积 = 点亮的格子总数 × 单格面积。

25m 大概是一个篮球场的长边。这个精度是试出来的——50m 太粗,城市里走完一整条街可能只点亮两三个格子,体验很糊;10m 太细,GPS 在高楼之间的漂移会导致你坐在办公室面积都在涨,数据失去意义。25m 刚好,步行能明显看到新区域被点亮,静止时又不会乱飘。

网格索引的实现就是经纬度到整数坐标的映射:

// 将经纬度映射到 25m 网格索引
let metersPerDegLat = 111_320.0
let metersPerDegLon = 111_320.0 * cos(latitude * .pi / 180)
let gridX = Int(floor(longitude * metersPerDegLon / 25.0))
let gridY = Int(floor(latitude * metersPerDegLat / 25.0))

每个 (gridX, gridY) 对就是格子的唯一标识,扔进 Set 去重,最后 count * 0.000625(25×25=625m²)得到探索面积。简单粗暴,但好用。

行政区匹配:比想象中麻烦得多

"你解锁了哪些省份"——听起来就是判断点在哪个多边形里,对吧?实际做起来有三个坑。

坐标系偏移。 中国地图用 GCJ-02(火星坐标),CoreLocation 返回的是 WGS-84,两者差几十到几百米。沿海城市和省界附近这个偏移很致命,深圳的点可能被判到香港去。App 里内置了坐标转换,确保和行政区边界数据对齐。

边界数据体积。 中国省市两级行政区的 GeoJSON,精度稍高就几十 MB。我做了一轮简化,省级保留足够辨识度,市级适当降低,区县级直接放弃了(数据量扛不住)。最终压到大概 3MB。

判定性能。 每来一个 GPS 点就遍历所有省份做 ray-casting,太慢。我的做法是先用 bounding box 粗筛掉明显不相关的省份,再对候选区域精确判定。加上缓存最近命中的省份——下一个点大概率还在同一个省——命中缓存直接跳过。优化后后台运行基本感受不到开销。

成就系统怎么定义

面积算出来之后得让它有意义。我做了一套徽章系统,分了几条线:exploration(探索类)、consistency(坚持类)、china(行政区类)、world(全球类)。

BadgeMetrics 是成就判定的核心输入,把用户的统计数据打包在一起:

struct BadgeMetrics {
    let totalDistanceKilometers: Double
    let recordedDays: Int
    let currentStreakDays: Int
    let chinaProvinceCount: Int
    let chinaAreaKm2: Double
    let cityUnlockCount: Int
    let globalAreaKm2: Double
    // ... 十几个字段
}

每个徽章拿 metrics 里的字段做判定,比如「解锁 5 个省份」就是 chinaProvinceCount >= 5。同一维度还有多级递进,探索面积从 1km² 到 10km² 到 100km²。

定阈值花了很多时间。我自己用了两个月,记录日常通勤和周末出行的数据,才摸清普通用户的面积增长曲线。太容易没意思,太难劝退人。

轨迹分段也有讲究。两个 GPS 点间隔超过 10 分钟就切成两段,单段少于 8 个点直接过滤——8 个点、每个间隔约 15 米,意味着至少走了 100 多米。低于这个的多半是手机放桌上 GPS 飘出来的垃圾数据。

后台定位的电量取舍

做持续定位的 App,电量永远绕不开。

CLLocationManager 的 allowsBackgroundLocationUpdates 开了之后耗电很明显。我试了几种策略:

  • 高精度持续定位:轨迹漂亮,但一天吃 15%-20% 电量,不可接受。
  • significantLocationChange:省电,但城市里走几条街可能只给一两个点,面积算不出来。
  • 折中:desiredAccuracy = kCLLocationAccuracyNearestTenMeters + distanceFilter = 15。静止时不上报,走动时 15 米一个点,25m 网格够用。

我在 iPhone 15、iOS 17 上测的数据:日常通勤后台挂着大概 8-10 小时,耗电 5%-8%(蓝牙关闭、Wi-Fi 开启)。能接受,但也不算少。

回头看的一些想法

App Store 评分目前 5 分,评论数很少,下载量也很低。

说实话,我花了三周反复调网格精度,花了两周处理行政区边界数据,但花在想分发渠道上的时间基本为零。现在回头看,这个比例完全反了。

有几个决策我觉得做对了:25m 网格在 citywalk 场景下体验确实不错;徽章分多条线,出差多的人省份解锁快,跑步党连续打卡数高,不同用户能找到不同的成就感。

有一个地方做得不够好:面积数字缺乏参照。用户看到 2.3 km²,不知道算多还是少。应该换算成"xx 个足球场"之类的,或者给个城市排名。但排名涉及用户数据比较,隐私和服务端成本都是问题,暂时搁置了。

想讨论一个具体问题

行政区边界匹配我现在是 bounding box 粗筛 + ray-casting 精判,能用但总觉得不够好。有没有人试过用 S2 Geometry 或者 Uber 的 H3 来做空间索引?理论上六边形网格做区域归属判定应该更高效,但我没在 iOS 端实际跑过,不确定引入这些库的包体积和性能表现怎么样。如果有踩过坑的,评论区聊聊。