HarmonyOS开发:位置订阅与轨迹追踪

举报
Jack20 发表于 2026/06/21 16:17:53 2026/06/21
【摘要】 HarmonyOS开发:位置订阅与轨迹追踪核心要点:深入掌握HarmonyOS位置订阅机制与轨迹追踪技术,实现高精度运动轨迹记录、轨迹数据压缩、轨迹可视化展示,构建生产级运动健康与物流追踪应用。项目说明核心模块@ohos.geoLocationManager关键技术持续定位订阅、Douglas-Peucker压缩、轨迹可视化 一、背景与动机 1.1 轨迹追踪的应用价值轨迹追踪是位置服务中最...

HarmonyOS开发:位置订阅与轨迹追踪

核心要点:深入掌握HarmonyOS位置订阅机制与轨迹追踪技术,实现高精度运动轨迹记录、轨迹数据压缩、轨迹可视化展示,构建生产级运动健康与物流追踪应用。

项目 说明
核心模块 @ohos.geoLocationManager
关键技术 持续定位订阅、Douglas-Peucker压缩、轨迹可视化

一、背景与动机

1.1 轨迹追踪的应用价值

轨迹追踪是位置服务中最具商业价值的能力之一。从运动健康的跑步轨迹、骑行路线,到物流配送的实时追踪、网约车的行程分享,再到共享出行的电子围栏计费,轨迹追踪技术支撑着千亿级市场规模的核心业务。

应用场景 轨迹需求 精度要求 更新频率
跑步/骑行 运动轨迹、距离、配速 5~10m 1~3秒
步行导航 导航路线、偏航检测 3~5m 1秒
物流追踪 配送路径、ETA 10~50m 5~30秒
网约车 行程分享、计费 5~10m 2~5秒
儿童守护 位置追踪、安全告警 10~30m 5~10秒

1.2 轨迹追踪的技术挑战

flowchart TB
    A[轨迹追踪技术挑战] --> B[数据采集]
    A --> C[数据处理]
    A --> D[数据存储]
    A --> E[数据展示]
    
    B --> B1[GPS精度波动]
    B --> B2[室内外切换]
    B --> B3[功耗与精度平衡]
    
    C --> C1[轨迹漂移修正]
    C --> C2[数据压缩降噪]
    C --> C3[速度/距离计算]
    
    D --> D1[大量轨迹点存储]
    D --> D2[离线缓存同步]
    D --> D3[隐私数据保护]
    
    E --> E1[地图渲染性能]
    E --> E2[实时轨迹更新]
    E --> E3[轨迹回放动画]
    
    classDef rootStyle fill:#6C5CE7,stroke:#5B4CDB,color:#FFF,font-weight:bold
    classDef branchStyle fill:#E17055,stroke:#D63031,color:#FFF,font-weight:bold
    classDef challengeStyle fill:#0984E3,stroke:#0770C2,color:#FFF,font-weight:bold
    
    class A rootStyle
    class B,C,D,E branchStyle
    class B1,B2,B3,C1,C2,C3,D1,D2,D3,E1,E2,E3 challengeStyle

1.3 轨迹追踪系统架构

一个完整的轨迹追踪系统包含以下层次:

层级 组件 职责
采集层 位置订阅引擎 持续获取GPS位置
处理层 轨迹处理引擎 漂移修正、压缩降噪、指标计算
存储层 轨迹存储引擎 本地缓存、云端同步
展示层 轨迹渲染引擎 地图绘制、动画回放

二、核心原理

2.1 位置订阅机制

HarmonyOS提供两种位置订阅方式:

1. 主动订阅(on(‘locationChange’))

// 基于时间和距离间隔的持续定位
const request: geoLocationManager.ContinuousLocationRequest = {
  priority: geoLocationManager.LocationRequestPriority.ACCURACY_PRIORITY,
  scenario: geoLocationManager.LocationRequestScenario.NAVIGATION,
  timeInterval: 2,      // 每2秒更新
  distanceInterval: 5,   // 每移动5米更新
  maxAccuracy: 50        // 最大可接受精度50米
};

const callbackId = geoLocationManager.on('locationChange', request, (location) => {
  // 处理位置更新
});

2. 被动订阅(on(‘locationEnabledChange’))

// 监听定位服务状态变化
geoLocationManager.on('locationEnabledChange', (state: boolean) => {
  if (!state) {
    // 定位服务被关闭,暂停轨迹追踪
    pauseTracking();
  }
});

2.2 轨迹漂移修正

GPS轨迹漂移是最常见的数据质量问题,主要表现为:

漂移类型 特征 原因
静态漂移 静止时轨迹点散布 GPS精度波动
跳跃漂移 突然跳到远处再回来 多径效应、信号遮挡
偏移漂移 轨迹整体偏向一侧 卫星几何分布不佳

卡尔曼滤波修正

卡尔曼滤波是最常用的轨迹平滑算法,通过预测-更新两步迭代实现最优估计:

预测步骤:
  x̂ₖ|ₖ₋₁ = F · x̂ₖ₋₁|ₖ₋₁          // 状态预测
  P|ₖ₋₁ = F · Pₖ₋₁|ₖ₋₁ · F+ Q  // 协方差预测

