两个位置点相距二十多米,是否足以说明设备发生了移动?只看经纬度差无法回答,样本本身的精度描述也必须进入判断。GeoMarker 的第四篇把连续订阅中的两点拿出来,用固定合成输入比较中心距离与精度半径之和。计算结果可以逐项复算,页面则明确显示它是模型指标,避免将一条简单公式包装成真实路程测量。

误差圆与测量轨道组成的距离质量门封面

图1:误差圆与测量轨道组成的距离质量门封面。

一、先给每个数字标明身份

位置服务的纬度、经度采用其文档规定的坐标体系,精度字段以米表达。应用应保留原始数值供计算,不能把页面已经保留六位小数的字符串再转回数字。显示舍入和算法输入是两条路径:前者追求可读性,后者需要保留数据原貌。上一章回调中的样本可以通过一个小型适配器转换成这里的模型,定位模块不需要知道质量门的实现细节。

本篇使用球面模型,半径常量在代码中固定为六百三十七万一千米;这是示例选择的计算模型,不是宣称地球为精确球体。中心距离用半正矢形式求得,适合示例经纬度角度和米之间的换算。需要测绘级、长距离或椭球精确结果的产品,应另选满足误差预算的算法,本篇不会依据小数位数给出未经验证的精度承诺。

质量门有三项应用规则:经纬度必须有限且在范围内,精度值必须大于零且不超过三十米,样本序号必须递增。三十米是示例业务阈值,不是系统规定的最佳设置;零精度在此被当作不适合当前模型的输入,不能直接解读成完全无误差。序号由应用在接收时递增,只检验传入顺序,不声称它能证明样本的真实新鲜度。

没有采用系统时间字段做速度过滤,是一个刻意收窄的范围。速度门需要明确时间单位、时钟来源、跨重启语义以及时间倒退处理,不能只看到一个时间戳就相减。当前模型只评价一段几何关系,样本是否足够新、两个点间隔多久,仍由接入层根据经过核实的时间契约决定。

精度值也不是适合直接做加权平均的万能权重。若两点都来自相关的系统误差,平均坐标未必按想象中那样变准;把精度较小的一点完全替换另一点,也会改变时间含义。当前模型没有进行平滑或融合,它只展示两个样本在同一个规则下的关系。这样每个输入变化都能追溯到明确输出,而不是藏在没有说明的滤波参数中。

二、把判断写成可复算的纯函数

创建 API 12 Stage 项目 GeoMarker,在 common/SegmentGate.ets 放入以下代码,模板入口继续加载 pages/Index。这一页直接运行合成样本,不调用定位接口,因此不需要声明位置权限;接入第三篇的真实回调时才沿用前台授权。纯函数没有页面、文件或系统服务依赖,可以先验证数学和边界,再观察设备样本。

export interface Sample {
  latitude: number;
  longitude: number;
  accuracy: number;
  sequence: number;
}
export interface SegmentResult {
  valid: boolean;
  distance: number;
  residual: number;
  reason: string;
}
function validSample(s: Sample): boolean {
  return Number.isFinite(s.latitude) && Math.abs(s.latitude) <= 90 &&
    Number.isFinite(s.longitude) && Math.abs(s.longitude) <= 180 &&
    Number.isFinite(s.accuracy) && s.accuracy > 0 && s.accuracy <= 30 &&
    Number.isInteger(s.sequence) && s.sequence > 0;
}
export function evaluateSegment(a: Sample, b: Sample): SegmentResult {
  if (!validSample(a) || !validSample(b) || b.sequence <= a.sequence) {
    return { valid: false, distance: 0, residual: 0, reason: '样本未通过质量门' };
  }
  const rad = Math.PI / 180;
  const latA = a.latitude * rad;
  const latB = b.latitude * rad;
  const dLat = (b.latitude - a.latitude) * rad;
  const dLon = (b.longitude - a.longitude) * rad;
  const term = Math.sin(dLat / 2) ** 2 +
    Math.cos(latA) * Math.cos(latB) * Math.sin(dLon / 2) ** 2;
  const h = Math.min(1, Math.max(0, term));
  const distance = 6371000 * 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
  const residual = Math.max(0, distance - a.accuracy - b.accuracy);
  return { valid: true, distance: distance, residual: residual,
    reason: residual > 0 ? '模型可区分位移' : '误差范围重叠' };
}
export interface PairCase { name: string; a: Sample; b: Sample; }
export const CASES: PairCase[] = [
  { name: '重叠样本', a: { latitude: 0, longitude: 0, accuracy: 8, sequence: 1 },
    b: { latitude: 0, longitude: 0.0001, accuracy: 8, sequence: 2 } },
  { name: '清晰位移', a: { latitude: 0, longitude: 0, accuracy: 4, sequence: 1 },
    b: { latitude: 0, longitude: 0.0002, accuracy: 4, sequence: 2 } },
  { name: '无效输入', a: { latitude: 0, longitude: 0, accuracy: 4, sequence: 1 },
    b: { latitude: 91, longitude: 0.0002, accuracy: 4, sequence: 2 } }
];

