🐡

HarmonyOS運動開発:運動速度トラックの描画方法

に公開

前言

屋外運動アプリでは、運動速度トラックを描画することで、ユーザーの運動ルートを直感的に表示するだけでなく、色の変化で速度の変化を反映し、ユーザーが自分の運動状態をよりよく理解できるようにすることができます。しかし、鴻蒙システムでこの機能を実現するにはどうすればよいでしょうか。この記事では、実際の開発経験を基に、データ処理からマップの描画までの全プロセスを深く解析し、運動速度トラックの描画方法をステップバイステップで学んでいただきます。

Image description
上記の画像のリンクは、ネットワークの問題のために解析に失敗しました。この問題は、リンク自体の合法性やネットワーク状況に関連する可能性があります。ウェブページのリンクが正しいことを確認し、適宜再度お試しください。

一、コアツール:トラックの色と最適化

運動速度トラックを描画するための鍵は、2つのツールクラスにあります:PathGradientToolPathSmoothTool。この2つのツールクラスは、それぞれトラックの色を処理し、トラックのなめらかさを最適化するために使用されます。

  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;

    // 1回目のループ:速度データの収集
    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';
    }
  }
}
  1. トラックの最適化ツールクラス:PathSmoothTool

PathSmoothToolの役割は、トラックのなめらかさを最適化し、トラックポイントのノイズと冗長性を減らすことです。以下がPathSmoothToolのコアロジックです:

export class PathSmoothTool {
  private mIntensity: number = 3;
  private mThreshhold: number = 0.01;
  private mNoiseThreshhold: number = 10;

  /**
   * トラックの平滑化と最適化
   * @param originlist 元のトラックリスト、list.sizeは2より大きい
   * @returns 最適化後のトラックリスト
   */
  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.sizeは2より大きい
   * @returns フィルタリング処理後のトラックリスト
   */
  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 抽象化されるトラックリスト

  • @param threshHold 閾値

  • @returns 抽象化されたトラックリスト
  /
  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 元のトラックリスト

  • @returns ノイズ除去されたトラックリスト
  /
  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);
  }

  2つの点間の距離を計算する
  /
  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 抽象化されるトラックリスト

  • @param sourcePoints ソースデータリストで、inPointsと11に対応する

  • @param threshHold 閾値

  • @returns 抽象化されたトラックリストと対応するソースデータリストを含む
  /
  reducerVerticalThresholdWithSource<T>(inPoints: RunLatLng[], sourcePoints: T[], threshHold: number = this.mThreshhold): PointSource<T> {
    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 };
  }
}

二、運動速度トラックの描画

上記の2つのツールクラスを使用して、運動速度トラックの描画を開始することができます。以下がトラックを描画する完全なプロセスです:

  1. トラックポイントデータの準備

まず、元のトラックポイントデータをRunLatLng配列に変換し、後続の処理に備えます:

// トラックポイントを RunLatLng 配列に変換して最適化する
let tempTrackPoints = this.record!.points.map(point => new RunLatLng(point.latitude, point.longitude));
  1. トラックポイントの最適化

PathSmoothToolを使用してトラックポイントを最適化し、ノイズを除去し、フィルタリングし、抽象化します。ソースデータの正確性を保証するため、ここでは抽象化のみを行います:

// トラックの最適化
const pathSmoothTool = new PathSmoothTool();
const optimizedPoints = pathSmoothTool.reducerVerticalThresholdWithSource<RunPoint>(tempTrackPoints, this.record!.points);
  1. トラックポイントをマップ表示形式に変換する

最適化されたトラックポイントをマップに必要なLatLng形式に変換します:

// 最適化されたポイントを LatLng 配列に変換してマップに表示する
this.trackPoints = optimizedPoints.points.map(point => new LatLng(point.latitude, point.longitude));
  1. トラックの色付け配列を取得する

PathGradientToolを使用して、速度に応じてトラックポイントに色付け配列を生成します:

// トラックの色付け配列を取得する
const colors = PathGradientTool.getPathColors(optimizedPoints.sources, 100);
  1. トラックラインの描画

トラックポイントと色付け配列をマップコンポーネントに渡し、トラックラインを描画します:

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メソッドによって実現されます。

  1. トラックの最適化

PathSmoothToolはカルマンフィルターを使用してトラックポイントをフィルタリングし、ノイズと冗長なポイントを減らします。トラックの抽象化は垂直距離の閾値によって実現され、トラックポイントの数を減らし、描画のパフォーマンスを向上させます。

  1. トラックの描画

Baiduマップコンポーネント(例えばPolyline)を使用してトラックラインを描画し、colorListを使用してグラデーション効果を実現します。マップの中心点はトラックの最初のポイントに設定され、トラックが完全に表示されるように保証されます。

四、まとめと展望

上記の手順を経て、運動速度トラックの描画を成功裏に実現しました。トラックの色は速度の変化を反映し、最適化されたトラックはよりなめらかで、パフォーマンスが優れています。

Discussion