Garmin / Strava の走行記録をまとめて表示する

いままでに走ったことのある場所を一つの地図上にまとめて表示したい。自分と誰かの走行記録を一緒に表示したい。まだ走ったことのない場所を手早く見つけたい。

Garmin Edge といったサイクルコンピュータや Strava のような SNS を利用されている場合、今までのライドログを複数重ねて地図上にマッピングすることは簡単に実行できます。

必要な手順は大まかに3つぐらいです。

(1)座標データを集める
(2)座標データを Leaflet で描画する
(3)描画された地図(HTMLファイル)を表示する

一つづつ見ていきましょう。

(1)座標データを集める

Garmin デバイスの場合、スタートボタンを押して計測を開始してから保存を完了するまでのライド記録のすべてを FIT という形式のファイルに保存しています。

1つのファイルにライド1回分の情報があります。

この FIT ファイルは連携している Strava からもダウンロードすることが可能です。

この中には移動速度や気温やケイデンスなどのあらゆる計測データが記録されていますので、そこから座標データを抜き出して地図上に描画してやれば目的が達成されます。

ところが、ここで少し困ったことには FIT ファイルはバイナリファイルなので、テキストエディタなどで中身を覗こうとしても普通の人には読めません。


FITファイルの概要とFIT SDKのサンプルコード
https://qiita.com/harry0000/items/5931bbbf3d5b68fe8bc0


そこで一般的にはプログラムを使ってファイルから必要な情報を抜き出す処理を行います。




こうしたプログラムにはさまざまな種類がありまして、何を使ってもいいのですけれども、開発元からも公式にプログラムが配布されていますので、ひとまずはそれを使用することを考えます。


Downloads – THIS IS ANT
https://www.thisisant.com/developer/resources/downloads/


上のリンク先から FIT SDK という項目を選択すると Please Accept this Agreement Before Viewing This File という条件文が表示されますので、それを一読したあとに同意されるとファイルのダウンロードが行われます。

ダウンロードされた ZIP ファイルを解答すると java というディレクトリ(フォルダ)がありますので、その中にある FitCSVTool.jar というファイルが存在することを確認してください。

これは Java という言語で書かれたプログラムファイルです。プログラムを実行するには Java Runtime Environment という外部ソフトウェアが必要になります。

このプログラムファイルに正しく引数を与えて実行すると FIT ファイルがテキストファイルに変換されます。具体的にはこうやります。

$ java -jar FitCSVTool.jar -b activities/0000000000.fit outputFileName.csv

作成されたテキストファイルの中身には、以下のような記録が数十行から数千行に渡って並んでいます。

Data,12,record,timestamp,”933504317″,s,position_lat,”425645280″,semicircles,position_long,”1667343683″,semicircles,distance,”4978.48″,m,altitude,”56.60000000000002″,m,speed,”8.491″,m/s,unknown,”2710″,,unknown,”706″,,temperature,”27″,C,enhanced_altitude,”56.60000000000002″,m,enhanced_speed,”8.491″,m/s,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,

やるべきことは、ここから位置座標データを抜き取ることです。

目的の座標データは position_lat と position_long の直後に並んでいる数字列(太文字で強調している部分)です。

見た目は謎の数字列でしかありませんが、これを 2^31 (2の31乗)で割ってから 180 を掛けあわせると緯度と経度になります。

つまり、上の座標 (425645280,1667343683) とは 35°40’37.8″N 139°45’18.5″E のことです。

記憶容量の節約やプログラム処理の容易さなどの理由から32ビット整数で緯度経度を表現したら、こうなったのでしょうね。

対象となるファイルと目的が定まりましたら、対象の部分を抜き取って緯度経度に変換するという処理をスクリプトに落とし込み、作業をループで回して個々のファイルを処理していけば、必要な座標データは揃います。

一例をあげると、こんな感じに書けばやりたいことはできるかなと。

import com.garmin.fit.*;
import com.garmin.fit.csv.MesgCSVWriter;
import com.garmin.fit.csv.MesgFilter;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;

public class Converter {

