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

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • MyrinNew
    Senior Member
    • Feb 2024
    • 5175

    #1

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

    前言


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




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


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


    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(co lor));
    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的作用是优化轨迹的平滑度,减少轨迹点的噪声和冗余。以下是PathSmoo thTool的核心逻辑:






    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.length2)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.length2)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:R unLatLng,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:RunL atLng):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:RunLat Lng[],sourcePoints:T[],threshHold:number=this.mThreshhold):PointSource{
    if(!inPoints||!sourcePoints||inPoints.length2||inP oints.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.reducerVerticalThresholdWithSourceR unPoint>(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.sou rces, 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实现颜色渐变效果。地图中心 点设置为轨迹的起点,确保轨迹完整显示。


    四、总结与展望


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




    More...
Working...