更新步骤:
  K= P|ₖ₋₁ · H· (H · P|ₖ₋₁ · H+ R)⁻¹  // 卡尔曼增益
  x̂ₖ|= x̂ₖ|ₖ₋₁ + K· (zₖ - H · x̂ₖ|ₖ₋₁)      // 状态更新
  P|= (I - Kₖ · H) · P|ₖ₋₁                    // 协方差更新

其中:
- x̂:状态估计(位置+速度)
- P:估计协方差
- F:状态转移矩阵
- H:观测矩阵
- Q:过程噪声协方差
- R:观测噪声协方差
- K:卡尔曼增益
- z:观测值(GPS读数)

2.3 Douglas-Peucker轨迹压缩

长时间追踪会产生大量轨迹点,需要进行压缩存储。Douglas-Peucker算法是最经典的线简化算法:

flowchart TB
    A[原始轨迹点序列] --> B[连接首尾两点]
    B --> C[计算所有中间点到连线的距离]
    C --> D{最大距离 > 阈值?}
    D -->|| E[保留首尾两点<br/>删除中间点]
    D -->|| F[在最大距离点处分割]
    F --> G[递归处理左半段]
    F --> H[递归处理右半段]
    G --> I[合并结果]
    H --> I
    
    classDef inputStyle fill:#6C5CE7,stroke:#5B4CDB,color:#FFF,font-weight:bold
    classDef processStyle fill:#0984E3,stroke:#0770C2,color:#FFF,font-weight:bold
    classDef decisionStyle fill:#FDCB6E,stroke:#F0B429,color:#333,font-weight:bold
    classDef resultStyle fill:#00B894,stroke:#1ABC9C,color:#FFF,font-weight:bold
    
    class A inputStyle
    class B,C processStyle
    class D decisionStyle
    class E,F,G,H resultStyle
    class I resultStyle

压缩效果

压缩阈值 压缩比 轨迹保真度 适用场景
1m 30~50% 运动轨迹
5m 60~80% 导航路线
10m 80~95% 物流追踪
50m 95~99% 极低 粗略路线

三、代码实战

3.1 轨迹追踪引擎

// TrackTrackingEngine.ets
// 功能:轨迹追踪核心引擎,支持位置订阅、漂移修正、指标计算

import { geoLocationManager } from '@kit.LocationKit';
import { BusinessError } from '@kit.BasicServicesKit';

const TAG = '[TrackTrackingEngine]';

// 轨迹点
export interface TrackPoint {
  latitude: number;     // 纬度
  longitude: number;    // 经度
  altitude: number;     // 海拔
  accuracy: number;     // 精度
  speed: number;        // 速度(m/s)
  bearing: number;      // 方向角
  timestamp: number;    // 时间戳
  distance: number;     // 累计距离(m)
  duration: number;     // 累计时长(ms)
}

// 追踪统计
export interface TrackStatistics {
  totalDistance: number;   // 总距离(m)
  totalDuration: number;   // 总时长(ms)
  avgSpeed: number;        // 平均速度(m/s)
  maxSpeed: number;        // 最大速度(m/s)
  minAltitude: number;     // 最低海拔
  maxAltitude: number;     // 最高海拔
  elevationGain: number;   // 累计爬升
  elevationLoss: number;   // 累计下降
  pointCount: number;      // 轨迹点数
}

// 追踪状态
export enum TrackingState {
  IDLE = 'idle',
  TRACKING = 'tracking',
  PAUSED = 'paused'
}

// 追踪回调
export interface TrackingCallback {
  onPointAdded(point: TrackPoint): void;
  onStatisticsUpdated(stats: TrackStatistics): void;
  onStateChanged(state: TrackingState): void;
  onError(error: BusinessError): void;
}

export class TrackTrackingEngine {
  // 追踪状态
  private state: TrackingState = TrackingState.IDLE;
  private callbackId: number = -1;
  private callback: TrackingCallback | null = null;

  // 轨迹数据
  private trackPoints: TrackPoint[] = [];
  private startTime: number = 0;
  private pauseTime: number = 0;
  private totalPauseDuration: number = 0;

  // 卡尔曼滤波参数
  private kfLat: number = 0;
  private kfLng: number = 0;
  private kfPLat: number = 1;
  private kfPLng: number = 1;
  private kfInitialized: boolean = false;

  // 漂移过滤参数
  private readonly MAX_SPEED = 50;        // 最大合理速度 m/s(180km/h)
  private readonly MIN_ACCURACY = 100;    // 最小可接受精度
  private readonly DRIFT_THRESHOLD = 0.0005; // 漂移阈值(约50米)

  /**
   * 设置回调
   */
  setCallback(callback: TrackingCallback): void {
    this.callback = callback;
  }

  /**
   * 开始追踪
   */
  startTracking(): boolean {
    if (this.state === TrackingState.TRACKING) {
      console.warn(TAG, '已在追踪中');
      return false;
    }

    try {
      const request: geoLocationManager.ContinuousLocationRequest = {
        priority: geoLocationManager.LocationRequestPriority.ACCURACY_PRIORITY,
        scenario: geoLocationManager.LocationRequestScenario.NAVIGATION,
        timeInterval: 1,
        distanceInterval: 0,
        maxAccuracy: this.MIN_ACCURACY
      };

      this.callbackId = geoLocationManager.on('locationChange', request,
        (location: geoLocationManager.Location) => {
          this.handleLocationUpdate(location);
        }
      );

      this.state = TrackingState.TRACKING;
      this.startTime = Date.now();
      this.totalPauseDuration = 0;
      this.kfInitialized = false;
      this.trackPoints = [];

      this.callback?.onStateChanged(this.state);
      console.info(TAG, '轨迹追踪已启动');
      return true;
    } catch (error) {
      const err = error as BusinessError;
      console.error(TAG, `启动追踪失败: ${err.code} - ${err.message}`);
      this.callback?.onError(err);
      return false;
    }
  }

