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

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

上海の自転車事情

中国発のイノベーションとして、一時期、話題になった自転車シェアリング。

日本や英国でもサービスを開始したと思いきや、わずか1年ほどで撤退してしまい、今では人々の記憶にすらも残っていないかも知れません。

App store に残されたレビューにも、サービスの継続やアプリの更新を疑問視する声が溢れています。

ところが、本場の中国においては自転車サイクリングは決して過去のものではなく、2019年現在においても街中の至るところで利用されています。

道路を走っているところを見かけるのは(自動)二輪車以外は、ほとんどがレンタルバイクと言ってもいいほど多くの利用者がおり、社会に定着している感があります。

※ 観察している限り、中国では二輪車と自転車との区分は明確ではないのか、同じ場所を一緒に走っていることが多いです。その一方で机动车(四輪車を含むエンジン付きの車) と非机动车(エンジン無しの自転車)の区別は明確です。




上海の道路は自転車で走りやすいところが多く、地形的に起伏が少ないことも影響しているかもしれません。

もちろん、上海の道路においても圧倒的に優遇されているのは四輪車なのですが(日本のように建前だけは歩行者優先ではなく完全に自動車が優先です)、郊外においては自転車と二輪車の専用レーンが設置されており、四輪車とも歩行者とも完全に分離されています。

加えて、驚くべきことに路上駐車が本当に少ないです。しばらく仕事で滞在していますが、最後に路上駐車している車を見たのはいつだか思い出せないほどです。

率直に述べて、東京と上海とで道路環境のみを比較した場合、東京のほうが上海よりも優れている点あるいは両者で均衡していると言える点など、ただの一つもありません。

かと言って、土地の所有権もない警察国家が羨ましいかどうかというのは、また別の話ではありますが。

通行者を容赦なく撮影する速度違反取締装置があり、絶対的な権力を持った警察が常時監視しているから、日本の道路を通行するよりも安全に感じるという一面もあるかもしれません。

それでも逆走や不法投棄などを少なからず目にするあたりに民族性を感じます。

都心部では自転車専用レーンがないところも一般的なので、歩道を走行している自転車や二輪車も見かけます。

どうやら歩行者専用道を除いては、自転車も二輪車も問題なく歩道を走行できるようです。

自転車が通行できない場所には明確に通行禁止の標識が掲げられていたり、禁止非机动车通行と警告表示がなされています。

さきに述べた繁華街の歩行者専用道などには、こうした標識が提示されています。

自転車を借りられることが分かり、交通ルールも覚えたら、実際にレンタルバイクに乗って散策してみたくなるものです。

中国の自転車シェアリングサービスを利用する場合には、現地で通信できるスマートフォンと WeChat もしくは Alipay が必要になります。

前者は SIM カードを現地調達すればいいとして、後者は少し厄介でして、インストールしたアプリに中国の銀行口座を紐付けないと使えないサービスもあるようです。

無事に準備が完了できたら、自転車に添付されているQRコードをスマートフォンの専用アプリで読み取って解錠します。

これだけを見ると簡単に利用できて便利に見えますが、実際には無人で管理されているがために、個々の自転車ごとに著しい状態差があったり、返却手続きが機能しなかったりと不便な点も少なからず感じられます。

レンタルバイクのなかには走行不能、もしくは、それに近いものも存在します。

かと言って、自分の自転車を持っていくと盗難の問題が付きまといますので、それはそれで不便な面もあります。

スポーツサイクルのショップも普通にありますけれども、路上でロードバイクやクロスバイクが走行しているところを見たことがありませんので、どれぐらい普及しているのかは不明です。

レンタルバイクに比較すると個人所有の自転車そのものが少ないので、安価でレンタルできる自転車をわざわざ購入する積極的な理由が乏しいのかも知れません。

私個人も写真撮影が捗らない、大気汚染が改善されてきているとはいえ臭いがまだ気になる、外国人がむやみに近づいてはならない場所が多すぎる、そもそもインターネット環境が不便すぎて情報検索すら一苦労などの諸々の事情から、あまり自分の自転車を持ってきたいとも思いません。

地下鉄を始めとする公共交通の料金もおそろしく安いので、都心部で生活する分には自転車がなくても不自由はしません。

凪の西伊豆

海外に出張続きの日々が続くと、ときどき海岸線の風景が見たくなります。

切り立った崖沿いの小道を進んでいくと、突如として眼下に広がる湊町、遠浅の海の透き通った水に古びた商店など、いかにも日本らしい光景が最高です。

東京近辺でこうした風景が見られるのは三浦半島の南端ですが、半島地形だけに休日はどの道を通っても 20km 近くの断続渋滞に巻き込まれます。

そこで、今回は関東平野を抜け出して、知る人ぞ知るサイクリストの楽園にやってきました。関東近郊で自転車をもっとも楽しめる最高の場所、それは西伊豆です。

西伊豆の良さは海と高山を併せ持つダイナミックな地形、交通量の少ない静かな道路、運転マナーの良さ、地元の人の親切さなど枚挙に暇がありません。




ただし、ここを訪れる人が少ないことからも分かるように、それなりに高いハードルもあります。

急峻な地形、人口希薄地帯の広さ、強風、夏の暑さと日陰の少なさなど、自転車で周遊する際の難易度では伊豆半島は暫定日本トップ3に入るぐらいです。

ちなみに他の2つは紀伊半島の北部(奈良県南部、和歌山県北部、三重県南西部)と山梨県の大弛峠で、この中で一番やさしいのが大弛峠です。

伊豆半島では普通に舗装路を走っているだけで、距離 100km にして獲得標高は 2,000m 超なんてことになります。集落と集落のあいだは民家すら存在しない無人地帯で、自販機1台すらも無い山道が 20km 近くに渡って続くことが普通です。