计算先检查输入,再转换弧度。纬度差和经度差在取三角函数之前统一乘转换系数,不能把一项当角度、另一项当弧度。中间量限制在零到一,是为了处理浮点舍入在端点附近造成的轻微越界,防止平方根出现非法值;它不意味着允许原本非法的经纬度混入函数。输入质量门和数值稳定性分别解决不同问题。

扣除两端精度半径后的剩余值用于表示“在这个圆模型下还有多少可区分位移”。圆相交时该值归零,圆分离时保留中心间距减半径和的部分。它不是真实位移的统计下界,因为系统精度描述不等于绝对包住真实位置的保证圆;它也不是去噪后的真实路径长度。页面把这两条限制写在指标旁边,而不是隐藏在很远的注释中。

函数没有把拒绝样本返回成一段零米路程。无效结果携带 valid=false,调用者必须先检查有效性,再决定是否展示距离。否则“没有移动”和“无法判断”都会变成零,统计报表就会悄悄把无效采样当成静止证据。相同坐标但合法输入则是有效的零距离,这两种情况可以通过结果类型明确区分。

同样的经度差在不同纬度处对应不同距离,公式中的纬度余弦项因此不可删除。直接使用“每一度多少米”的固定换算,只在受限近似条件下才有意义,不能成为通用经纬度距离函数。为了让最初两组数字易于复算,示例选择零纬度;生产输入仍按完整公式处理,不把这个方便测试的条件写死到算法里。

三、用四组输入建立量级直觉

以下数字均由示例公式计算,属于合成数据,不是某台设备的定位日志。前两组位于数学上方便复算的零纬度附近:经度差万分之一度对应约十一点一二米,翻倍后的经度差对应约二十二点二四米。精度半径从八米减为四米之后,同样的中心距离会产生不同的模型解释。

样例纬度、经度变化两端精度中心距离扣除半径后结果
重叠样本(0,0) → (0,0.0001)8米、8米11.12米0.00米误差范围重叠
清晰位移(0,0) → (0,0.0002)4米、4米22.24米14.24米模型可区分位移
跨日期线(0,179.999) → (0,-179.999)5米、5米222.39米212.39米模型可区分位移
非法纬度(0,0) → (91,0.0002)4米、4米不计算不计算质量门拒绝

第三组专门检验跨日期线。直接用经度差乘一个米数会把接近三百六十度的差误判为绕地球一周;三角函数的周期性质使这组输入对应很短的球面弧段。仍然需要限制经度范围,不能因为某个越界输入恰好算出相似结果就把它当成有效坐标。第四组则在任何三角运算之前被拒绝。

重复序号、倒序序号、非有限坐标和负精度都应该走拒绝分支。完全相同的两个有效坐标应得到中心距离零,残余指标也为零;精度恰好等于三十米可以进入,而略大于阈值不可以。阈值两边的检查比再增加一百组随机正常输入更能说明应用规则是否按预期实现。

白色 DevEco Studio 中的球面距离纯函数与合成样本页面布局示意

图2:白色 DevEco Studio 中的球面距离纯函数与合成样本页面布局示意。

还应核对成对输入属于同一个坐标体系。位置服务返回的原始坐标和某个地图展示坐标即使数值都在合法范围内,也不能因此直接相减;范围检查无法识别这种语义错误。当前示例只接受同一来源约定下的数值,不包含地图坐标转换。若以后接入地图组件,转换后的显示坐标与计算用原始坐标应分开保存和命名。

四、让页面展示计算过程而非包装结论

import { CASES, evaluateSegment, SegmentResult } from '../common/SegmentGate';