  /**
   * 暂停追踪
   */
  pauseTracking(): void {
    if (this.state !== TrackingState.TRACKING) return;
    this.state = TrackingState.PAUSED;
    this.pauseTime = Date.now();
    this.callback?.onStateChanged(this.state);
    console.info(TAG, '轨迹追踪已暂停');
  }

  /**
   * 恢复追踪
   */
  resumeTracking(): void {
    if (this.state !== TrackingState.PAUSED) return;
    this.totalPauseDuration += Date.now() - this.pauseTime;
    this.state = TrackingState.TRACKING;
    this.callback?.onStateChanged(this.state);
    console.info(TAG, '轨迹追踪已恢复');
  }

  /**
   * 停止追踪
   */
  stopTracking(): TrackPoint[] {
    if (this.callbackId !== -1) {
      try {
        geoLocationManager.off('locationChange', this.callbackId);
      } catch (e) {
        // 忽略
      }
      this.callbackId = -1;
    }

    const points = [...this.trackPoints];
    this.state = TrackingState.IDLE;
    this.callback?.onStateChanged(this.state);
    console.info(TAG, `轨迹追踪已停止, 共${points.length}个点`);
    return points;
  }

  /**
   * 处理位置更新
   */
  private handleLocationUpdate(location: geoLocationManager.Location): void {
    if (this.state !== TrackingState.TRACKING) return;

    // 第1步:精度过滤
    if (location.accuracy > this.MIN_ACCURACY) {
      console.debug(TAG, `精度不足: ${location.accuracy.toFixed(1)}m,跳过`);
      return;
    }

    // 第2步:速度异常过滤
    if (location.speed > this.MAX_SPEED) {
      console.debug(TAG, `速度异常: ${location.speed.toFixed(1)}m/s,跳过`);
      return;
    }

    // 第3步:卡尔曼滤波平滑
    const smoothed = this.kalmanFilter(location.latitude, location.longitude);

    // 第4步:漂移检测
    if (this.trackPoints.length > 0) {
      const lastPoint = this.trackPoints[this.trackPoints.length - 1];
      const drift = Math.abs(smoothed.lat - lastPoint.latitude) +
                    Math.abs(smoothed.lng - lastPoint.longitude);
      if (drift > this.DRIFT_THRESHOLD && location.speed < 1) {
        console.debug(TAG, '检测到静态漂移,跳过');
        return;
      }
    }

    // 第5步:构建轨迹点
    const duration = Date.now() - this.startTime - this.totalPauseDuration;
    const distance = this.calculateTotalDistance(smoothed.lat, smoothed.lng);

    const trackPoint: TrackPoint = {
      latitude: smoothed.lat,
      longitude: smoothed.lng,
      altitude: location.altitude,
      accuracy: location.accuracy,
      speed: location.speed,
      bearing: location.direction,
      timestamp: location.timeStamp,
      distance: distance,
      duration: duration
    };

    this.trackPoints.push(trackPoint);

    // 第6步:通知回调
    this.callback?.onPointAdded(trackPoint);
    this.callback?.onStatisticsUpdated(this.calculateStatistics());
  }

  /**
   * 卡尔曼滤波
   */
  private kalmanFilter(lat: number, lng: number): { lat: number; lng: number } {
    const Q = 0.001; // 过程噪声
    const R = 0.01;  // 观测噪声

    if (!this.kfInitialized) {
      this.kfLat = lat;
      this.kfLng = lng;
      this.kfPLat = 1;
      this.kfPLng = 1;
      this.kfInitialized = true;
      return { lat, lng };
    }

    // 预测
    this.kfPLat += Q;
    this.kfPLng += Q;

    // 更新
    const KLat = this.kfPLat / (this.kfPLat + R);
    const KLng = this.kfPLng / (this.kfPLng + R);

    this.kfLat += KLat * (lat - this.kfLat);
    this.kfLng += KLng * (lng - this.kfLng);
    this.kfPLat *= (1 - KLat);
    this.kfPLng *= (1 - KLng);

    return { lat: this.kfLat, lng: this.kfLng };
  }

  /**
   * 计算累计距离
   */
  private calculateTotalDistance(newLat: number, newLng: number): number {
    if (this.trackPoints.length === 0) return 0;

    const lastPoint = this.trackPoints[this.trackPoints.length - 1];
    const segmentDistance = this.haversineDistance(
      lastPoint.latitude, lastPoint.longitude, newLat, newLng
    );

    return lastPoint.distance + segmentDistance;
  }

