HarmonyOS运动开发:如何绘制运动速度轨迹

前言

在户外运动应用中,绘制运动速度轨迹不仅可以直观地展示用户的运动路线,还能通过颜色变化反映速度的变化,帮助用户更好地了解自己的运动状态。然而,如何在鸿蒙系统中实现这一功能呢?本文将结合实际开发经验,深入解析从数据处理到地图绘制的全过程,带你一步步掌握如何绘制运动速度轨迹。

一、核心工具:轨迹颜色与优化

绘制运动速度轨迹的关键在于两个工具类:PathGradientToolPathSmoothTool。这两个工具类分别用于处理轨迹的颜色和优化轨迹的平滑度。

1.轨迹颜色工具类:PathGradientTool

PathGradientTool的作用是根据运动速度为轨迹点分配颜色。速度越快,颜色越接近青色;速度越慢,颜色越接近红色。以下是PathGradientTool的核心逻辑:

typescript 复制代码
export class PathGradientTool {
  /**
   * 获取路径染色数组
   * @param points 路径点数据
   * @param colorInterval 取色间隔,单位m,范围20-2000,多长距离设置一次颜色
   * @returns 路径染色数组
   */
  static getPathColors(points: RunPoint[], colorInterval: number): string[] | null {
    if (!points || points.length < 2) {
      return null;
    }

    let interval = Math.max(20, Math.min(2000, colorInterval));
    const pointsSize = points.length;
    const speedList: number[] = [];
    const colorList: string[] = [];
    let index = 0;
    let lastDistance = 0;
    let lastTime = 0;
    let maxSpeed = 0;
    let minSpeed = 0;

    // 第一遍遍历:收集速度数据
    points.forEach(point => {
      index++;
      if (point.totalDistance - lastDistance > interval) {
        let currentSpeed = 0;
        if (point.netDuration - lastTime > 0) {
          currentSpeed = (point.netDistance - lastDistance) / (point.netDuration - lastTime);
        }
        maxSpeed = Math.max(maxSpeed, currentSpeed);
        minSpeed = minSpeed === 0 ? currentSpeed : Math.min(minSpeed, currentSpeed);
        lastDistance = point.netDistance;
        lastTime = point.netDuration;

        // 为每个间隔内的点添加相同的速度
        for (let i = 0; i < index; i++) {
          speedList.push(currentSpeed);
        }
        // 添加屏障
        speedList.push(Number.MAX_VALUE);
        index = 0;
      }
    });

    // 处理剩余点
    if (index > 0) {
      const lastPoint = points[points.length - 1];
      let currentSpeed = 0;
      if (lastPoint.netDuration - lastTime > 0) {
        currentSpeed = (lastPoint.netDistance - lastDistance) / (lastPoint.netDuration - lastTime);
      }
      for (let i = 0; i < index; i++) {
        speedList.push(currentSpeed);
      }
    }

    // 确保速度列表长度与点数一致
    if (speedList.length !== points.length) {
      // 调整速度列表长度
      if (speedList.length > points.length) {
        speedList.length = points.length;
      } else {
        const lastSpeed = speedList.length > 0 ? speedList[speedList.length - 1] : 0;
        while (speedList.length < points.length) {
          speedList.push(lastSpeed);
        }
      }
    }

    // 生成颜色列表
    let lastColor = '';
    let hasBarrier = false;
    for (let i = 0; i < speedList.length; i++) {
      const speed = speedList[i];
      if (speed === Number.MAX_VALUE) {
        hasBarrier = true;
        continue;
      }

      const color = PathGradientTool.getAgrSpeedColorHashMap(speed, maxSpeed, minSpeed);
      if (hasBarrier) {
        hasBarrier = false;
        if (color.toUpperCase() === lastColor.toUpperCase()) {
          colorList.push(PathGradientTool.getBarrierColor(color));
          continue;
        }
      }
      colorList.push(color);
      lastColor = color;
    }

    // 确保颜色列表长度与点数一致
    if (colorList.length !== points.length) {
      if (colorList.length > points.length) {
        colorList.length = points.length;
      } else {
        const lastColor = colorList.length > 0 ? colorList[colorList.length - 1] : '#FF3032';
        while (colorList.length < points.length) {
          colorList.push(lastColor);
        }
      }
    }

    return colorList;
  }