坂道はどこも急勾配で、多くの坂において最高斜度は 14% を越えます。

この無補給地帯に耐えられる体力と激坂に負けない走力を備えた者だけが伊豆の美しさを堪能できるのです。

地形的な困難も大きい分、走り終えたあとの満足感や心地よい疲労感もひとしお — そんな伊豆ライドの開始点は函南駅か三島駅のどちらかがオススメです。

小田原から下田までの国道135号線は本当に使えない道路(※自動車専用道を除くと、関東から直接伊豆に向かう唯一の道路ながら常時渋滞している最大のボトルネック)なので、ここをいかに回避するかが伊豆旅行の質に大きく影響します。

湯河原から網代までを避ければ、恒常的に渋滞している区間はそれほど多くはないので、東京から訪れる場合でも小田原や熱海ではなく、函南や三島から中伊豆(韮山や修善寺)を目指されるとスムーズに到着できます。

実際のところ、道路が中伊豆の方を向いているので、海岸沿いの混雑地帯を避けながら伊豆に向かうと自然と伊豆長岡あたりを経由することになります。

ここまで来たら、東伊豆(伊東や河津)を目指しても、西伊豆(戸田や土肥)を目指しても、南伊豆(下田や石廊崎)を目指しても、もちろん天城や伊豆高原を目指しても、どこに向かっても急勾配の坂道だらけです。

ちょっと丘を越えようと思って登り始めた坂が 10km 以上も続いて、終わりが見えなくなることも少なくありません。

あの国道1号線ですら海抜 4.8m の小田原(市民会館前)から、わずか 18km で標高 874m (箱根町 国道1号線最高地点)まで登ることを余儀なくされているのが、この富士箱根伊豆という地域なのです。

この日は初秋らしく気温は 32℃ 湿度は 80% もありましたが、風は 1m/s の東風とほとんど無風状態でした。

こういうときは遠景こそ期待できませんが、凪の穏やかな水面を堪能することができます。

行き先としては仁科峠を越えて松崎に向かうよりも戸田や内浦のほうが楽しめそうな気象条件です。

ここで今日の行き先が決まりました。

と言っても、どこに行くにも山越えが避けられないのが伊豆半島です。きっちり標高 730m の戸田峠まで登ります。

この辺り、もし北海道であったら間違いなく道の駅が設置されているであろう場所が、ことごとくゴルフ場やテーマパークなどの敷地で埋められています。

こうした事情も伊豆らしさであり、補給場所が皆無なのでとってもツライです。

誇張なしに補給場所は だるま山高原レストハウス とそこから 22km 離れた 西天城高原「牧場の家」 の2つしかありません。

道路の雰囲気は湯河原椿ライン(神奈川県道75号線)に似ていて、交通量は少なく、舗装も比較的良好ですが、斜度はこちらの方が厳し目です。

戸田峠まで登りきると、そこから西伊豆スカイラインに入って、さらに登り続けることができます。

西伊豆スカイラインの高原風景は一見の価値ありです。苦労して登ってきた甲斐があります。

この後もまだまだ登り続けて、気がついたら標高 900m を越えていたりするのですが。

今日は海を見に行きたいので土肥峠で西伊豆スカイラインを降りて、土肥温泉へ向かいます。

普通はあまり西伊豆スカイラインから土肥温泉に向かう人はいません。

そのままスカイラインを南下すると絶景で有名な西天城高原に辿り着くので、そこまで行く人が多いのと、土肥峠あたりは最大斜度 14% の下り坂がつづくので慣れていないと危険です。

事前情報では路面状態が悪いと伺っていましたが、訪れてみた際にはそれほど悪いとは思いませんでした。

長い長い下り坂を抜けると、いよいよ海が見えてきました。

目論見通りに凪の海は透明度が高く、海岸沿いでも奇跡的な無風状態です。

この後の予定がなければ、このまま何時間でも波打つ水面を眺めていられます。

でも、楽しいのは、実はここからです。

海岸沿いの道路は漁村を通り抜けて、切り立った崖を登って山道になり、そこから海を見下ろした後に、次の漁村に向かう長い下り坂に変わります。

景色の移り変わりやコースの高低差という意味では、決して山間地にも引けを取らないものがあります。

仁科峠まで行かなかったことを全く後悔させないぐらいの素晴らしい景色です。

相変わらず、補給地点は市街地までいかないと一つもありませんし、わずかな市街地を除けば坂道しか無いほど、平地がほとんど無いのですが、それがまた楽しくもあります。

ほんとうに強風さえなければ、西伊豆は最高です。

土肥から戸田、江梨、西浦と進むに連れて、景色もさらに魅力を増していきますが、それと同時に交通量も少しづつ増えていきます。

三津まで行くと、沼津の中心部からの市街地が連続しているので、ほとんど沼津の郊外といった趣になります。

地図上の距離で見ると、伊豆への輪行には沼津駅も良さそうに見えますが、三島よりも沼津のほうが市街地区間が長いのと国道414号線は道幅が狭くトンネルも多いので、あまり通りたいとは思えないところが残念です。

国道414号線の内浦ルートに比べると、往路につかった伊豆長岡ルートは景色は単調ですが、田園地帯なので交通量は少なめです。

ここまでで意外と余力が残っていたので、復路は長岡と韮山を経由して函南まで行くことに決めました。

もっと早い時間にたどり着いていれば、帰り道の途中に十国峠を入れて 3,000m UP も楽しそうなんて思えてきます。

実際のところは日没後のダウンヒルは神経を使うだけで楽しくないですし、箱根の渋滞を避けようとすると湯河原に抜けて、そこから国道135号線という最低の道路を通ることになるので、なかなか難しいところです。

そうすると今度は熱海を起点にして考えてみるのが良いんでしょうかね。