	public void convert(String dirPath) {
		try {
			for (File file : listFiles(dirPath)) {
				String outputFileName = file.getName().substring(0, 8);
				
				System.out.println(outputFileName);
				
				Decode decode = new Decode();
				MesgCSVWriter mesgWriter = new MesgCSVWriter(outputFileName + ".csv");
				FileInputStream fileInputStream = new FileInputStream(file);

				MesgFilter mesgFilter = new MesgFilter();
				mesgFilter.addListener((MesgDefinitionListener) mesgWriter);
				mesgFilter.addListener((MesgListener) mesgWriter);

				decode.addListener((MesgDefinitionListener) mesgFilter);
				decode.addListener((MesgListener) mesgFilter);
	
				while (fileInputStream.available() > 0) {
					decode.read((InputStream) fileInputStream);
					decode.nextFile();
				}
				mesgWriter.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private List listFiles(String dirPath) throws IOException {
		return Files.walk(Paths.get(dirPath)).filter(Files::isRegularFile).map(Path::toFile)
				.collect(Collectors.toList());
	}

	public static void main(String args[]) {
		Converter Converter = new Converter();
		Converter.convert(args[0]);
	}
}

ただ、この段階で座標を変換しようとすると少し面倒だったので、出力される数値は生データのままです。

ここまで書かかなくても、わかる人はさっさと実行されていると思うのですけれども、反響(意味わかんねえよという苦情)が多ければ、ボタン操作で必要な情報を抜き出せるプログラムを公開するかもしれません。

作成すること自体は簡単なのですけれども、配布するためにはライセンスと利用規約を調べないといけないので、そこが面倒で時間がかかるのです。

座標データの準備ができましたら描画に移ります。

(2)座標データを Leaflet で描画する

データの準備ができましたら、走行経路を図形として地図上に描画します。

この目的で非常によく使われているライブラリに Leaflet.js というものがあります。Facebook でも Foursquare でも、どこでも使われているので、車輪の再発明を行うよりも使えるものは使ったほうがいいです。

ライブラリは CDN で配布されいるので HTML ファイルの最初にリンクを貼っておけば、ネットワークに接続されているコンピュータで利用できます。

使い方は単純なので公式のチュートリアルを見ながら自身で走行経路を描画されてもいいのですが、folium という Python のライブラリを使うと自動的に HTML ファイルとスクリプトを生成してくれるらしく、少し調べると先人の試行錯誤の記録がたくさん見つかります。

非常にありがたいことです。


Python + folium で Strava の "全"記録を地図で可視化 ? – hibomaの日記
https://hiboma.hatenadiary.jp/entry/2017/09/13/094056


Leaflet: Mapping Strava runs/polylines on Open Street Map · Mark Needham
https://markhneedham.com/blog/2017/04/29/leaflet-strava-polylines-osm/


ざっくりと見た感じでは、これを使うのが一番簡単そうに見えましたので、使ってみることにしますけれども、本音を述べると「なんだかなぁ」という気もします。

だって、普通の人のパソコンには Java も Python もインストールされてないじゃないですか(あと個人的にはあまり Python が好きではありません)。

取り敢えず、さきほど FIT ファイルから抜き出した座標データをそのまま使うのであれば、以下のように改編してやる必要があります。

#!/usr/bin/env python3
import os, sys, codecs, folium

def make(path, center):

    attributes='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
    my_map = folium.Map(center, zoom_start=9,attr=attributes)
    
    folium.TileLayer('openstreetmap').add_to(my_map)
    folium.TileLayer('stamenterrain').add_to(my_map)
    folium.LayerControl().add_to(my_map)
    
    filelist = os.listdir(path)
    files = [file for file in filelist if file[-4:].lower() == ".csv"]
    for file in files:
        locations = []
        fin = codecs.open(path + "/" + file, "r", "utf-8")
        lines = fin.readlines()
        for line in lines:
            list = line.replace('"', '').strip().split(",")
            if list[0] != "Data" or list[6] != "position_lat" or list[9] != "position_long":
                continue
            try:
                lat = float(list[7]) * 180 / 2**31
                lon = float(list[10]) * 180 / 2**31              
                if isinstance(lat,float) and isinstance(lon,float):
                    locations.append(tuple([lat,lon]))
            except ValueError:
                continue
        if len(locations) > 1:        
            folium.PolyLine(locations=locations,color="red").add_to(my_map)
    my_map.save("./myroutes.html")

if __name__ == "__main__":
    dir_path = sys.argv[1]
    # center = sys.argv[2]
    center = [35.01848, 135.7799]
    make(dir_path, center)

各変数の内容を変えるとズームレベルや中心の座標、タイルの種類、走行経路の表示色を変更できます。

実行環境はこんな感じです。

$ python -V
Python 3.6.4 :: Anaconda, Inc.
$ pip -V
pip 19.2.3 from /home/buran/anaconda3/lib/python3.6/site-packages/pip (python 3.6)
$ python
>>> folium.__version__
'0.10.0'

(3)描画された地図(HTMLファイル)を表示する

folium で HTML ファイルを生成した場合には自動的にスクリプトファイルも読み込まれますので、このステップは必要ありません。

それ以外の場合には leaflet.js で地図を表示する HTML ファイルを作成し、そこに座標ファイルから作成された走行経路(具体的には polyline )を表示するスクリプトをあわせて読み込まないとなりません。

おそらく、こんな形になると思います。

<!DOCTYPE html>
<html>
<head>
  <title>My Ride Logs</title>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=9.0">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.5.1/leaflet.css" crossorigin="" />
  <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.5.1/leaflet.js" crossorigin=""></script>
  <style>html, body {width: 100%;height: 100%;margin: 0;padding: 0;}</style>
  <style>#mapid {position:absolute;top:0;bottom:0;right:0;left:0;}</style>
  <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head>
<body>
  <div id="mapid"></div>
  <script src="./myridelogs.js"></script>
</body>
</html>

詳しくは公式のチュートリアルを参照してください。

これに走行経路を描画するスクリプトを読み込ます。

ここでは myridelogs.js という名前のファイルを読み込んでいるので、これを再利用されるのであれば同じ名前のファイルを用意して HTML ファイルと同じディレクトリの中に入れて下さい。

L.tileLayer(
  'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: 'Map data © OpenStreetMap contributors, ' +
      'CC-BY-SA, ' +
      'Imagery © Mapbox',
    id: 'mapbox.streets'
  }).addTo(mymap);

var polyLine089 = L.polyline(
  [
    [48.12477, 11.56289],
    [48.12180, 11.56864],
    [48.12163, 11.56827],
    // 無駄に長いので以下略
  ], {
    "bubblingMouseEvents": true,
    "color": "green",
    "dashArray": null,
    "dashOffset": null,
    "fill": false,
    "fillColor": "red",
    "fillOpacity": 0.2,
    "fillRule": "evenodd",
    "lineCap": "round",
    "lineJoin": "round",
    "noClip": false,
    "opacity": 1.0,
    "smoothFactor": 1.0,
    "stroke": true,
    "weight": 3
  }
).addTo(mymap);

ここまでで用意できた HTML をウェブブラウザで読み込むと、サイクリングやランニングの走行記録を地図上に表示することができます。

やろうと思えば、サイクリングは青、ランニングは赤、スイミングは緑などと色を変えて同一の地図上に一緒に表示することも可能です。

やってること自体は単純なので、すぐにでも実現できるのではないかと思われますけれども、プログラムを自分で書いて実行しないといけないので、その部分は少し大変かも知れません。


追記:サイコンの FIT ファイルから位置情報を読み込んで地図上に線を引くアプリをつくりました

アプリを使ってライド記録を地図上に描こう

さくらのVPSの障害でいろいろ泣かされた話

さくらインターネットのVPS(仮想専用サーバー)の連日の障害発生により、私が契約していた東京や大阪のサーバでも意図せぬ停止と再起動が行われました。

その結果、自作のサービスにアクセスできない旨のお叱りを受けて、早朝からサーバの再設定という生産性もなく、責任重大な業務に負われる事態に陥り、考えうる限り最悪の気分で一日の始まりを迎えました。

これがあるから、モバイルPCを手放せないんですよ。

消えた Nginx の設定ファイル

さて、ウェブブラウザを用いて問題のドメイン名にアクセスしてみると、ページを表示できませんというエラーメッセージが出てくるばかりです(pingが通っていたかどうかは確認しましたが、結果をメモするのを忘れました)。

This site can’t be hogefuga.piyo refused to connect.
Try:

Checking the connection
Checking the proxy and the firewall
ERR_CONNECTION_REFUSED

私はさくらの VPS 上でリバースプロキシ兼ロードバランサとして Nginx を設置していますので、エラーが表示される場合には(1)ドメインのエラー、(2) サーバのエラー、(3) Nginx のエラー、(4) アプリケーションのエラー、(5)データベースのエラーの5つの可能性を検討しなければなりません。

今回の障害が発生したサーバとは無関係なアプリケーションとデータベースは除外して、ドメインとサーバと Nginx の状態を一つづつ確認していきます。




NsLookup でドメインを確認するとドメインは正常。そもそも更新時期とも違うので念の為の確認です。サーバも SSH で正常にログインできる時点で問題なし。

システム管理デーモン (systemd) で Nginx の状態を確認すると、ここが起動できていません。OSの再起動時に自動的に立ち上がる設定になっているはずなのですが、動いていないものは仕方がないので、コマンドを実行して稼働させます。

ところが Nginx が正常に起動しません。確認してみると、設定ファイルが見当たりません。

仕方がないので新規に設定ファイルを作成して Nginx を起動させます。

ポートが開いていない問題

Nginx を起動させたら全ての問題が解決… と簡単にはいきません。

ドメイン名でアクセスしても、IPアドレスでアクセスしても依然としてウェブブラウザでウェブサイトを表示できない状態に変化はありません。

Nginx の設定で 80 番ポートのアクセスを 443 番ポートにリダイレクトしているので、開いてるポート番号を確認すると、なんと Linux ファイアウォール (iptables) 側の設定でポートが閉じられているという事態が発覚しました。

自動再起動がほとんど機能していないですね。

画面が真っ白(ブランクページ)になるやつ

ポートを開放してファイアウォールデーモンをリロードすると、ようやくドメイン名でアクセスすることが可能になりました。

ログイン認証にも無事に成功しましたので、データベース接続にも異常はありません。アプリケーションやデータベースには問題は無さそうです。

と思っていたところ、一部のページがブランクになって何も表示されません。

ブラウザの Developer Tools でソースコードを参照すると、どうやらヘッダだけは読み込んでいる様子。さらに調べていると、次のようなエラーメッセージを表示させることができました。

ERR_CONTENT_LENGTH_MISMATCH 200 (OK)

こういうのは基本的には Nginx のエラーメッセージなので、設定ファイルに以下の1行を追加して Nginx をリロードします。

server {
	proxy_buffering	off;
}

ここまで来て、ようやく正常にウェブサイトにアクセスできるようになりました。

さいわいにもアプリケーションとデータベースには障害が出ていなかったので、今回はこれだけで済みましたけれども、Nginx そのものを冗長化することも真剣に考えなければならない気がしてきました。


[24時間365日] サーバ/インフラを支える技術 ‾スケーラビリティ、ハイパフォーマンス、省力運用 (WEB+DB PRESS plusシリーズ)

ツーリング用モバイル PC の最適解か – 約 500g の 7インチ ノート PC

日常的に出張を繰り返す私にとって、モバイルPCは無くてはならない存在です。

今までにも Chromebook を Linux マシンとして利用したときの使用感や、仮想環境上にて実行できる高性能なラップトップ PC の購入について述べてきました。

通常の出張においてであれば、こうした既存の PC を持ち運ぶことに何ら不満はないのですけれども、「自転車旅でも PC を持ち運びたい」と考えたとき、さらに小型で軽量な PC への欲求が湧いてきます。

Tabキーで入力補完のできるキーボードが付属していて、ローカルで Shell scripts と Python さえ動く端末があれば、移動中の時間も無駄にすることなくデータの処理が行なえますし、VPN と SSH と Git まで用意できれば、それこそサーバ経由であらゆることが実行できます。

自転車で遠くまで遊びに行ったときの隙間時間(フェリーや飛行機で移動する場合では数時間から数日間)を活用することで、もっと自由に遊べる時間を増やせるわけです。

そんな都合が良いものが無いかと年単位で探し続けていたら、先日、ついに見つけてしまいました。


ONE-NETBOOK Technology OneMix2s(専用デジタルスタイラスペン/WPS Office スタンダード版付属)シルバー[Core m3-8100Y /メモリ 8GB/ PCIe SSD 256GB] ONEMIX2SJ-S2

キーボードと一体型の筐体に、振動で故障しにくい SSD ストレージ、本体質量 515g の重さに 6,500mAh のバッテリー容量という素晴らしいスペックながら、182 x 110 x 17 mm という手のひらサイズの体積の「ノートパソコン」です。

この大きさでタブレット端末ではなくて PC というのが最高に素晴らしいです。

軽量なタブレット端末に Bluetooth キーボードを接続する方法も、もちろん試したことはあるのですが、ファイルアクセスが面倒であったり、レスポンスが悪かったりと、ストレス無くスクリプトを書いて走らせることは困難です。




そうは言っても、普通のノート PC は薄型であっても自転車で持ち運ぶには大き過ぎますし、専用の充電器も荷物になります。また軽量モデルほど高価になる傾向がありますので、廉価モデルには持ち運びに適したものが少ないです。

Chromebook は USB 充電できることと低価格が魅力ですが、本体はあまり小型ではありません。作業用マシンとしての使い勝手もそれなりです。

タブレットは使えなくはありませんけれども、ローカルで実行できることが限定されますので、長時間の作業を続ける環境としては、いろいろと辛いと思います。

作業性 USB充電 質量 価格
ノートPC 700g – 9万円 –
Chromebook 900g – 2.5万円 –
タブレット 230g – 2万円 –
7インチPC 500g – 6万円 –

ところが、7インチPCは携帯性と作業性を兼ね備えていて、PCとしてのスペック的にも全く妥協していません。

その分、本体価格も高価なので、ツーリングに向いていない点があるとしたら(雨に濡らしたり、道路に落下させたりする可能性が高い場面で用いるPCとしては)価格が高過ぎることでしょうか。

ストレージ容量を考えるとパーティションを分割して、デュアルブートにしても良さそうだなと考えると、ますます欲しくなってきます。

仮想マシンを動かすには RAM が心許ないですが、256GB のストレージ容量は2分割しても数年前のノート PC よりも余裕がありますので、確認作業やファイル閲覧用に Windows を残しておけるのも理想的です。

購入すべきかどうかを迷っているのは、ただ一点、遊びに出かけていないときにどれだけ使用機会があるかどうかということに尽きます。

小型化のためにポインティング・スティックと特殊なキーボード配列を搭載した筐体は、通常の用途では使いにくいかもしれません。

キーボードしか使わない私のような原理主義者は OS ごと Debian で上書きすれば良いと考え始めるので、あまり問題にならないと思われるのですが、トラックパッドの方が何かと使いやすいのも違いありません。

現状ではおよそ 1kg の旧いノートパソコンに SD カードから Linux をブートして使用しています。

旧いノートパソコンとは言え、数年前のフラッグシップモデルだった PC だけに、キーボードも使いやすく、スクリーンも広くて、動作速度も快適です。

SSDストレージ容量は時代なりですし、メーカーの補修部品の在庫もそろそろ怪しいところですが、単体での作業性で見たらこちらの方が使いやすいことは明白です(ストレージ容量と稼働時間は、さすがに厳しいですけど)。

使わなくなった時点で郵送してしまえば、携帯性は考えなくても良いですし、ある意味では既に役目を終えた PC の再利用なので、事故による故障や紛失も大きな問題にはなりません。

遊びに出かけている間に使用することはないので、長い移動時間の最中にだけ手元にあれば良いことを考えると、これでも良いかなという気になってきます。

郵送が頼りにできないような場面が出てきたら、その時こそ小型モバイルPCの買い時なのかも知れません。

何れにせよ、このスペックと携行性があれば利用用途はいくらでもありますし、モニターとキーボードを外付けすれば操作性も格段に向上しますので、メイン機として考えても良いななどと思っていると夢が無限に膨らみます。