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 ファイルから位置情報を読み込んで地図上に線を引くアプリをつくりました

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

ロングライド・イベントの魅力

スポーツ自転車を購入したばかりの頃は、ライドイベントなど自分には縁がないものだと思っていました。

どうして参加費と交通費を支払って 160km も自転車で走行するのだろう。

率直に言って「意味がわからない」とさえ思いました。

そう考えている人こそ、じつはライドイベントに参加することで得られものが大きいです。

なにしろ、それまでの自分の価値観とは異なる世界に赴いて、いままでに経験したことの無い新しいことをするわけです。

自分の経験から言って、ロードバイクを購入して1年ぐらいで、いきなり東京から瀬戸内海に飛行機輪行してイベントに参加したことは正解だったと感じます。

誰も知り合いのいないところに一人で参加することには抵抗を覚えるかもしれませんが、友人と一緒に参加しても 50-200km も走行しているうちに、自分の走力にあった集団に自然と別れるので、一人で参加しても全く問題ありません

むしろ、趣味仲間が増えやすくて、美味しいです。

また、とくに車種制限のない舗装路のイベントであれば、リカンベント、ミニベロ、クロスバイクで参加されていても誰も気にしません

もちろん、ロードバイクの方が完走は楽なので数としては多いですが、どのイベントでも一定数はミニベロやクロスバイクの参加者を見かけます。

そして、イベント前日からゼッケンを付けていると、ほかのイベント参加者や地元の人から話し掛けられやすくなるので、有意義な情報をたくさん得ることができます。




コースそのものもファンライドの魅力です。

地域振興の性格をもったロングライドの場合、通常時には自転車通行できない自動車専用道や広域農道などを特別に使用できる場合があり、そうした場所を自転車で走れる唯一の機会がイベントだったりします。

コース上には一定距離ごとにエイドステーションが臨時設置され、いつもなら飲料水の確保にも困るような山間地でも、万全の体制で向かうことが可能です。

コースになるのも走りやすくて、交通量が少ないところが多いので、経験上、しまなみ海道のような有名観光地よりも、普通の人は誰も知らないような開催地のほうが得られるもの(隠れた絶景スポットや穴場情報)が大きい気がします。

完走すれば記念品も頂けて、非日常的な達成感も得られますし、仮に完走できなくてもサポートカーに救護してもらえるので安心です。

一度、思い切って参加されてみると、ライドイベントの魅力がお分かりになるかと思います。

その一方で、ライドイベントに継続的に参加されていた方が、イベントから離れていくことも珍しくありません。

私も、ここ2年ぐらいは何のイベントにも参加していませんので、参加を辞めた方の気持ちも良く分かります。

イベントから離れた理由は、いくつもあるのですけれども、大別すると走力、天候、コースの3つの要素が大きいです。

50km から 160km ぐらいまで、幾つかコースが設定されているロングライドでは、参加者も数百人から千人以上になります。

これだけの参加者がいると、自転車だけでも道路が混雑して落車に巻き込まれる可能性が高まります

ダウンヒル中にボトルを落とす人がいたり、目の前で接触事故が発生したりと、イベントならではの注意点というのも無いわけではありません。

また、何年も走り込んでいくうちに自然と走力も向上し、大多数の他の参加者との走行速度の違いも意識させられるようになります。

私だって始めたばかりの頃は斜度 9% の坂が崖に見えましたし、それを普通に自転車で登れているほうがおかしいと思いました。

それが年間 6,000km 以上もロードバイクで走り込んでいくと、平坦な道路が退屈に思えてくるので不思議なものです。

こうなってくると、イベント参加者の多くとは走行速度が変わってきますので、先頭近くでスタートできた場合には、最初から最後まで一人で走ることになる場合もあります。




二点目の天候ですが、ライドイベントの多くは雨天中止です。

地元のイベントであれば、週末は他のことに費やせば良いだけですが、イベントを目的に沖縄や長野や北海道などを訪れていると、当日になって中止される事態にやりきれない気持ちを覚えます。

イベント開催には道路使用許可などの入念な準備が必要とされますので、当日の天候が悪ければ、次回の開催は一年後を待たなければならないことも少なくありません。

何度かイベントに参加しているうちに、天候が自由にならないのであれば、コースや日時のほうを調整して対応すれば良いのではないかと思い始めます。

こうなると徐々にイベントに参加するという意識が希薄になっていきます。

三点目のコースについては、毎年、ほぼ同じコースを同じ時期に走るので、一度、満足してしまうと、次からは決してコースにはならない周辺の峠道や行き止まりの岬などが気になりだします。

繰り返し同じコースを走ることで、自分の成長を感じることはできますが、ファンライドはレースではありませんので、速く走れることが良いというわけでもありません。

走り込んで自転車に慣れると、エイドステーションの数も4分の1(80-120km に1箇所)ぐらいで十分に思われてきますので、たくさんの参加者と一緒に走る必要もないかなという気もしてきます。

というのも、日頃から東京都内のような劣悪な環境にいると、走りやすいコースを 100km ぐらい走行した程度では全く疲れません。

そもそも、日本の人口の3分の1以上を占める関東在住者にとっては、自宅から「まとも」なサイクリングスポットに行って帰ってくるだけで、ロングライドを完走する以上の距離を走らされることが普通です。

新宿から奥多摩湖まで最短で片道 70km もあります。