  /**
   * Haversine距离计算
   */
  private haversineDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
    const R = 6371000;
    const dLat = (lat2 - lat1) * Math.PI / 180;
    const dLng = (lng2 - lng1) * Math.PI / 180;
    const a = Math.sin(dLat / 2) ** 2 +
      Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
      Math.sin(dLng / 2) ** 2;
    return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  }

  /**
   * 计算统计指标
   */
  calculateStatistics(): TrackStatistics {
    if (this.trackPoints.length === 0) {
      return {
        totalDistance: 0, totalDuration: 0, avgSpeed: 0,
        maxSpeed: 0, minAltitude: 0, maxAltitude: 0,
        elevationGain: 0, elevationLoss: 0, pointCount: 0
      };
    }

    const lastPoint = this.trackPoints[this.trackPoints.length - 1];
    let maxSpeed = 0;
    let minAlt = Infinity;
    let maxAlt = -Infinity;
    let elevGain = 0;
    let elevLoss = 0;

    for (let i = 0; i < this.trackPoints.length; i++) {
      const p = this.trackPoints[i];
      if (p.speed > maxSpeed) maxSpeed = p.speed;
      if (p.altitude < minAlt) minAlt = p.altitude;
      if (p.altitude > maxAlt) maxAlt = p.altitude;

      // 累计爬升/下降
      if (i > 0) {
        const altDiff = p.altitude - this.trackPoints[i - 1].altitude;
        if (altDiff > 1) elevGain += altDiff;
        if (altDiff < -1) elevLoss += Math.abs(altDiff);
      }
    }

    return {
      totalDistance: lastPoint.distance,
      totalDuration: lastPoint.duration,
      avgSpeed: lastPoint.duration > 0 ? lastPoint.distance / (lastPoint.duration / 1000) : 0,
      maxSpeed: maxSpeed,
      minAltitude: minAlt === Infinity ? 0 : minAlt,
      maxAltitude: maxAlt === -Infinity ? 0 : maxAlt,
      elevationGain: elevGain,
      elevationLoss: elevLoss,
      pointCount: this.trackPoints.length
    };
  }

  /**
   * 获取所有轨迹点
   */
  getTrackPoints(): TrackPoint[] {
    return [...this.trackPoints];
  }

  /**
   * 获取当前状态
   */
  getState(): TrackingState {
    return this.state;
  }
}

3.2 Douglas-Peucker轨迹压缩

// TrackCompressor.ets
// 功能:Douglas-Peucker轨迹压缩算法实现

const TAG = '[TrackCompressor]';

// 轨迹点(简化版)
export interface SimpleTrackPoint {
  latitude: number;
  longitude: number;
  timestamp: number;
}

export class TrackCompressor {
  /**
   * Douglas-Peucker算法压缩轨迹
   * @param points 原始轨迹点
   * @param epsilon 压缩阈值(米)
   * @returns 压缩后的轨迹点
   */
  static compress(points: SimpleTrackPoint[], epsilon: number): SimpleTrackPoint[] {
    if (points.length <= 2) {
      return [...points];
    }

    // 找到距离首尾连线最远的点
    let maxDist = 0;
    let maxIndex = 0;

    const first = points[0];
    const last = points[points.length - 1];

    for (let i = 1; i < points.length - 1; i++) {
      const dist = TrackCompressor.perpendicularDistance(points[i], first, last);
      if (dist > maxDist) {
        maxDist = dist;
        maxIndex = i;
      }
    }

    // 递归压缩
    if (maxDist > epsilon) {
      const left = TrackCompressor.compress(points.slice(0, maxIndex + 1), epsilon);
      const right = TrackCompressor.compress(points.slice(maxIndex), epsilon);
      // 合并(去掉重复的中间点)
      return [...left.slice(0, -1), ...right];
    } else {
      // 所有点到首尾连线的距离都小于阈值,只保留首尾
      return [first, last];
    }
  }

  /**
   * 计算点到线段的垂直距离(米)
   */
  static perpendicularDistance(
    point: SimpleTrackPoint,
    lineStart: SimpleTrackPoint,
    lineEnd: SimpleTrackPoint
  ): number {
    const R = 6371000; // 地球半径

    // 将经纬度转换为平面坐标(小范围内近似)
    const x0 = point.longitude * R * Math.cos(point.latitude * Math.PI / 180);
    const y0 = point.latitude * R;
    const x1 = lineStart.longitude * R * Math.cos(lineStart.latitude * Math.PI / 180);
    const y1 = lineStart.latitude * R;
    const x2 = lineEnd.longitude * R * Math.cos(lineEnd.latitude * Math.PI / 180);
    const y2 = lineEnd.latitude * R;

    // 计算点到线段的距离
    const dx = x2 - x1;
    const dy = y2 - y1;

    if (dx === 0 && dy === 0) {
      // 线段退化为点
      return Math.sqrt((x0 - x1) ** 2 + (y0 - y1) ** 2);
    }

    const t = Math.max(0, Math.min(1,
      ((x0 - x1) * dx + (y0 - y1) * dy) / (dx * dx + dy * dy)
    ));

    const projX = x1 + t * dx;
    const projY = y1 + t * dy;

    return Math.sqrt((x0 - projX) ** 2 + (y0 - projY) ** 2);
  }

  /**
   * 滑动窗口压缩
   * 适用于实时流式压缩场景
   */
  static slidingWindowCompress(
    points: SimpleTrackPoint[],
    windowSize: number,
    epsilon: number
  ): SimpleTrackPoint[] {
    if (points.length <= 2) return [...points];

    const result: SimpleTrackPoint[] = [points[0]];
    let windowStart = 0;

    for (let i = 1; i < points.length; i++) {
      if (i - windowStart >= windowSize) {
        // 对窗口内的点进行压缩
        const windowPoints = points.slice(windowStart, i + 1);
        const compressed = TrackCompressor.compress(windowPoints, epsilon);
        result.push(...compressed.slice(1)); // 跳过第一个点(已添加)
        windowStart = i;
      }
    }

    // 处理剩余的点
    if (windowStart < points.length - 1) {
      const remaining = points.slice(windowStart);
      const compressed = TrackCompressor.compress(remaining, epsilon);
      result.push(...compressed.slice(1));
    }

    return result;
  }

