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

前言

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

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

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

1.轨迹颜色工具类:PathGradientTool

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

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的核心逻辑:

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数组,以便后续处理:

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

2.优化轨迹点

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

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

3.转换为地图显示格式

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

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

4.获取轨迹颜色数组

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

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

5.绘制轨迹线

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

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实现颜色渐变效果。地图中心点设置为轨迹的起点,确保轨迹完整显示。

四、总结与展望

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

全部评论

相关推荐

三三叁:线下面试感觉不太可能,大厂效率第一位,不可能面试影响工作timeline,资本家们不可能浪费一点时间吸血。而且大厂我估计现在也不是很在意ai辅助面试,就算招的人不行,裁了呗,牛马有的是
点赞 评论 收藏
分享
点赞 评论 收藏
分享
各位好,我是客户端劝退第二人。曾经,我也是一名客户端开发,如今则是一名全职滴滴快送骑手,穿梭在这座城市的街头巷尾,靠双手和车轮谋生。看到被暴力裁员的一鸣,勾起了我无数痛苦回忆。沉寂了半年多,我一直在修复自己残破的生活、麻木的心灵和早已偏离航向的人生。如今终于能稍微平静一些,讲讲我自己的故事。2024年7月25日,我被挖到开水团,入职了海外SaaS业务组。在接下来的几个月里,和其他同事不一样,我的主管马某从未与我沟通过试用期目标,似乎我天生就是一届炮灰。前几个月我一直按部就班地完成自己手头的工作,需要加班就默不作声地加班。在这个“以嫡系为中心,长期且有耐心”的工作环境中,我不再是《摩登时代》里的卓别林,而是一条日夜不停的流水线。那时候的我觉得,能忍,还不至于滚的程度。真正的转折点发生在一位从Tiktok跳槽过来的资深研发同事,试用期没通过,被光速离职。这时,压力就落在了我身上。我成为了继任者,作为小组里唯一的客户端开发,一面要完成新的需求开发,一面要解决前人光速离职留下的问题。在有限的时间内,一遍又一遍地让我逆向分析Toast的源代码,写方案、出文档、开发,同时还要遭受质疑、否定、打压和不认可。那是我人生最黑暗、也最酗酒的一段日子。我几乎走遍了杨浦大学城附近所有能叫得上名号的酒吧,试图靠酒精麻痹自己,去抵御职场里的苟且和内心的绝望。在试用期的第六个月,我的主管马某第一次找我谈话,直接表示试用期不会让我通过了,也没说明原因,只是说“不符合预期”,希望我在三天内提离职走。我提出疑问:“没有缓冲期吗?没有赔偿吗?”对方表示越快越好,对我、对团队都是好事,也不存在赔偿这一说。对方同时表示,如果我觉得自己行,也可以在这几天内争取转岗。期间我试图与我们的HRBP杨某沟通。杨某表示,主管马某没有跟我沟通试用期目标,确实是他的问题,但我也没有主动去约他沟通,这是我的主动性不足,需要改进。其次,职级的能力范畴不能只看内部能力模型,要根据实际情况来定。我的主管马某之所以让我承担更高级的工作,是因为对我有所期待,而我也要承担这份期待落空的代价。后来我表示,主动离职需要一个明确的理由,公司应就我是否造成资产损失以及所有相关问题以书面形式通知我,并按照劳动法规章制度处理。但后续进程就是一直没有给我类似文件,只是说我可以去争取转岗。2025年1月14日,我通过内部系统约到一位北京Base的部门主管,约好第二天上午在办公楼一楼会议室面试。面试结束后,杨某问我在哪里、在做什么,我如实告知正在一楼面试间转岗面试。对方立即表示我这是“擅离职守”,因为面试前没有向主管马某报备。2025年1月15日下午,HRBP杨某给我打电话沟通。第一,要么立刻提离职;第二,将单方解除我的劳动合同,并在背景调查中如实告知有劳动纠纷和争议。1月16日下午,我被叫进小会议室,当场收到《违纪行为处理决定》《试用期解除劳动合同通知书》。我明确表示不认同“擅离职守”这一指控,但他们根本不给申辩的机会,收走了设备、权限,草草了事。仲裁时他们更是颠倒黑白,伪造打卡记录证明我某天“擅离职守”,记录里我从三楼出门又从一楼出门,期间消失了两个多小时,时间线漏洞百出。更荒唐的是,公司死咬着我参加的转岗面试不是“同一家公司”的面试,因为该部门属“不同主体”。可面试是通过内部系统预约,地点是公司办公楼,流程也是内部流程,就因为主体不同,我败诉了。我没有像爽文男主那样逆袭。尽管我带着尽可能全的证据去仲裁,依然输得干干净净,甚至还被HRBP和主管践行了“秋后算账”的承诺。之后我几乎每一次背调,都能听到他们在背后释放的负面言论,直接让我在互联网行业彻底被封杀。如今,我依然在一审上诉的路上,看透了职场的人性丑恶、尔虞我诈和利己至上。走出办公室,我选择做一名快送骑手,每天骑车奔波在大街小巷,感受久违的阳光和新鲜空气。我不再迷茫,也不指望奇迹,只想把握当下,凭本事好好活着。这,就是我当下的真实人生。其实到最后,这件事我图的根本就不是赔偿金。那些钱早就不重要了。为了这场官司,我付出的时间、精力、金钱早已远远超过他们本该赔给我的数额。有人笑我不值,有人劝我算了,可我从来不觉得这有什么好笑或者后悔。我要的,是以后别再有哪个兄弟姐妹,在职场里碰上这帮吃人不吐骨头的傻逼领导,明面一套、背后一套,试用期不定目标、不沟通,半年后一句“你不符合预期”就让你滚蛋。我要的,是别再有哪个HR装聋作哑、和稀泥,面对职场PUA、阴阳怪气、打压挤兑的时候,动不动一句“是你主动性不够”“是你自己没去约主管”,站在裁人那一方,跟刽子手一个德性。我要的,是这操蛋的职场里少一点这些傻逼,多一点公道,哪怕只是一点点。
Java抽象小篮子:因为自己淋过雨,所以也不想别人淋雨,太伟大了劝退侠
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务