しかも道路は狭く、信号、路上駐車、左折車の影響で渋滞していることが当たり前。休日は東京を出発する始発列車まで登山客で混雑して輪行スペースなど存在しないことさえあります。

こんな場所でイベントと同じペースで休憩していては、最寄りの山や海岸の入り口に行って帰ってくるだけで日が暮れてしまいます。

それなら、せめてイベントの1日くらいは、安全で快適な道路を通りたいと願ってしまいます。

当然ながら、これは何年も走り込んでいてエイドステーションも見慣れているからこその意見であり、イベントに参加し始めた頃は 20-30km おきに休憩地点が確保されていることが非常にありがたかった記憶があります。

エイドステーションがたくさんあると、それだけ他の参加者とも話す機会がありますので、機材や練習場所についての貴重な意見を伺える機会も増えます。

参加人数が多いことも、レースのような多人数での走行に対しての貴重な練習機会になりますし、距離や獲得標高の数字から具体的な走行時間や疲労感をイメージできるようになるまでは、コースのルートから得られるものも多々あります。

なにより、自分のまったく知らない地域のイベントであれば、その地域の定番コースを手軽かつ安全に体験できて、現地の知り合いまでできます。

「もう来年はいいかな」と思えても、参加してみるとやっぱり楽しいものです。

キャリパーブレーキはなくなる気がするという話

数年前にディスクブレーキ仕様のロードバイクの購入を検討して、あれこれと考えたことをまとめたことがあります。

その当時のロードバイクの価値観では「軽量化」が大正義であり、「転がり抵抗」や「エアロダイナミクス」なども今ほど一般的に語られる機会は多くありませんでした。

また、より重要な背景として、ディスクブレーキ搭載のロードバイク自体が実験的な製品の扱いであり、その特性が活きてくるような商品展開もなされていなかったことが挙げられます。

したがって、雨天走行を日常的に行うようなライダーを除いては、わざわざ少数派の(アップグレードパーツの選択肢の少ない)ディスクブレーキ車を選択するメリットが不明瞭でした。

ところが現在では、ホイールリムとタイヤのワイド化が進行していて、かつては「太い」と見なされていた 25C のタイヤが主流になりつつあります。

一部のモデルでは更にワイドなタイヤを採用する動きもあり、従来のキャリパーブレーキでは対応が難しくなってきています。

現在のホイールでさえ、ワイドリム非対応のキャリパーブレーキには大きすぎて、ブレーキアーチのレリーズを半開放しながら使用している人を見かけるぐらいです。




この点、ディスクブレーキであれば、ホイールリムのワイド化など問題になりません。

ディスクブレーキ専用設計のロードバイクの登場を待つまでもなく、もう既にディスクブレーキ車の優位性を一般のホビーライダーが実感できる環境が整いつつあります。

もちろん、クリアランスの自由度だけではなく、操作性や整備性もディスクブレーキ車に優位性があります。

ここでメーカー視点に立ってみると、生産ラインをディスクブレーキに一本化できれば、ダイレクトマウント、デュアルピボットといったキャリパーブレーキを何種類も製造する必要はなくなりますし、ホイールの開発や製造も容易になります。

自転車の部品の中で、もっとも精度が求められるのはどこかと考えると、候補の筆頭に挙げられる気難しい部分がホイールです。

スポークテンションが少し変わっただけで横振れが生じ、真円度がさがれば突き上げるように走行に異常をきたし、乗り心地にも加速感にも大きな影響を与えます。

それに加えて、最近のチューブレスタイヤの普及です。これによって以前にも増してホイール(リム)の精度が重要になっています。

空気も漏らさない程度の精度を維持しつつ、衝撃にも強くなければなりませんし、さらにキャリパーを含むリムブレーキでは摩擦熱や摩耗、雨天使用時の制動力にも気を配らなければなりません。

こうなってくると、ブレーキ面に特殊な加工が必要で耐摩耗性も要求されるリムブレーキ仕様のホイールを止めて、ディスクブレーキ仕様に傾注したくなることは自然だと思われます。

ここで、ふと考えてしまったのです。ロードバイクのキャリパーブレーキは将来的には無くなるのではないかと。

現在の状況が数年間で大きく変わることはないでしょうが、キャリパーブレーキを前提とした新製品の開発が止まることで、性能重視のフラッグシップモデルから徐々に先細りしていく未来を想像してしまいます。

廉価モデルは現状維持か、もしくはVブレーキのクロスバイクとコンポーネントの一部を共有することになるかもしれません。

それではディスクブレーキ車に飛びつけば良いのでしょうか。

私は少なくとも所有しているキャリパーブレーキ仕様のバイクを乗り潰すまでは、新車を購入するつもりはありません。

その理由を申しますと「まだ過渡期に見えるから」です。

SHIMANO, SRAM, Campagnolo の3社ともディスクブレーキ仕様のグループセットが出揃って、エンド幅やマウントの規格は落ち着いた感があります。

しかし、SHIMANO を眺めていると Di2 に続いてパワーメーター内蔵クランクやグラベル専用のグループセットを新規開発したりと、いろいろやりたいことを残しているような気がして仕方がありません。


SHIMANO GRX FC-RX810-2 クランクセット 48/31 T

個人的には次のロードバイクには電動無線コンポーネント、それらと統合されたサイコン、ディスクブレーキ専用設計のエアロフレームが欲しいです。

それはそれで非常に楽しみです。でも今は全てが機械式の今のロードバイクを精一杯たのしみます。

輪行中にフレームが割れたりしない限りは…