  /**
   * 计算压缩率
   */
  static getCompressionRatio(original: number, compressed: number): number {
    if (original === 0) return 0;
    return ((original - compressed) / original) * 100;
  }
}

3.3 轨迹存储管理

// TrackStorageManager.ets
// 功能:轨迹数据本地存储与持久化管理

import { relationalStore } from '@kit.ArkData';
import { TrackPoint, TrackStatistics } from './TrackTrackingEngine';
import { SimpleTrackPoint, TrackCompressor } from './TrackCompressor';

const TAG = '[TrackStorageManager]';

// 轨迹记录
export interface TrackRecord {
  id: string;
  name: string;
  startTime: number;
  endTime: number;
  statistics: TrackStatistics;
  pointCount: number;
  compressedPointCount: number;
  compressionRatio: number;
}

export class TrackStorageManager {
  private store: relationalStore.RdbStore | null = null;
  private readonly DB_NAME = 'TrackDB';
  private readonly TRACK_TABLE = 'track_records';
  private readonly POINT_TABLE = 'track_points';

  /**
   * 初始化数据库
   */
  async init(context: Context): Promise<void> {
    const config: relationalStore.StoreConfig = {
      name: this.DB_NAME,
      securityLevel: relationalStore.SecurityLevel.S1
    };

    try {
      this.store = await relationalStore.getRdbStore(context, config);
      await this.createTables();
      console.info(TAG, '数据库初始化成功');
    } catch (error) {
      console.error(TAG, '数据库初始化失败', JSON.stringify(error));
    }
  }

  /**
   * 创建数据表
   */
  private async createTables(): Promise<void> {
    if (!this.store) return;

    // 轨迹记录表
    const createTrackTable = `
      CREATE TABLE IF NOT EXISTS ${this.TRACK_TABLE} (
        id TEXT PRIMARY KEY,
        name TEXT NOT NULL,
        start_time INTEGER NOT NULL,
        end_time INTEGER NOT NULL,
        total_distance REAL DEFAULT 0,
        total_duration INTEGER DEFAULT 0,
        avg_speed REAL DEFAULT 0,
        max_speed REAL DEFAULT 0,
        point_count INTEGER DEFAULT 0,
        compressed_point_count INTEGER DEFAULT 0
      )
    `;

    // 轨迹点表
    const createPointTable = `
      CREATE TABLE IF NOT EXISTS ${this.POINT_TABLE} (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        track_id TEXT NOT NULL,
        sequence INTEGER NOT NULL,
        latitude REAL NOT NULL,
        longitude REAL NOT NULL,
        altitude REAL DEFAULT 0,
        accuracy REAL DEFAULT 0,
        speed REAL DEFAULT 0,
        bearing REAL DEFAULT 0,
        timestamp INTEGER NOT NULL,
        distance REAL DEFAULT 0,
        duration INTEGER DEFAULT 0,
        FOREIGN KEY (track_id) REFERENCES ${this.TRACK_TABLE}(id)
      )
    `;

    await this.store.executeSql(createTrackTable);
    await this.store.executeSql(createPointTable);

    // 创建索引
    await this.store.executeSql(
      `CREATE INDEX IF NOT EXISTS idx_point_track_id ON ${this.POINT_TABLE}(track_id)`
    );
    await this.store.executeSql(
      `CREATE INDEX IF NOT EXISTS idx_track_start_time ON ${this.TRACK_TABLE}(start_time)`
    );
  }

  /**
   * 保存轨迹
   * @param trackId 轨迹ID
   * @param name 轨迹名称
   * @param points 原始轨迹点
   * @param stats 统计数据
   */
  async saveTrack(
    trackId: string,
    name: string,
    points: TrackPoint[],
    stats: TrackStatistics
  ): Promise<boolean> {
    if (!this.store || points.length === 0) return false;

    try {
      // 压缩轨迹
      const simplePoints: SimpleTrackPoint[] = points.map(p => ({
        latitude: p.latitude,
        longitude: p.longitude,
        timestamp: p.timestamp
      }));
      const compressed = TrackCompressor.compress(simplePoints, 5); // 5米阈值
      const compressionRatio = TrackCompressor.getCompressionRatio(
        points.length, compressed.length
      );

      // 插入轨迹记录
      const trackValues: relationalStore.ValuesBucket = {
        id: trackId,
        name: name,
        start_time: points[0].timestamp,
        end_time: points[points.length - 1].timestamp,
        total_distance: stats.totalDistance,
        total_duration: stats.totalDuration,
        avg_speed: stats.avgSpeed,
        max_speed: stats.maxSpeed,
        point_count: stats.pointCount,
        compressed_point_count: compressed.length
      };
      await this.store.insert(this.TRACK_TABLE, trackValues);

      // 批量插入轨迹点
      for (let i = 0; i < points.length; i++) {
        const pointValues: relationalStore.ValuesBucket = {
          track_id: trackId,
          sequence: i,
          latitude: points[i].latitude,
          longitude: points[i].longitude,
          altitude: points[i].altitude,
          accuracy: points[i].accuracy,
          speed: points[i].speed,
          bearing: points[i].bearing,
          timestamp: points[i].timestamp,
          distance: points[i].distance,
          duration: points[i].duration
        };
        await this.store.insert(this.POINT_TABLE, pointValues);
      }

      console.info(TAG, `轨迹已保存: ${trackId}, 原始${points.length}点, ` +
        `压缩后${compressed.length}点, 压缩率${compressionRatio.toFixed(1)}%`);
      return true;
    } catch (error) {
      console.error(TAG, '保存轨迹失败', JSON.stringify(error));
      return false;
    }
  }