@Entry
@Component
struct Index {
  @State selected: number = 1;
  @State result: SegmentResult = evaluateSegment(CASES[1].a, CASES[1].b);
  private select(index: number): void {
    this.selected = index;
    this.result = evaluateSegment(CASES[index].a, CASES[index].b);
  }
  build() {
    Column({ space: 12 }) {
      Text('位移质量门').fontSize(26).fontWeight(FontWeight.Bold).height(38)
      Text('GeoMarker · 合成样本计算').fontSize(13).fontColor('#806B6C').height(22)
      Column({ space: 8 }) {
        Text(CASES[this.selected].name).fontSize(17).fontColor('#6D3639').height(26)
        Text('中心距离').fontSize(14).fontColor('#806B6C').height(24)
        Text(this.result.valid ? `${this.result.distance.toFixed(2)} 米` : '--')
          .fontSize(30).fontColor('#6D3639').height(46)
        Text(`半径合计  ${CASES[this.selected].a.accuracy + CASES[this.selected].b.accuracy} 米`)
          .fontSize(15).fontColor('#806B6C').height(26)
        Text(this.result.valid ? `扣除半径后  ${this.result.residual.toFixed(2)} 米` : '不计算位移')
          .fontSize(18).fontColor('#6D3639').height(32)
      }.padding(20).height(244).width(316).backgroundColor('#FFFFFF').borderRadius(18)
      Row({ space: 8 }) {
        Button('重叠样本').width(98).height(44).fontSize(14).backgroundColor('#A75255')
          .fontColor('#FFFFFF').onClick(() => { this.select(0); })
        Button('清晰位移').width(98).height(44).fontSize(14).backgroundColor('#A75255')
          .fontColor('#FFFFFF').onClick(() => { this.select(1); })
        Button('无效输入').width(98).height(44).fontSize(14).backgroundColor('#A75255')
          .fontColor('#FFFFFF').onClick(() => { this.select(2); })
      }
      Text(this.result.reason).fontSize(15).fontColor('#A75255').height(44)
      Text('模型指标,不代表真实路程下界').fontSize(13).fontColor('#806B6C').height(24)
    }.padding({ top: 32 }).width('100%').height('100%').backgroundColor('#F8EFF0')
  }
}

页面默认展示“清晰位移”合成样本,中心距离二十二点二四米、半径和八米、剩余十四点二四米。点击另两个按钮可以到达重叠和非法输入状态,所有数字都来自同一个函数。界面没有地图背景、行走轨迹或定位成功图标,避免读者将固定输入误认为系统刚刚返回的真实位置;合成样本的身份就在副标题中。

两组误差圆、半径和及残余位移公式的几何机制示意

图3:两组误差圆、半径和及残余位移公式的几何机制示意。

图中圆圈是解释模型的辅助图形,不承诺特定概率覆盖率。两圆相交并不能证明设备没有移动,只能说明这套规则无法把位移从当前精度描述中分离出来;两圆分开也不能证明实际走过的路径等于中心连线。算法输出越简单,文案越需要精准,尤其不能把“可区分”改成“已确认行走”。

如果将模型接入连续采样,不要直接把每段残余值相加命名为总路程。缓慢移动时,相邻点的精度圆可能一直重叠,逐段扣除会造成明显低估;频繁更新锚点也可能让真实位移长期被吞掉。要累计路程,需要另外设计锚点、停留识别、时间约束和误差模型,并用有参考轨迹的数据验证,本篇不把这些工作隐含在一条公式里。

对数值端点的检查可以单独记录为算法验证:两个相同点的中间量接近零,对跖点接近一,输出都应为有限数字。夹紧处理只修正浮点误差范围内的越界,不应被理解成“所有错误输入都能自动修好”。因此质量门必须在前,数学运算在后,显示舍入最后发生,三个步骤的顺序不能互换。

五、接回实时样本时保持边界

在第三篇回调中增加递增序号,把经纬度、精度和序号复制成 Sample,再将两份样本送入函数即可。复制小对象让纯函数不依赖系统对象的其他字段,也避免把来源、时间和地址解析不加区分地塞进同一个算法。真实接入应明确何时重置上一点:开始新轮次、权限变化或长时间中断以后,不能自动连接前后两段。

无效点应如何影响锚点,也是需要显式决定的策略。简单跳过一个低质量点再连接很久之后的下一个好点,可能跨过没有观测的弯路;因此“拒绝输入”和“维持连续性”不能由同一条真假判断代替。当前页面只评价选定的一对样本,不保存轨迹,它将这个产品决策留在调用方,而不是给出一个含糊的自动累计器。

如果将来统计拒绝比例,分母也要定义清楚:是收到的位置数、尝试比较的路段数,还是最终展示的结果数。当前函数一次处理两个点,重复使用同一个锚点会让这些数量彼此不同。把失败原因保留下来可以区分范围非法、精度超限和顺序错误;示例用一个统一拒绝文案保持页面简单,扩展统计时再细分结果枚举会更合适。

性能方面,计算只涉及常量数量的数学运算和小对象,不需要为了两点比较引入线程池。真正容易扩大的成本往往来自下游绘图、存储或地址请求,应在接入以后依据实际测量再做优化。本篇公式和固定样例可在普通脚本中复算,但 HarmonyOS 页面和定位接入仍需在目标 SDK 与设备验证;算法输出通过不等于定位服务已经通过设备测试。

参考资料

Logo

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

更多推荐