  /**
   * 根据速度定义不同的颜色区间来绘制轨迹
   * @param speed 速度
   * @param maxSpeed 最大速度
   * @param minSpeed 最小速度
   * @returns 颜色值
   */
  private static getAgrSpeedColorHashMap(speed: number, maxSpeed: number, minSpeed: number): string {
    const range = maxSpeed - minSpeed;
    if (speed <= minSpeed + range * 0.2) { // 0-20%区间配速
      return '#FF3032';
    } else if (speed <= minSpeed + range * 0.4) { // 20%-40%区间配速
      return '#FA7B22';
    } else if (speed <= minSpeed + range * 0.6) { // 40%-60%区间配速
      return '#F5BE14';
    } else if (speed <= minSpeed + range * 0.8) { // 60%-80%区间配速
      return '#7AC36C';
    } else { // 80%-100%区间配速
      return '#00C8C3';
    }
  }
}

2.轨迹优化工具类:PathSmoothTool

PathSmoothTool的作用是优化轨迹的平滑度,减少轨迹点的噪声和冗余。以下是PathSmoothTool的核心逻辑:

typescript 复制代码
export class PathSmoothTool {
  private mIntensity: number = 3;
  private mThreshhold: number = 0.01;
  private mNoiseThreshhold: number = 10;

  /**
   * 轨迹平滑优化
   * @param originlist 原始轨迹list,list.size大于2
   * @returns 优化后轨迹list
   */
  pathOptimize(originlist: RunLatLng[]): RunLatLng[] {
    const list = this.removeNoisePoint(originlist); // 去噪
    const afterList = this.kalmanFilterPath(list, this.mIntensity); // 滤波
    const pathoptimizeList = this.reducerVerticalThreshold(afterList, this.mThreshhold); // 抽稀
    return pathoptimizeList;
  }

  /**
   * 轨迹线路滤波
   * @param originlist 原始轨迹list,list.size大于2
   * @returns 滤波处理后的轨迹list
   */
  kalmanFilterPath(originlist: RunLatLng[], intensity: number = this.mIntensity): RunLatLng[] {
    const kalmanFilterList: RunLatLng[] = [];
    if (!originlist || originlist.length <= 2) return kalmanFilterList;

    this.initial(); // 初始化滤波参数
    let lastLoc = originlist[0];
    kalmanFilterList.push(lastLoc);

    for (let i = 1; i < originlist.length; i++) {
      const curLoc = originlist[i];
      const latLng = this.kalmanFilterPoint(lastLoc, curLoc, intensity);
      if (latLng) {
        kalmanFilterList.push(latLng);
        lastLoc = latLng;
      }
    }
    return kalmanFilterList;
  }

  /**
   * 单点滤波
   * @param lastLoc 上次定位点坐标
   * @param curLoc 本次定位点坐标
   * @returns 滤波后本次定位点坐标值
   */
  kalmanFilterPoint(lastLoc: RunLatLng, curLoc: RunLatLng, intensity: number = this.mIntensity): RunLatLng | null {
    if (this.pdelt_x === 0 || this.pdelt_y === 0) {
      this.initial();
    }

    if (!lastLoc || !curLoc) return null;

    intensity = Math.max(1, Math.min(5, intensity));
    let filteredLoc = curLoc;

    for (let j = 0; j < intensity; j++) {
      filteredLoc = this.kalmanFilter(lastLoc.longitude, filteredLoc.longitude, lastLoc.latitude, filteredLoc.latitude);
    }

    return filteredLoc;
  }