  /**
   * 查询轨迹列表
   */
  async getTrackList(): Promise<TrackRecord[]> {
    if (!this.store) return [];

    try {
      const predicates = new relationalStore.RdbPredicates(this.TRACK_TABLE);
      predicates.orderByDesc('start_time');
      const resultSet = await this.store.query(predicates);

      const records: TrackRecord[] = [];
      while (resultSet.goToNextRow()) {
        records.push({
          id: resultSet.getString(resultSet.getColumnIndex('id')),
          name: resultSet.getString(resultSet.getColumnIndex('name')),
          startTime: resultSet.getLong(resultSet.getColumnIndex('start_time')),
          endTime: resultSet.getLong(resultSet.getColumnIndex('end_time')),
          statistics: {
            totalDistance: resultSet.getDouble(resultSet.getColumnIndex('total_distance')),
            totalDuration: resultSet.getLong(resultSet.getColumnIndex('total_duration')),
            avgSpeed: resultSet.getDouble(resultSet.getColumnIndex('avg_speed')),
            maxSpeed: resultSet.getDouble(resultSet.getColumnIndex('max_speed')),
            minAltitude: 0,
            maxAltitude: 0,
            elevationGain: 0,
            elevationLoss: 0,
            pointCount: resultSet.getLong(resultSet.getColumnIndex('point_count'))
          },
          pointCount: resultSet.getLong(resultSet.getColumnIndex('point_count')),
          compressedPointCount: resultSet.getLong(resultSet.getColumnIndex('compressed_point_count')),
          compressionRatio: 0
        });
      }
      resultSet.close();
      return records;
    } catch (error) {
      console.error(TAG, '查询轨迹列表失败', JSON.stringify(error));
      return [];
    }
  }

  /**
   * 删除轨迹
   */
  async deleteTrack(trackId: string): Promise<boolean> {
    if (!this.store) return false;

    try {
      // 先删除轨迹点
      const pointPredicates = new relationalStore.RdbPredicates(this.POINT_TABLE);
      pointPredicates.equalTo('track_id', trackId);
      await this.store.delete(pointPredicates);

      // 再删除轨迹记录
      const trackPredicates = new relationalStore.RdbPredicates(this.TRACK_TABLE);
      trackPredicates.equalTo('id', trackId);
      await this.store.delete(trackPredicates);

      console.info(TAG, `轨迹已删除: ${trackId}`);
      return true;
    } catch (error) {
      console.error(TAG, '删除轨迹失败', JSON.stringify(error));
      return false;
    }
  }
}

3.4 运动追踪完整UI

// SportTrackingPage.ets
// 功能:运动追踪完整UI,展示实时轨迹与运动数据

import { geoLocationManager } from '@kit.LocationKit';
import {
  TrackTrackingEngine,
  TrackPoint,
  TrackStatistics,
  TrackingState,
  TrackingCallback
} from './TrackTrackingEngine';
import { TrackCompressor } from './TrackCompressor';

@Entry
@Component
struct SportTrackingPage {
  private trackingEngine = new TrackTrackingEngine();

  // 状态变量
  @State trackingState: TrackingState = TrackingState.IDLE;
  @State totalDistance: string = '0.00';
  @State totalDuration: string = '00:00:00';
  @State avgSpeed: string = '0.00';
  @State currentSpeed: string = '0.00';
  @State maxSpeed: string = '0.00';
  @State elevationGain: string = '0';
  @State pointCount: number = 0;
  @State recentPoints: string[] = [];

  aboutToAppear(): void {
    this.trackingEngine.setCallback({
      onPointAdded: (point: TrackPoint) => {
        this.pointCount++;
        this.currentSpeed = (point.speed * 3.6).toFixed(1); // m/s → km/h

        // 更新最近轨迹点
        const pointStr = `(${point.latitude.toFixed(6)}, ${point.longitude.toFixed(6)}) ` +
          `${(point.speed * 3.6).toFixed(1)}km/h`;
        this.recentPoints.unshift(pointStr);
        if (this.recentPoints.length > 10) {
          this.recentPoints.pop();
        }
      },
      onStatisticsUpdated: (stats: TrackStatistics) => {
        this.totalDistance = (stats.totalDistance / 1000).toFixed(2);
        this.avgSpeed = (stats.avgSpeed * 3.6).toFixed(1);
        this.maxSpeed = (stats.maxSpeed * 3.6).toFixed(1);
        this.elevationGain = stats.elevationGain.toFixed(0);
        this.updateDuration(stats.totalDuration);
      },
      onStateChanged: (state: TrackingState) => {
        this.trackingState = state;
      },
      onError: (error) => {
        console.error('追踪错误', JSON.stringify(error));
      }
    });
  }

