🍜 Ride with GPSからダウンロードした .gpx を読み込めるようにJSで変換する ④
作成日: 2021/10/02
0

読み飛ばして良いまえがき
ユーザー編集用input作成編 → https://ticketnote.dev/ticket/q1zKjfMRReQpbml4tyGy
完成予定のCodePen → https://codepen.io/kaitou1192/pen/gORyBGX

①では、ローカルファイルの内容を文字列で読み込み、②では、読み込んだ文字列をDOMに変換したのち、書き出しのためにDOMを文字列に再変換、文字列→Blob→ファイル出力というのをやりました。
今回は③に引き続きOregon 450で読み込める内容に変換する(つまり①と②の間の処理)を進めます。


本日の本題

今回は早速本題に入ります。

ユーザー側で設定できる範囲を.gpxファイルに反映させる箇所を追加します。
この時点でのCodePen → https://codepen.io/kaitou1192/pen/dyRLqOE

//ファイルを出力する際の処理
const generateNewFile = (event, temporaryGpx) => {
  //前処理
  const baseGpx = temporaryGpx.documentElement;
  const baseTrk = baseGpx.querySelector('trk');
  const baseTrkName = baseTrk.querySelector('name');
  const baseTrkseg = baseTrk.querySelector('trkseg');
  const baseTrkpt = baseTrkseg.querySelectorAll('trkpt');
  
  //ポイントの間引き
  if(baseTrkpt.length != inputPoints.value) {
    const temporaryTrkseg = document.createElement('trkseg');
    const thinningParameter = Math.ceil(baseTrkpt.length / inputPoints.value);
    baseTrkpt.forEach((element, index) => {
      const addElement = (element) => {
        const temporaryElevation = element.querySelector('ele');
        temporaryElevation.textContent = Math.round(temporaryElevation.textContent * 100) / 100;
        temporaryTrkseg.appendChild(element);
      }

      if(index % thinningParameter === 0) {
        addElement(element);
      } else if(index === baseTrkpt.length - 1) {
        addElement(element);
      }
    });
    
    //ポイントの入れ替え
    baseTrkseg.remove();
    baseTrk.appendChild(temporaryTrkseg);
  }
  
  //トラックネームの設定
  baseTrkName.textContent = inputTruckName.value;
  
  //出力用GPX用意
  const outputGpx = new XMLSerializer();
  let outputGpxText = outputGpx.serializeToString(temporaryGpx);

  const blob = new Blob([outputGpxText], {type: 'application/xml'});

  //ダウンロード用のエリア内に要素がある場合は削除
  while(download.firstElementChild) {
    download.firstElementChild.remove();
  }

  //ファイルをダウンロードさせる
  const a = document.createElement('a');
  a.href = window.URL.createObjectURL(blob);
  a.textContent = 'ダウンロード';
  a.download = inputFileName.value + '.gpx';

  download.appendChild(a);

  //ダウンロード用のエリアを表示
  download.setAttribute('style','display: block;');
}

前処理がgenerateNewFile側にも入っているのは、別の関数の範囲になってしまったためloadedFile内のconstが読み込めないので、あらためて設定し直してあります。

  //前処理
  const baseGpx = temporaryGpx.documentElement;
  const baseTrk = baseGpx.querySelector('trk');
  const baseTrkName = baseTrk.querySelector('name');
  const baseTrkseg = baseTrk.querySelector('trkseg');
  const baseTrkpt = baseTrkseg.querySelectorAll('trkpt');

ポイントの間引きは baseTrkpt.length と inputPoints.value が異なる際に発動します。
thinningParameterは、何ポイントおきにポイントを採用するか?の間隔を出しています。その何個おきを「index % thinningParameter === 0」で確認し、採用するポイントの場合は addElement 経由の temporaryTrkseg.appendChild で貯めています。
「index === baseTrkpt.length - 1」の箇所は、最終のポイントが間引かれてしまうと微妙に精度が低く感じられるかと思い、最終ポイントを採用するという処理です。
temporaryElevation.textContent に100を掛けて四捨五入し、100で割っているのは 11.1999999 みたいな数字を丸めるためです。
最後に元々のbaseTrksegを削除し、代わりにtemporaryTrksegを追加ということで入れ替えました。

  //ポイントの間引き
  if(baseTrkpt.length != inputPoints.value) {
    const temporaryTrkseg = document.createElement('trkseg');
    const thinningParameter = Math.ceil(baseTrkpt.length / inputPoints.value);
    baseTrkpt.forEach((element, index) => {
      const addElement = (element) => {
        const temporaryElevation = element.querySelector('ele');
        temporaryElevation.textContent = Math.round(temporaryElevation.textContent * 100) / 100;
        temporaryTrkseg.appendChild(element);
      }

      if(index % thinningParameter === 0) {
        addElement(element);
      } else if(index === baseTrkpt.length - 1) {
        addElement(element);
      }
    });
    
    //ポイントの入れ替え
    baseTrkseg.remove();
    baseTrk.appendChild(temporaryTrkseg);
  }

トラックネームの設定はシンプルでこんな感じ。

  //トラックネームの設定
  baseTrkName.textContent = inputTruckName.value;

ファイル名の設定もこんな感じです。

  a.download = inputFileName.value + '.gpx';

さて…… これで、ユーザーに入力してもらった情報は反映できるのでXMLを出力して中身を見てみましょう。
スクリーンショット 2021-10-02 23.06.22.png

肝心の空白を取る処理を入れていないので、インデントがついたままになっています。また trkseg や trkpt にxmlns属性がついてしまっています。
これはいただけないのですが…… ここは上手い処理が思いつかないので、DOM→文字列にした段階で不要な文字列を正規表現で取ってしまいます。
(上手いやり方をご存じの方、ぜひ教えてください。)

outputGpxTextに対してtrkptのxmlnsを削除、trksegのxmlnsを削除、2つ以上連続している半角スペースを削除、改行を削除という処理をしました。
この時点でのCodePen → https://codepen.io/kaitou1192/pen/gORyBGX

  //出力用GPX用意
  const outputGpx = new XMLSerializer();
  let outputGpxText = outputGpx.serializeToString(temporaryGpx);
  outputGpxText = outputGpxText.replace(/<trkpt xmlns=\"http:\/\/www.topografix.com\/GPX\/1\/1\" lat=\"/g, '<trkpt lat="');
  outputGpxText = outputGpxText.replace(/<trkseg xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">/g, '<trkseg>');
  outputGpxText = outputGpxText.replace(/ {2,}/g, '');
  outputGpxText = outputGpxText.replace(/\r?\n/g, '');

出力したファイルを確認してみるとこんな感じです。
スクリーンショット 2021-10-02 23.12.08.png

おー、素晴らしい。
やっと読み込めるファイルが出力できました。 🎉🎉🎉

このコード自体は、真面目に作るとすると今まで注意書きとして書いたバリデーションの問題etc.があります。また、ポイントの間引きも機械的に行っているだけなのですが、真面目にやろうとする場合は、極近接しているポイントを優先的にまとめるであったり、ほぼ直線上に並んでいる点を優先的に間引いたりというロジックが考えられると思いますが、現時点ではプライベートな日曜エンジニアリングということで機械的に間引いています。

もしご質問や、改善のアイデアetc.ありましたら、コメント欄に記入していただけると助かります。




Ride with GPSからダウンロードした .gpx を読み込めるようにJSで変換する



Twitterもやっているので、よかったらフォローしてください。🙏🏻
https://twitter.com/Kaitou1192

本業はコードを書かせてもらえないフロントエンドエンジニアです。 こんなサービス作っています。 https://lp.re-shine.jp