  轨迹抽稀

• @param inPoints 待抽稀的轨迹list

• @param threshHold 阈值

• @returns 抽稀后的轨迹list
/
private reducerVerticalThreshold(inPoints:RunLatLng[],threshHold:number):RunLatLng[]{
if(!inPoints||inPoints.length<=2)return inPoints||[];


    const ret: RunLatLng[] = [];
    for (let i = 0; i < inPoints.length; i++) {
      const pre = this.getLastLocation(ret);
      const cur = inPoints[i];

      if (!pre || i === inPoints.length - 1) {
        ret.push(cur);
        continue;
      }

      const next = inPoints[i + 1];
      const distance = this.calculateDistanceFromPoint(cur, pre, next);
      if (distance > threshHold) {
        ret.push(cur);
      }
    }
    return ret;

}

/

• 轨迹去噪

• @param inPoints 原始轨迹list

• @returns 去噪后的轨迹list
/
removeNoisePoint(inPoints:RunLatLng[]):RunLatLng[]{
if(!inPoints||inPoints.length<=2)return inPoints||[];


    const ret: RunLatLng[] = [];
    for (let i = 0; i < inPoints.length; i++) {
      const pre = this.getLastLocation(ret);
      const cur = inPoints[i];

      if (!pre || i === inPoints.length - 1) {
        ret.push(cur);
        continue;
      }

      const next = inPoints[i + 1];
      const distance = this.calculateDistanceFromPoint(cur, pre, next);
      if (distance < this.mNoiseThreshhold) {
        ret.push(cur);
      }
    }
    return ret;

}

/

• 获取最后一个位置点
/
private getLastLocation(points:RunLatLng[]):RunLatLng|null{
if(!points||points.length===0)return null;
return points[points.length-1];
}

/

• 计算点到线的垂直距离
/
private calculateDistanceFromPoint(p:RunLatLng,lineBegin:RunLatLng,lineEnd:RunLatLng):number{
const A=p.longitude-lineBegin.longitude;
const B=p.latitude-lineBegin.latitude;
const C=lineEnd.longitude-lineBegin.longitude;
const D=lineEnd.latitude-lineBegin.latitude;
const dot=A * C+B * D;
const len_sq=C * C+D * D;
const param=dot/len_sq;


    let xx: number, yy: number;
    if (param < 0 || (lineBegin.longitude === lineEnd.longitude && lineBegin.latitude === lineEnd.latitude)) {
      xx = lineBegin.longitude;
      yy = lineBegin.latitude;
    } else if (param > 1) {
      xx = lineEnd.longitude;
      yy = lineEnd.latitude;
    } else {
      xx = lineBegin.longitude + param * C;
      yy = lineBegin.latitude + param * D;
    }

    const point = new RunLatLng(yy, xx);
    return this.calculateLineDistance(p, point);

}

/

• 计算两点之间的距离
/
private calculateLineDistance(point1:RunLatLng,point2:RunLatLng):number{
const EARTH_RADIUS=6378137.0;
const lat1=this.rad(point1.latitude);
const lat2=this.rad(point2.latitude);
const a=lat1-lat2;
const b=this.rad(point1.longitude)-this.rad(point2.longitude);
const s=2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a/2),2)+
Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(b/2),2)));
return s * EARTH_RADIUS;
}

/

• 角度转弧度
/
private rad(d:number):number{
return d * Math.PI/180.0;
}

/

• 轨迹抽稀(同时处理源数据)

• @param inPoints 待抽稀的轨迹list

• @param sourcePoints 源数据list,与inPoints一一对应

• @param threshHold 阈值

• @returns 包含抽稀后的轨迹list和对应的源数据list
/
reducerVerticalThresholdWithSource(inPoints:RunLatLng[],sourcePoints:T[],threshHold:number=this.mThreshhold):PointSource{
if(!inPoints||!sourcePoints||inPoints.length<=2||inPoints.length!==sourcePoints.length){
return{points:inPoints||[],sources:sourcePoints||[]};
}


    const retPoints: RunLatLng[] = [];
    const retSources: T[] = [];

    for (let i = 0; i < inPoints.length; i++) {
      const pre = this.getLastLocation(retPoints);
      const cur = inPoints[i];

      if (!pre || i === inPoints.length - 1) {
        retPoints.push(cur);
        retSources.push(sourcePoints[i]);
        continue;
      }

      const next = inPoints[i + 1];
      const distance = this.calculateDistanceFromPoint(cur, pre, next);
      if (distance > threshHold) {
        retPoints.push(cur);
        retSources.push(sourcePoints[i]);
      }
    }

    return { points: retPoints, sources: retSources };

}
}

二、绘制运动速度轨迹

有了上述两个工具类后,我们就可以开始绘制运动速度轨迹了。以下是绘制轨迹的完整流程:

1.准备轨迹点数据

首先,将原始轨迹点数据转换为RunLatLng数组,以便后续处理:

typescript 复制代码
// 将轨迹点转换为 RunLatLng 数组进行优化
let tempTrackPoints = this.record!.points.map(point => new RunLatLng(point.latitude, point.longitude));

2.优化轨迹点