  aboutToDisappear(): void {
    if (this.trackingState !== TrackingState.IDLE) {
      this.trackingEngine.stopTracking();
    }
  }

  /**
   * 更新时长显示
   */
  private updateDuration(ms: number): void {
    const totalSeconds = Math.floor(ms / 1000);
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;
    this.totalDuration = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
  }

  build() {
    Column() {
      // 标题
      Text('运动追踪')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .margin({ top: 20, bottom: 16 })

      // 核心数据展示
      Column() {
        // 总距离(大字显示)
        Text(this.totalDistance)
          .fontSize(56)
          .fontWeight(FontWeight.Bold)
          .fontColor('#4ECDC4')
        Text('公里')
          .fontSize(14)
          .fontColor('#AAAAAA')
          .margin({ top: 4 })

        // 时长
        Text(this.totalDuration)
          .fontSize(28)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Medium)
          .margin({ top: 8 })
      }
      .margin({ bottom: 20 })

      // 详细数据网格
      Grid() {
        GridItem() {
          this.DataCard('当前速度', `${this.currentSpeed}`, 'km/h')
        }
        GridItem() {
          this.DataCard('平均速度', `${this.avgSpeed}`, 'km/h')
        }
        GridItem() {
          this.DataCard('最高速度', `${this.maxSpeed}`, 'km/h')
        }
        GridItem() {
          this.DataCard('累计爬升', `${this.elevationGain}`, 'm')
        }
      }
      .columnsTemplate('1fr 1fr')
      .rowsTemplate('1fr 1fr')
      .width('90%')
      .height(180)
      .columnsGap(8)
      .rowsGap(8)

      // 采样点数
      Row() {
        Text(`采样点: ${this.pointCount}`)
          .fontSize(12)
          .fontColor('#888888')
        Text(this.trackingState === TrackingState.TRACKING ? '● 追踪中' : 
             this.trackingState === TrackingState.PAUSED ? '⏸ 已暂停' : '○ 未开始')
          .fontSize(12)
          .fontColor(this.trackingState === TrackingState.TRACKING ? '#4ECDC4' : '#888888')
          .margin({ left: 16 })
      }
      .width('90%')
      .margin({ top: 16 })

      // 操作按钮
      Row() {
        if (this.trackingState === TrackingState.IDLE) {
          Button('开始追踪')
            .width('90%')
            .height(52)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .backgroundColor('#4ECDC4')
            .borderRadius(26)
            .onClick(() => this.trackingEngine.startTracking())
        } else if (this.trackingState === TrackingState.TRACKING) {
          Button('暂停')
            .width('42%')
            .height(52)
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FDCB6E')
            .fontColor('#333')
            .borderRadius(26)
            .onClick(() => this.trackingEngine.pauseTracking())

          Button('结束')
            .width('42%')
            .height(52)
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FF6B6B')
            .borderRadius(26)
            .onClick(() => {
              const points = this.trackingEngine.stopTracking();
              console.info(`追踪结束,共${points.length}个点`);
            })
        } else if (this.trackingState === TrackingState.PAUSED) {
          Button('继续')
            .width('42%')
            .height(52)
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#4ECDC4')
            .borderRadius(26)
            .onClick(() => this.trackingEngine.resumeTracking())

          Button('结束')
            .width('42%')
            .height(52)
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FF6B6B')
            .borderRadius(26)
            .onClick(() => this.trackingEngine.stopTracking())
        }
      }
      .width('90%')
      .justifyContent(FlexAlign.SpaceBetween)
      .margin({ top: 20 })

      // 最近轨迹点
      if (this.recentPoints.length > 0) {
        Text('最近轨迹点')
          .fontSize(14)
          .fontColor('#AAAAAA')
          .width('90%')
          .margin({ top: 20, bottom: 8 })

        List() {
          ForEach(this.recentPoints, (point: string, index: number) => {
            ListItem() {
              Text(point)
                .fontSize(11)
                .fontColor(index === 0 ? '#4ECDC4' : '#888888')
                .width('100%')
                .padding({ left: 8, right: 8, top: 4, bottom: 4 })
            }
          })
        }
        .width('90%')
        .height('20%')
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1A1A2E')
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  DataCard(title: string, value: string, unit: string) {
    Column() {
      Text(title)
        .fontSize(11)
        .fontColor('#888888')
      Row() {
        Text(value)
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        Text(unit)
          .fontSize(11)
          .fontColor('#888888')
          .margin({ left: 4, bottom: 4 })
      }
      .alignItems(VerticalAlign.Bottom)
      .margin({ top: 4 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .borderRadius(12)
    .backgroundColor('rgba(255,255,255,0.06)')
    .backdropBlur(10)
  }
}

四、踩坑与注意事项

4.1 位置订阅耗电优化

持续定位是最大的耗电来源之一,必须做好功耗优化:

// 动态调整定位频率
class AdaptiveLocationStrategy {
  private lastSpeed: number = 0;
  private stationaryCount: number = 0;

  /**
   * 根据运动状态动态调整定位参数
   */
  getAdaptiveRequest(): geoLocationManager.ContinuousLocationRequest {
    if (this.lastSpeed < 0.5) {
      // 静止状态:低频定位
      this.stationaryCount++;
      return {
        priority: geoLocationManager.LocationRequestPriority.LOW_POWER_PRIORITY,
        scenario: geoLocationManager.LocationRequestScenario.DAILY_LIFE_SERVICE,
        timeInterval: Math.min(5 + this.stationaryCount * 2, 30), // 逐步降低频率
        distanceInterval: 10,
        maxAccuracy: 100
      };
    } else if (this.lastSpeed < 5) {
      // 步行:中频定位
      this.stationaryCount = 0;
      return {
        priority: geoLocationManager.LocationRequestPriority.ACCURACY_PRIORITY,
        scenario: geoLocationManager.LocationRequestScenario.NAVIGATION,
        timeInterval: 3,
        distanceInterval: 3,
        maxAccuracy: 50
      };
    } else {
      // 驾车:高频定位
      this.stationaryCount = 0;
      return {
        priority: geoLocationManager.LocationRequestPriority.ACCURACY_PRIORITY,
        scenario: geoLocationManager.LocationRequestScenario.NAVIGATION,
        timeInterval: 1,
        distanceInterval: 5,
        maxAccuracy: 30
      };
    }
  }

  updateSpeed(speed: number): void {
    this.lastSpeed = speed;
  }
}

4.2 轨迹点内存管理

长时间追踪可能产生大量轨迹点,导致内存溢出:

// ❌ 错误:无限累积轨迹点
private allPoints: TrackPoint[] = [];
// 追踪24小时可能产生86400个点

// ✅ 正确:分批存储 + 内存只保留最近点
class MemorySafeTracker {
  private recentPoints: TrackPoint[] = [];  // 内存中只保留最近1000个点
  private readonly MAX_MEMORY_POINTS = 1000;
  private batchBuffer: TrackPoint[] = [];    // 批量写入缓冲
  private readonly BATCH_SIZE = 100;

  addPoint(point: TrackPoint): void {
    this.recentPoints.push(point);
    this.batchBuffer.push(point);

    // 内存溢出保护
    if (this.recentPoints.length > this.MAX_MEMORY_POINTS) {
      this.recentPoints.shift();
    }

    // 批量写入数据库
    if (this.batchBuffer.length >= this.BATCH_SIZE) {
      this.flushToDatabase();
    }
  }

  private flushToDatabase(): void {
    // 异步写入数据库
    const batch = [...this.batchBuffer];
    this.batchBuffer = [];
    // ... 写入数据库
  }
}

4.3 轨迹精度与采样频率

运动类型 建议频率 建议距离间隔 说明
跑步 1~2秒 3~5米 需要精确配速
骑行 2~3秒 5~10米 速度较快需适当加密
步行 3~5秒 5~10米 速度慢可降低频率
驾车 1~2秒 10~20米 高速需高频采样
徒步 5~10秒 10~20米 低速可大幅降低频率

4.4 常见错误码

错误码 含义 解决方案
3301000 位置服务不可用 检查系统定位开关
3301100 定位开关关闭 引导用户开启
3301300 请求频率过高 降低定位频率
3301400 权限未授予 申请位置权限
3301500 定位失败 检查GPS信号

五、HarmonyOS 6适配

5.1 低功耗轨迹追踪

HarmonyOS 6引入了低功耗轨迹追踪模式

  • 系统级轨迹记录服务,应用无需持续运行
  • 通过系统服务后台记录轨迹,应用按需获取
  • 功耗降低50%以上,适合长时间追踪场景
// HarmonyOS 6 低功耗轨迹追踪(预期API)
const lowPowerConfig = {
  scenario: 'running',      // 运动类型
  minInterval: 5,           // 最小间隔
  accuracy: 'medium',       // 精度级别
  autoPause: true           // 自动暂停
};

// 注册系统级轨迹追踪
geoLocationManager.startLowPowerTracking(lowPowerConfig);

5.2 轨迹智能分段

HarmonyOS 6支持轨迹智能分段:

  • 自动识别运动类型变化(步行→驾车)
  • 自动在停留点处分割轨迹
  • 为每段轨迹生成独立的统计信息

5.3 轨迹数据安全

HarmonyOS 6增强了轨迹数据安全:

  • 轨迹数据加密存储
  • 轨迹分享支持有效期和访问次数限制
  • 轨迹数据自动过期删除

六、总结

本文深入探讨了HarmonyOS位置订阅与轨迹追踪的技术原理与开发实践:

知识点 关键内容
位置订阅 on(‘locationChange’)持续定位、时间/距离间隔
轨迹处理 卡尔曼滤波平滑、漂移检测、精度过滤
轨迹压缩 Douglas-Peucker算法、滑动窗口压缩
运动统计 距离、配速、爬升、海拔等指标计算
数据存储 关系型数据库存储、批量写入、分批管理
功耗优化 动态频率调整、静止降频、批量存储
HarmonyOS 6 低功耗追踪、智能分段、数据安全

核心建议

  1. 生产环境务必实现精度过滤→漂移修正→卡尔曼平滑的完整处理链路
  2. 长时间追踪必须实现内存安全机制,分批存储避免OOM
  3. 根据运动类型动态调整定位频率,平衡精度与功耗
  4. 使用Douglas-Peucker压缩轨迹数据,减少存储和传输开销
  5. 页面销毁时务必停止位置订阅,避免后台持续耗电

至此,位置服务系列5篇文章全部完成。从GPS定位、网络定位、权限合规、地理围栏到轨迹追踪,我们完整覆盖了HarmonyOS位置服务的核心能力。希望这些内容能帮助你构建出优秀的LBS应用!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。