当前位置: 首页 > news >正文

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

四、总结与展望

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

http://www.xdnf.cn/news/665353.html

相关文章:

  • day 22 练习——泰坦尼克号幸存者预测
  • Dify中的GoogleSearch工具插件开发例子
  • 华为OD机试真题——新工号中数字的最短长度(2025A卷:100分)Java/python/JavaScript/C/C++/GO最佳实现
  • 【AI论文】LLaDA-V:具备视觉指令微调能力的大型语言扩散模型
  • 基于 LoRA 和 GRPO 的 Qwen2.5-3B 数学推理模型微调示例
  • java学习日志——Spring Security介绍
  • 二维坐标变换、三维坐标变换、综合变换
  • 人工智能工程师学习路线总结(上)
  • MySQL的日志和备份
  • 热点数据的统计到应用
  • C 语言学习笔记二
  • 202505系分论文《论模型驱动分析方法及应用》
  • FallbackHome的启动流程(android11)
  • 泪滴攻击详解
  • MDM在智能健身设备管理中的技术应用分析
  • 计算机系统简介(二)
  • python打卡day36@浙大疏锦行
  • C++ STL Queue容器使用详解
  • SPL 轻量级多源混算实践 1 - 在 RDB 上跑 SQL
  • vue3 浮点数计算
  • 码蹄集——矩形相交
  • 【大模型】分词(Tokenization)
  • unix的定时任务和quartz和spring schedule的cron表达式区别
  • C# 中 INI 文件操作扩展类:轻松管理配置文件
  • 开发一个交易所大概需要多少成本
  • 调试的按钮
  • 2.1 一文掌握 TypeScript 操作符
  • 配置Maven环境(全局)
  • 【辰辉创聚生物】JAK-STAT信号通路相关蛋白:细胞信号传导的核心枢纽
  • 【C++高级主题】异常处理(四):auto_ptr类