使用PathSmoothTool对轨迹点进行优化,包括去噪、滤波和抽稀,为保证源数据正确,我这里只做了抽稀:

typescript 复制代码
// 轨迹优化
const pathSmoothTool = new PathSmoothTool();
const optimizedPoints = pathSmoothTool.reducerVerticalThresholdWithSource<RunPoint>(tempTrackPoints, this.record!.points);

3.转换为地图显示格式

将优化后的轨迹点转换为地图所需的LatLng格式:

typescript 复制代码
// 将优化后的点转换为 LatLng 数组用于地图显示
this.trackPoints = optimizedPoints.points.map(point => new LatLng(point.latitude, point.longitude));

4.获取轨迹颜色数组

使用PathGradientTool根据速度为轨迹点生成颜色数组:

typescript 复制代码
// 获取轨迹颜色数组
const colors = PathGradientTool.getPathColors(optimizedPoints.sources, 100);

5.绘制轨迹线

将轨迹点和颜色数组传递给地图组件,绘制轨迹线:

typescript 复制代码
if (this.trackPoints.length > 0) {
  // 设置地图中心点为第一个点
  this.mapController.setMapCenter({
    lat: this.trackPoints[0].lat,
    lng: this.trackPoints[0].lng
  }, 15);

  // 创建轨迹线
  this.polyline = new Polyline({
    points: this.trackPoints,
    width: 5,
    join: SysEnum.LineJoinType.ROUND,
    cap: SysEnum.LineCapType.ROUND,
    isGradient: true,
    colorList: colors
  });

  // 将轨迹线添加到地图上
  this.mapController.addOverlay(this.polyline);
}

三、代码核心点梳理

1.轨迹颜色计算

PathGradientTool根据速度区间为轨迹点分配颜色。速度越快,颜色越接近青色;速度越慢,颜色越接近红色。颜色的渐变通过getGradient方法实现。

2.轨迹优化

PathSmoothTool通过卡尔曼滤波算法对轨迹点进行滤波,减少噪声和冗余点。轨迹抽稀通过垂直距离阈值实现,减少轨迹点数量,提高绘制性能。

3.地图绘制

使用百度地图组件(如Polyline)绘制轨迹线,并通过colorList实现颜色渐变效果。地图中心点设置为轨迹的起点,确保轨迹完整显示。

四、总结与展望

通过上述步骤,我们成功实现了运动速度轨迹的绘制。轨迹颜色反映了速度变化,优化后的轨迹更加平滑且性能更优。

相关推荐
Bruce_Liuxiaowei6 小时前
HarmonyOS NEXT~鸿蒙操作系统功耗优化特性深度解析
华为·harmonyos
SuperHeroWu76 小时前
【HarmonyOS 5】鸿蒙中的UIAbility详解(二)
华为·harmonyos·数据传递·uiability·启动模式·生命周期监听·监听设备环境信息
OBOO鸥柏商用液晶显示厂家8 小时前
OBOO鸥柏丨2025年鸿蒙生态+国产操作系统触摸屏查询一体机核心股
华为·harmonyos
实在智能RPA17 小时前
实在Agent成业界首批全面适配鸿蒙、麒麟、统信信创系统的智能体
人工智能·华为·harmonyos·agent智能体·实在agent
码农搬砖_202019 小时前
尝鲜纯血鸿蒙,华为国际版本暂时不支持升级。如mateX6 国际版?为什么不支持?什么时候支持?
华为·harmonyos
枫叶丹421 小时前
【HarmonyOS Next之旅】DevEco Studio使用指南(二十八) -> 开发云对象
华为·harmonyos·deveco studio·harmonyos next
鸿蒙布道师1 天前
HarmonyOS 5 应用开发导读:从入门到实践
android·ios·华为·harmonyos·鸿蒙系统·huawei
Python自动化办公社区1 天前
《全面解析鸿蒙相关概念:鸿蒙、开源鸿蒙、鸿蒙 Next 有何区别》
华为·开源·harmonyos
lqj_本人1 天前
鸿蒙OS&UniApp 制作倒计时与提醒功能#三方框架 #Uniapp
华为·uni-app·harmonyos
lqj_本人2 天前
鸿蒙OS&UniApp 实现自定义的侧边栏菜单组件#三方框架 #Uniapp
华为·uni-app·harmonyos