等高線付きの地図を作成する その2(アンドロイド地図アプリの開発 捕捉)

GWがステイホームで時間があったので、mapsforge用の等高線付きに地図作成にチャレンジしてみましたが、色々ハマりまくりで全然終わらず、目的の地図ができずじまいで終わってしまいました。

alasixosaka.hatenablog.com

今回は、その続きです。
その前に、ざっと前回のおさらい。
アンドロイド地図アプリを開発中で、アプリにはMapsforgeというエンジンを使っていて、map形式のファイルが必要。等高線付きの日本地図は20m間隔なら公開されている方がおられるが、もう少し精度を上げて10m間隔にしてみたい。そのためには、国土地理院から等高線のデータをダウンロードして自分で作成する必要がある。
やり方はをざっと書くと、まず、国土地理院のサーバーからデータをダウンロードする。それをfdggemというPythonスクリプトでGeoTIFF形式に変換。これらは非常に細かいメッシュに分かれているので、これをGdalを使って一つにまとめる。それをPhyghtmapを使ってOpenStreetMap形式に変換する。OpenStreetMapの地図ならこれと等高線なしの地図をマージすれば完成。
ところが、Mapsforgeでは、海と陸の境界を独特の処理で表しているので、陸地の外形をシェープファイルで取ってきて、それをまた、shape2osmでOpenStreetMapの地図に変換し、海の部分を表すOSM形式の簡単な地図を作成する。これらをosmosisでマージして、map形式に変換すれば完成する。
詳しいやり方は、前回の記事を見て下さい。

前回は、四国の地図についてはうまくできましたが、同じやり方で本州をやろうとしたらメモリ不足に悩まされてできませんでした。おそらく、等高線の情報が非常に多いためにデータサイズが多いのが原因ではないかと思います。なんせ四国の地図でも等高線付きは360MBもありますから。等高線が無いとわずか42MBです。
そこで、本州全体を一つにすることはあきらめて近畿だけの地図を作ることを考えました。ところが、処理がうまくできずに海が白い地図ができてしまいました。
ここまでが前回のお話。今回は何とかして近畿地方の等高線マップを作り上げるという話です。同じやり方で他の地方の地図も作れます。

やり方を考える。

とりあえず、いろいろ考えて、4つの方法がありそうだと思いました。

  1. osmosisのbboxという機能を使って、読み込みは本州全体として、書きだすところを近畿だけにしてみる。
  2. Mapsforgeサイトの記述に従って、海岸線のデータを切り出すところから近畿エリアを使って処理する。
  3. Mapsforgeサイトにある、Pythonスクリプトを使う方法を試す。
  4. Mapsforgeサイトにある、シェルスクリプトを使う方法を試す。

方法1

この方法がいままでのデータを使えるので最も簡単に済む。しかし、読み込むファイルを本州全体でやってしまうとメモリ不足が起こる可能性が高い。そこで、等高線のデータは近畿のデータにして、それ以外は本州全体のデータを使ってやってみた。
しかし、途中でバッファオーバーフローが出て止まってしまいうまくいかなかった。
そこで、kinki.polyというファイルと作って、

1 1 union_of_selected_boundaries
   134.256364 35.721237
   134.256364 33.400212
   137.039213 33.400212
   137.039213 35.721237
   134.256364 35.721237
END
END

osmcovert で元の地図データと海岸線のデータを切り出し、海用のデータkinki_s.osmを作成し合体して見た。しかし地図は出来たが相変わらず海は白いままだった。

方法2

方法2~方法4はいずれも、Mapsforge本家サイトに載っている方法。方法2が一番古いやり方らしく、コマンドラインでちまちまと処理を行っていく方法になっている。参考にしたサイトでも基本的にこの方法を使っている。
yueno.net
等高線を入れるとデータが大きくなり、処理に時間がかかるので、とりあえず愚直にサイトのやり方に従ってやってみた。
まず、海岸線のデータをダウンロードせよと書いてあるが、これは以前にダウンロードしてあるのでそれを使った。
そこからogr2ogrで必要なエリアを切り出す。ファイル名はkinki_o1.shpとした。

ogr2ogr -overwrite -progress -skipfailures -clipsrc 134.256364 33.400212 137.039213 35.721237 kinki_o1.shp land-polygons-split-4326/land_polygons.shp

つぎにこれをshape2osmでOSM形式に変換する。

py -2 -m shape2osm -l kinki_o1_ns kinki_o1.shp

これで、kinki_o1_ns.1.osmというファイルができる。
次に、海の部分を表す、kinki_s.osmを作った。

<?xml version='1.0' encoding='UTF-8'?>
<osm version="0.6" generator="osmconvert 0.8.2">
	<bounds minlat="33.400212" minlon="134.256364" maxlat="35.721237" maxlon="137.039213"/>
	<node timestamp="1969-12-31T23:59:59Z" changeset="20000" id="32951459320" version="1" lon="134.256364" lat="33.400212" />
	<node timestamp="1969-12-31T23:59:59Z" changeset="20000" id="32951459321" version="1" lon="134.256364" lat="35.721237" />
	<node timestamp="1969-12-31T23:59:59Z" changeset="20000" id="32951459322" version="1" lon="137.039213" lat="35.721237" />
	<node timestamp="1969-12-31T23:59:59Z" changeset="20000" id="32951459323" version="1" lon="137.039213" lat="33.400212" />
	<way timestamp="1969-12-31T23:59:59Z" changeset="20000" id="32951623372" version="1">
		<nd ref="32951459320" />
		<nd ref="32951459321" />
		<nd ref="32951459322" />
		<nd ref="32951459323" />
		<nd ref="32951459320" />
		<tag k="area" v="yes" />
		<tag k="layer" v="0" />
		<tag k="natural" v="sea" />
	</way>
</osm>

つぎにこれらを一つにマージする。

osmosis --rb file=kinki.osm.pbf --rx file=kinki_s.osm --s --m --rx file=kinki_o1_ns.1.osm --s --m --wb file=kinki_o1.pbf omitmetadata=true

最後にmap形式に変換する。

osmosis --rb file=kinki_o1.pbf --mw file=kinki_o1.map bbox=33.400212 134.256364 35.721237 137.039213  map-start-zoom=10

すると、とりあえず海は青くなったが、なんだか地図全体が青い。陸地も青いところだらけの変な地図になった。海と陸の境界がうまくいってないみたいだ。
なかなかうまくいかない。
うまくいかなかった理由は、海=seaと陸=landのレイヤーがあってなかったみたいだ。上のkinki_s.osmでは、tag k="layer" v="0" /となっているが、mapsforteのサイトではtag k="layer" v="-5" /となっている。
そして、kinki_o1_ns.osmを作った時に使ったshape2osmが2種類あり、Mapsforgeのサイトのものは、

fixed_tags = {
  'natural': 'nosea',
  'layer': '-5'

とlayer を-5にするようになっているが、参考サイトから持ってきたshape2osmでは

fixed_tags = {
  'natural': 'nosea',

となっていて、layerの記述がない。それで、tag k="layer" v="0" /としているのではないかと思う。レイヤーがあってないので海と陸の区別がうまくいかずに、全面海のような感じになったみたいだ。

f:id:alasixOsaka:20200517122957j:plain
京都市が水没した感じの地図になってしまった。
そこで、kinki_s.osmを作り直して、tag k="layer" v="0" /の部分をtag k="layer" v="-5" /とし、shap2osmもlayerの記述の入った方を使うことにして、始めからやり直した。
openstreetmapのファイル名はkinki_o1_o.pbfとした。

osmosis --rb file=kinki.osm.pbf --rx file=kinki_s.osm --s --m --rx file=kinki_o1_ns.1.osm --s --m --wb file=kinki_o1_o.pbf omitmetadata=true

これをmap形式に変換した。

osmosis --rb file=kinki_o1.pbf --mw file=kinki_o1_o.map bbox=33.400212 134.256364 35.721237 137.039213  map-start-zoom=10

すると、海がちゃんと表示されるようになった。
そこで、等高線の地図データとマージしてみた。

osmosis --rb file=kinki_o1_o.pbf --rb file=kinki_dem10b.osm.pbf --s -m --mw file=kinki_o1_oc.map bbox=33.400212 134.256364 35.721237 137.039213  map-start-zoom=10 type=hd

これでようやく等高線付きの地図ができた。

f:id:alasixOsaka:20200517125355j:plain
海が青く、等高線も表示できた。
ファイルのサイズは等高線なしで216MB、等高線ありだと748MBになった。10m間隔では本州全体を一つのファイルにするのは無理がありそう。
近所のポンポン山も10m間隔の方が見やすい。
f:id:alasixOsaka:20200517130641j:plain
ポンポン山付近の比較左が20m間隔。
今回は、方法2で何とかうまくいったので、方法3と方法4は試していない。ただ、方法4は地図データもサーバーからダウンロードするようになっているので、等高線をつけようと思うとシェルスクリプトを解読して変更する必要があるので、ちょっとハードルが上がりそう。pythonを使うのに問題が無ければ方法3の方が無難かもしれない。方法3なら自分で用意したosm の地図をベースに使えるのでやり易いと思う。また、時間のある時に試してみようかと思うがいつになるかは不明。

等高線付きの地図を作成する(アンドロイド地図アプリの開発 捕捉)

前回の総まとめでも書いた通り、map形式の地図ファイルで等高線付きのものは等高線間隔が20mまでです。10m間隔の地図は自分で作るしかないようです。
alasixosaka.hatenablog.com

そこで、出来るかどうかチャレンジしてみました。
基本的には次のサイトのやり方を踏襲しました。
[地図自作] 国土地理院の数値標高モデルを使って等高線入り地図を作成する方法 -すけログ
このサイトでは最終的にGamin GPS用の地図データを作成していますが、途中までは同じやり方でできます。

基本的には国土地理院のサーバーから数値標高モデルをダウンロードし、GeoTIFF形式に変換、等高線をOSM形式に変換、等高線のないOSM地図と合わせる。という流れになります。

等高線データのダウンロード

国土地理院のサーバーには日本の地形情報が数値データとして格納されていて、ダウンロードできるようになっています。
fgd.gsi.go.jp
ここから、数値標高モデルをダウンロードします。
ダウンロードは地域別とメッシュ別から選択できます。あまり深く考えずに地域別を選択し、日本全部をダウンロードしましたが、県境のエリアなどではオーバーラップがあるようで、同じファイルがダウンロードされてしまいます。メッシュでやった方が効率が良いようです。
ダウンロードしたファイルはCAB形式で圧縮されており、解凍するとさらにZIP形式に圧縮されています。後で述べる方法ではZIPを解凍する必要はありません。(私は別の方法を先に試したので解凍してしまいました)。
ファイルは全部で4885個あります。とんでもない数です。改めて地図というのは情報の塊であるということを認識しました。

GeoTIFF形式に変換

ここのサイトには色々なやり方が書いてありますが、QGISというソフトを使うのが一番簡単そうなのでまず初めにやってみましたが、プラグインのfgddemImporterが最新版に対応していないので使えませんでした。
www.chuogeomatics.jp
結局、最初の参考サイトのPythonスクリプトを使った方法になりました。ただ、これも一筋縄ではいかず。参考サイトはLinux環境で作業されていますが、Windowsでやろうとして色々ハマりました。
まず、Phythonスクリプトですが、作者の方がアップされているスクリプトがPhython2.x系のものだったので、3.x系ではエラーになって走らず、Phython2.7を改めてインストールしました。
github.com
それでも、numpyがないと言われるので、PIPでインストール。すると、今度はGDALが無いと言われる。ところが、GDALはPIPではインストールできず、調べるといろいろと手順が必要なことが分かった。
基本的には次のサイトの記述通りにやればできました。
gpsrsgis.seesaa.net
まず、GDALのHPへ行って、コアというのをダウンロードしてインストールする。次に同じサイトのPythonとGDALをバインドするためのプログラムをダウンロードする。このとき、Pythonのバージョンに気を付ける。今回の場合は2.7が必要。
最後に、環境変数を設定
PATHにC:\program files\GDAL を追加
GDAL_DATAを作成し、変数値をC:\program files\GDAL\gdal-data にする。
GDAL_DRIVER_PATHを作成し、変数値をC:\program files\GDAL\gdalplugins にする。
これでようやく変換用のスクリプトが動くようになりました。
スクリプト(fgddem.py)と国土地理からダウンロードしたファイルをd:\GPS\DEMの置きました。
Windowsコマンドプロンプトディレクトリをファイルのあるところに移動し、
py -2 fgddem.py FG-GML-*-DEM10B-*.xml
と打ち込めば変換が始まります。とっても時間がかかるので気長に待ちましょう。

f:id:alasixOsaka:20200503141414j:plain
変換には時間がかかる
とにかく、等高線のOSMファイルを作ってしまえばこの作業は頻繁にすることはないので時間のある時にやるしかないですね。地形は大規模な開発や火山の噴火などがあったら変わりますけど、基本的には変わらないものですから。ダウンロードしたデータも2016年のもので地図データとしてはけして新しいものではないですし。むしろ、道ができたりとかの方が重要なので、合わせる地図データの方をいつも最新にしておくのが良いと思います。

出来たファイルを結合する。

出来たファイルは結合して一つにします。
参考サイトでは
gdalbuildvrt japan-dem10b.vrt FG-GML-*-DEM10B.tif 
で結合していますが、ワイルドカードを受け付けないみたいで、どうやってもうまくいきませんでした。ファイルを一つ一つ指定すればできるみたいでしたが、4000以上あるファイルを手で打ち込むのは不可能なので、別の方法を行いました。
結合する方法はgdalbuildvrt.exe を使う方法以外に、gdal_merge.pyを使う方法があるようです。ただし、gdal_merge.pyもワイルドカードを受け付けないので、ファイルのリストを作ってそれを使う方法が次のサイトに書いてありました。
deerfoot.exblog.jp
同じ方法が、gdalbuildvrt.exeにも使えるのではと思い、Gdalのドキュメントを読むとやはり、gdalbuild.vrtでもリストファイルを使う方法がありました。
gdalbuildvrt — GDAL documentation
まず、
dir /b *.tif > list.txt 
でtifファイルのリストをlist.txtに書き出し、
gdalbuildvrt -input_file_list list.txt japan-dem10b.vrt
として一つのファイルにまとめました。

等高線のOSMファイルを作成する。

GeoTIFFからOSMファイルを作成すれば等高線部分は完成です。参考サイトには2つのやり方が書いてあって、一長一短という感じですが、両方試してみました。しかし、どちらもとっても苦労しました。
2つのやり方はphyghtmapを使う方法とgdal_contourを使う方法です。

phyghtmapを使う方法

シンプルだが等高線が所々で途切れるとのこと。
まずは、日本全体のファイルを細かく分割します。
japan-dem10b.vrtがあるフォルダに子フォルダとして、retileを作成します。コマンドプロンプト上で
gdal_retile.py -ps 9000 6000 -overlap 1 -co "COMPRESS=LZW" -co "PREDICTOR=2" -co "NUM_THREADS=ALL_CPUS" -targetDir ./retile japan-dem10b.vrt
とすれば細切れになったファイルがいっぱいできます。
次に、これをphyghtmapを使ってOSMファイルに変換するわけですが、始めはWindowsでやろうとしましたが断念。phyghtmapのサイトにはWindowsにも対応していると書いてありましたが、どうやらバージョンが古く、matplotlibの削除されたモジュールを使っているのが原因でエラーになります。matplotlibのバージョンを1.1くらいに戻さないと必要なモジュールが入ってないようですが、インストールしようとすると古すぎるのか拒否されてうまくいきません。
仕方がないので、WSLを使ってWindows上でUbuntuを動かして処理することにしました。
ここからの作業はWLS上のUbuntuコマンドプロンプト上での作業になります。
2020/5/5現在、WSLでUbuntuを導入するとバージョンは18.04になるようです。デフォルトでPython3.8が入っていました。これにPython2.7を導入しようとしましたが、今一やり方がわからず、困ってしまいましたが、phygmapのオリジナルサイトをみると、最新版は2.21でPhython3.x系に対応しているようです。
phyghtmap

そこで、参考サイトを見てバージョンだけを1.80から2.21に変更し、
wget http://katze.tfiu.de/projects/phyghtmap/phyghtmap_2.21.orig.tar.gz
でダウンロードしようとしましたが、サイトに拒否られてダウンロードできません。
仕方がないので、オリジナルサイトから直接ダウンロードし、適当なフォルダに置いておいて、そこから
tar zxvf phyghtmap_2.21.orig.tar.gz
で直接展開しました。
そして、
sudo python3 setup.py install 
でインストールできました。参考サイトでは
sudo python setup.py install 
とpython3の部分がpythonとなっていますが、これはPython2.7で行っているためと思います。Python3.xではスクリプトの実行には、
Python3 XXXX.pyとする必要があります。
Python関係の必要なモジュールをインストールする手順もすべて
sudo apt-get install python3-XXXXのようにpythonではなくpython3としないとだめのようです。一見インストールされたように見えますが、スクリプトを動かすとモジュールが無いと怒られます。必要なモジュールは、gdal-bin、python-numpy、python-matplotlibです。
これでようやくphyghtmapが動くようになったので、参考サイトに倣って、
phyghtmap --pbf --output-prefix=japan_dem10b --step=10 --line-cat=500,50 --no-zero-contour --void-range-max=-500 --start-node-id=20000000000 --start-way-id=10000000000 --max-nodes-per-tile=1000000 --write-timestamp --jobs=4 japan-dem10b*.tif
とすれば良いのですが、こうするとまた、細切れのファイルが大量にできてしまいます。参考サイトではこれを全部合わせて等高線以外のデータを合体しGaeminGPS用の地図を作っています。自分の目的はMapsforgeで使えるmap形式のファイルを作ることなのでここから先が異なります。また、ファイルは細切れでなく一つにしておきたいので、別のサイトを参考に、

    • max-nodes-per-tile=1000000

となっているところを

    • max-nodes-per-tile=0

とすると一つのファイルになります。
上野家のホームページ - PC/地図/Locus map用ベクター地図Locus Map - 資料室
時間はめちゃめちゃかかります。マシンとしては、CPUがCorei7の第八世代でメモリは16GB積んでいますが、半日では終わらないです。
参考サイトでは等高線のつなぎがおかしくなると書いてありますが、今のところおかしな部分は見つかりませんでした。詳細に確認していないので確かではないですが、--max-nodes-per-tile=0で結合したらうまくいったみたいです。

gdal_contourを使う方法

等高線は奇麗につながるが、手間がものすごく大きいとのこと。python3.xでやる方法がないので今のところ断念中。

等高線とマップデータを合わせる。

最後の仕上げです。タイトルの通りデータを合わせるだけなら大した作業ではないですが、mapsforgeでは海の処理を特殊な処理を行っているらしく、ここから先は海ですよと教えてあげないといけないらしい。そうしないと海が白く表示されてしまう。
blog.mori-soft.com
そこで海の処理を加えた形で等高線を合わせる必要がある。ここから先は別のサイトの記事を参考に作業した。
yueno.net
このサイトでは等高線付きのLocusMap用地図データを作成している。等高線についてはNASAのデータを使って20m間隔のものを作っておられるが、この等高線の部分をさっき作った国土地理院のデータから作成した10m間隔のものに変更した。

地図データの準備

地図データはOSM形式のものを使う。サーバーからなるべく最新のものをダウンロードする。ダウンロードできる地図は日本全土のものだが、作業は日本の地図を北海道、本州、四国、九州の4つに分けて行う。
そのために、osmconvertというツールを使う。Windows上で作業するには、サイトに行ってWindows用のファイルをダウンロードして実行すればOKでインストールの必要はない。ダウンロードして適当なフォルダに置いておいて、実行ファイルをコマンドプロンプトから実行する。
分割するには、参考サイトの記述に従い、参考サイトから4つのpolyファイルをダウンロードし、分割を行った。
参考サイトでは
osmconvert japan-latest.osm.pbf -o=japan-latest.o5m
として、一旦、o5m形式に変換しているが、これはこの後、余分なローマ字記述などを削除するためのものなので、そのまま使う場合はファイル形式を変換する必要はない。
分割は、例えば北海道なら
osmconvert --complex-ways japan-latest.o5m -B=..\poly\Hokkaido.poly -o=hokkaido.o5m
となるが、ファイル形式を変換しない場合。
osmconvert --complex-ways japan-latest.pbf -B=..\poly\Hokkaido.poly -o=hokkaido.pbf
でOK。
自分は、何も考えずにサイトの記述に従ってo5m形式に変換したが、その後の削除は必要ないと思ってそのままにしたので、分割してからまたpbf形式に戻すという作業をしてしまった。

海岸線のデータを用意する。

次に、海岸線のデータを用意する。これも参考サイトの記述に従った。
データはOpenStreetMap dataというサイトにあり、land-polygons-split-4326.zipファイルをダウンロードすればよい。
次にこの中にあるland_polygons.shpというファイルからogr2ogrを使って日本の部分を切り出します。
ogr2ogrはGDALをインストールしていれば入っているので、コマンドプロンプトでogr2ogrのあるディレクトリに行って、
ogr2ogr -overwrite -progress -skipfailures -clipsrc 122.560700 21.209920 153.890100 45.802450 japan.shp land-polygons-split-4326/land_polygons.shp
とすればできます。自分の場合は、d:\GPSの下に先ほどのファイルを解凍したので、その下にland-polygons-split-4326というフォルダができていて、その中に目的のファイルがあります。
ですので、コマンドプロンプト
d:
cd gps
としてd:\gpsディレクトリを持って行って先ほどのコマンドを実行しました。そうするとjapan.shpというシェイプファイルができます。
次にこれをosmファイルに変換するのですが、ここが少し苦労しました。
色々試行錯誤したので無駄なことをしているかもしれませんが、まず、参考サイトのリンクからshape2osm.pyというpython スクリプトをダウンロードして、サイトの記述通りにやってみましたが、
py -2 -m shape2osm.py -l japan_ns -o 10000000 japan.shp
とすると、shape2osm.pyなんてモジュールはないと怒られる。エクスプローラーで確認してd:\GPSにちゃんと入っているにもかかわらず。
仕方ないので、オリジナルサイト
GitHub - mvexel/shape2osm: script to convert POINT shapefiles to OSM XML files
から、ダウンロードし、実行すると、simplestxmlwriterがないと怒られる。調べるとpython2.5までは標準で実装されていたみたい。仕方ないので自分でインストールすることにした。下記のサイトからダウンロードし、
ElementTree Overview
サイトの記述に従ってインストールした。これを実行すると、今度は記述が正しくないと怒られる。確かに、オリジナルサイトの使い方では、
shape2osm.py [-h] INFILE OUTFILE
となっていて、シンプルな記述だし、インプットファイルとアウトプットファイルの順番が逆になっている。
訳が分からないので、Pycharmを起動し、参考サイトのスクリプトを読み込んで実行してみた。実行するときに、オプションで引数が入れれるので、そこに、

  • l japan_ns -o 10000000 japan.shp

として実行すると何故かうまくいった。訳が分からないがとりあえず、目的のファイルができているみたいだ。ただ、ファイル名がjapan_ns.1.osmとなっていて参考サイトの記述 japan_ns.osmと異なる。一応QGISでファイルを開いてみた、なんとなく日本列島の形になっているが海岸線が途切れているところとつながっているところが半々くらいになっている。こんなのでよいのかよくわからない。

その後、Python2とPython3を両方インストールしているWindows環境では、
py -2 -m shape2osm -l japan_ns -o 10000000 japan.shp
というように、拡張子を取るとちゃんと動作することが分かった。
したがって、python xxx.py 〇〇〇 と書いてあるのは、py -2 -m xxx 〇〇〇と読み替えると良いことが分かった。

出来たファイルはJOSMというソフトを使って確認した。
josm.openstreetmap.de

JOSMをダウンロードして、インストールします。シェープファイルを読むにはプラグインが必要なので、設定メニューから必要なプラグインを選んでインストールします。そして、先ほどのjapan.shpを読み込むと奇麗に日本の海岸線が表示される。
JOSMでは、読み込んだファイルをOSM形式でセーブする機能がある。
しかし、この機能を使ってしまうと、mapsforgeが海岸線を認識するのに必要なタグが付いていないのでうまくいかない(と思う)。やってみると、Pythonで作ったファイルとファイルサイズが異なる。
ちなみに、このJOSMというソフトはOSM形式のファイルを地図として表示してくれ、また、QGISと違って動作が軽い、ありがたいソフトなのだが、大きなファイルを読み込めない。後で出てくるJavaのメモリの問題のようだ。
できたosmファイルをまた、さっきと同じようにしてPolyファイルを使ってosmconvertで分割する。
そして、参考サイトから、北海道、本州、四国、九州それぞれに対応したxxx_s.osmファイルをダウンロードして、さっき作ったxxx_ns.osmと合体させる。
合体作業は、osmosisというのを使う。
例えば、四国なら
osmosis --read-xml file="shikoku_s.osm" --read-xml file="shikoku_ns.osm" --sort --merge --write-xml file="shikoku_sea.osm"
とすれば、四国版の海と海岸線のデータが完成する。

等高線と合体してmap形式のファイルを作成する。

ここから先は参考サイトでは、今まで作った各種ファイルを合体する作業とmap形式のファイルを作る作業を同時に実行しているので同じようにやってみた。
ここでも、osmosisを使うが、map形式のファイルを作成するのにプラグインをインストールする。
ここからの作業はまた、めちゃめちゃ苦労した。理由は、プラグインのインストールとメモリー不足。試行錯誤の連続で頭がウニになってしまったので整理して書くこともできない状態。
とりあえずやったことを書いておくことにした。日本を4つの島に分割しているのでまずはなるべくデータの小さい四国で試すことにした。

まず、用意するファイルは、OpenStreetMapのサーバーからダウンロードした日本全土の地図から上記の作業で切り出した四国のファイル。ファイル名はshikoku.osm.pbf。
それから、等高線地図から切り出した四国のファイル。ファイル名はshikoku_dem10b.osm.pbf。
そして、海と海岸線のデータの四国のファイル。ファイル名はshikoku_sea.osm
この3つをosmosisを使って合体し、なおかつmap形式のファイルへ変換する。

map writerプラグインのインストール

mapsforgeのサイトでは結構さりげなく書いてある。
github.com
osmosisをダウンロードして適当なフォルダに展開し、プラグインをダウンロードし、/.openstreetmap/osmosis/pluginsというフォルダを作ってそこに置いておく。
実はこれはLinuxでの手順だった。Windowsで行き詰ったのでWSL上のubuntuでもやってみたが、osmosisを動かすことはできたが、結局プラグインを動かすことができなかった。
一応手順というかやったことを書いておく。
WSLでubuntuを起動。参考サイトの記述に従って
wget http://bretth.dev.openstreetmap.org/osmosis-build/osmosis-latest.tgz
mkdir osmosis
mv osmosis-latest.tgz osmosis
cd osmosis
tar xvfz osmosis-latest.tgz
rm osmosis-latest.tgz
とした。ただ、これだけでは不十分なようで、
sudo apt install osmosisを実行しないとダメみたいだ。インストールしたのは最新版でなくバージョン0.41にした。何故かというと、ある掲示板に最新版ではプラグインが動かないが0.41なら動くと書いてあったから。
ちなみに、
sudo apt-get install osmosisでもインストールができるが、最新版(バージョン0.48)がインストールされるうえに、pbfファイルを読み込むところでエラーで止まってしまう。上記の方法だと、プラグインを使った動作以外はうまくいくみたいだ、試したわけではないので保証の限りではないが、pbfファイルの読み込みではエラーにならなかった。しかし、map形式へ変換しようとするとプラグインが無いというエラーを吐いて停止する。
結局Windowsでやるしかないので、Windowsでやった。
Windowsでのmapfile writerプラグインのインストール方法を探すとこのサイトを見つけた。
github.com
googleで検索すると結構上位にくるのでこのサイトの方法が正しく見えるが実は間違っている。
まず、プラグインを置いておくフォルダが違うし、ダウンロードするファイルも違う。
ここの掲示板の最後に書いてあるやり方が正しい。
stackoverflow.com
プラグインも色々バージョンがあるが、実績のある0.6.0を使った、ダウンロードは下記のサイトから行った。
repo1.maven.org
ダウンロードするファイルは長ったらしい名前だが
mapsforge-map-writer-0.6.0-jar-with-dependencies.jar
というファイル。with-dependenciesがついてるやつでないとダメらしい。
ちなみにosmosisも実績のある0.40を使った。ダウンロードは下記のサイトから。
bretth.dev.openstreetmap.org

Windows用は拡張子がzipのもの。
そして、zipを解凍し、適当なフォルダに置いておく。そして、その下にlibというフォルダがあり、さらにその下にdefaultというフォルダがあるのでプラグインはこの中に入れる。
また、この作業は必ずしも必須ではないが、コマンドプロンプトからosmosisを呼び出すので、環境変数を設定して、PATHを通しておいた方が作業がやり易い。

メモリ不足との闘い

ここまでの作業で、windowsコマンドプロンプトからosmosisとmapfile writerプラグインを動かすことができるようになった。(というか正しくはコマンドを認識してエラーを吐かないだけでしたが)
さて、これで行けると思ってmapファイルを作ろうと思ったがout of memoryのエラーでストップする。
まず、メモリ不足に対する対処として、検索するとヒープメモリを増やすという対処が見つかった。やり方は、コマンドプロンプト
java -Xmx1024mとすれば1024Mbyteすなわち1Gbyteのメモリを使えるようになるという。自分のマシンにはメモリが16Gbyteあるので、これを2048mとか4096mとかにすると、そんなにメモリは増やせないよと怒られる。
かといって、1024mとすると、コマンドの一覧が表示されて、入力したコマンドが理解されていない様子。
更に検索すると、osmosisで大きなファイルを扱うときにout of memoryエラーを回避するのに、コマンドにtype=hdと加えると良いというのが見つかった。参考サイトでもそうしている。だが、やってみるとさっきよりは進むが結局out of memory を吐いて止まる。
ここで、完全に行き詰ったので、上に書いたようにLinuxを試そうと、WSLのubuntuでやってみたが、書いたようにプラグインが動かせずにこちらも挫折した。
もう、頭が真っ白になって、とりあえずその日は寝ることにしたが、寝ながらあることを思い出した。実は地図のデータを置いていたドライブの容量がかつかつになっていたのだ。
また、いろいろと検索するとjavaには32bit版と64bit版があって、32bit版だと最大メモリが1.5Gbyteくらいに制限されるらしいことも分かった。さらに、Windows環境変数をいじる方法でもjavaのヒープメモリを増やせることが分かった。
とにかく、出来ることはいろいろやってみようと、まずは、インストールされているjavaのバージョンを調べた。すると、32bit版と64bit版の両方がインストールされていた。ややこしいので、32bit版はアンインストールした。この状態でヒープメモリのサイズを調べると4Gbyteに設定されていることが分かった。四国のデータくらいなら4Gbyteあれば実行できるだろうと思ったが、念のため、地図データは空き容量の大きい別のドライブに移して、とりあえずmapファイルの作成ができるかどうかを下記のコマンドを打って実行してみた。
osmosis --read-pbf file="shikoku.osm.pbf" --mapfile-writer file="shikoku.map"
つまり、等高線のないopenstreetmapの四国のデータのみをmap形式に変換してみた。
すると、ちゃんと変換できた。よーしと思って、
osmosis --read-pbf file="shikoku.osm.pbf" --read-pbf file="shikoku_dem10b.osm.pbf" --sort --merge --read-xml file="shikoku_sea.osm" --sort --merge --mapfile-writer file="shikoku.map" tag-conf-file=tags\tags.xml map-start-zoom=10
として変換をやると、あっさりとout of memory でストップ。
ならばと、最後にtype=hdとしてみた。するとしばらくは動くが途中でやっぱりout of memory でストップ。
仕方ないので、最後の手段と思い、環境変数をいじってメモリを増やした。
www.gwtcenter.com
環境変数の設定で、システム環境変数に新規を選択し、変数名を"_JAVA_OPTIONS"、値は"-Xmx6G"として6Gbyteを割り当てた。参考サイトで5Gbyteを割り当ててもtype=hdオプションを使わないとダメだったと書いてあったので。
そして、変換してみると今度はうまくいった。できた地図をAndroid Studioエミュレータで動かすと、見事に等高線と海が表示されていた。

f:id:alasixOsaka:20200509182103j:plain
徳島市付近の地図。海は青く、等高線もある。
ちなみに、テストで変換してみたファイルを使うと海が見事に真っ白だ。
f:id:alasixOsaka:20200509182212j:plain
海が白く表示される。
等高線の違いも山の部分で確認した。
f:id:alasixOsaka:20200509182423j:plain
石鎚山付近。等高線は10m間隔。
f:id:alasixOsaka:20200509182503j:plain
石鎚山付近。等高線は20m間隔。
こうしてみると、登山に使うならやっぱり10m間隔の地図が見やすい。

本州の地図でまた苦労する。

これで終わりかと思いきや、同じやり方で本州もできると思ったが、やはり本州はでかいのか、やっぱりOut of Memoryで途中で落ちた。仕方ないので、メモリーを12Gbyteまで増やして実行したが、今度はbuffer があふれると言って途中で止まった。参考サイトでは、等高線間隔が20mでやっているので本州全体でもできるのか、Windowsだからダメなのか、理由はよくわからない。少なくとも10m間隔ではデータが大きいのは間違いないので、本州のデータを切り取って小さくして作業してみた。
一番欲しいのは近畿の地図なので、Google mapで適当に座標を割り出して、Osmosisでカットした。まずは基本のOpenStreetMapから
Osmosis read-pbf file="honshu.som.pbf" --bounding-box top=35.721237 left=134.256364 bottom=33.400212 right=137.039213 --write-pbf file="kinki.osm.pbf"
そして、等高線も
Osmosis read-pbf file="honshu_dem10b.som.pbf" --bounding-box top=35.721237 left=134.256364 bottom=33.400212 right=137.039213 --write-pbf file="kinki_dem10b.osm.pbf"
そして、海と陸のデータ
Osmosis read-xml file="honshu_sea.osm" --bounding-box top=35.721237 left=134.256364 bottom=33.400212 right=137.039213 --write-pbf file="kinki_sea.osm"
こうやって、3つのファイルを用意して、合体した。
osmosis --read-pbf file="kinki.osm.pbf" --read-xml file="kinki_dem10b.osm" --sort --merge --read-xml file="kinki_sea.osm" --sort --merge --mapfile-writer file="kinki.map" tag-conf-file=tags\tags.xml map-start-zoom=10 comment="%date% made by Yuji Ueno" type=hd
ところがアプリで確認すると海が真っ白。海と陸のデータを切り出したのが悪かったのかもしれない。
そこで、等高線以外のデータを先に合体してから、近畿の部分を切り出して、等高線と合体してみた。これもダメだった。
またまた、試行錯誤が続くが、長くなったので続きは次回に書くことにする。

パン作り その5

前回と前々回はホシノ天然酵母を使ったパン作りを書きました。
alasixosaka.hatenablog.com
alasixosaka.hatenablog.com
今回は、また元に戻って白神こだまの酵母を使ってみました。
今回は、白神こだまGというホームベーカリー用の酵母を使いました。

パイオニア企画 白神こだま酵母ドライG 200g

パイオニア企画 白神こだま酵母ドライG 200g

  • 発売日: 2017/07/25
  • メディア: 食品&飲料
この後ろにGとつくのがホームベーカリー用ということで、予備発酵が不要のものになります。

早焼モード

HBはMKのHB-150という機種を使っています。
”いちばん簡単な白神こだま酵母パンレシピ”という本によると、白神こだま酵母の発酵は2時間くらいが最も活発な時間で、だいたいどのHBでも早焼モードで作るのが良いとのこと。

以前は何も知らずに、HB用でない普通の酵母を使って、うまく膨らみませんでしたが(普通の酵母でも予備発酵をすればちゃんと使えます、ただし予約には不向き)、果たして今回はどうなったか。
結論から言うと、やっぱりうまく膨らみませんでした。理由はよくわかりません。一応予約ではなく、様子を見ながら焼いていたんですが、一次発酵まではしっかり膨らんでいるように見えました。
ところが、最終発酵での膨らみが足りず、ずっしりとしたパンになり、Gであろうがなかろうがあまり結果に差が見られませんでした。
ちなみに、材料は
小麦(春よ恋ブレンド)400g
砂糖 24g
塩 7g
スキムミルク 9g
バター 24g
酵母(白神こだまドライG)7.1g
水 250cc
で、HBの早焼モードで焼きました。

HB+オーブン

今度は、1斤用の食パン型を使って、ねりと1次発酵をHBでそれ以降は手作業+レンジで行ってみました。
材料は
小麦(春よ恋ブレンド)280g
砂糖 16g
塩 4g
スキムミルク 6g
バター 10g
酵母(白神こだまドライG) 6g
水 175cc
膨らみが心配だったので酵母は少し多めにしてみました。
材料を混ぜて、HBのねり+発酵モードで発酵時間は1時間で1次発酵まで。フィンガーテストもOK。
生地を2分割し、ベンチタイム20分。
ベンチタイム後、生地を丸めて食パン型に入れ、レンジのオーブン発酵機能で30℃で二次発酵、最初60分で膨らみが足りず、結局90分でパン型の淵まで膨らむ。
レンジの食パンモードで焼き上げ。
こうすると、うまく膨らんでいい感じの食パンができた。(写真は撮り忘れました)
どうも、2次発酵に十分時間をかけた上げないと奇麗に膨らまないらしいということがわかってきた。

やわらかパンモード

MKのHBには”やわらかパンモード”というのがある。
以前も参考にさせてもらったこちらのブログでは、パネトーネマザーを使ったときに、MKだとやわらかパンモードが良いと書いてある。やわらかパンモードだと通常のモードよりも少し時間がかかるが、長時間の寝かしという工程が入るようだ。
HBに合ったパネトーネマザーの適量を知る,パネトーネマザーのレシピ(酵母が笑う♪天然酵母とホームベーカリー(HB)で作る手作りパンたち♪)
そこで、白神こだまでもこのモードでやったらうまくいくかもということでやってみた。
結果は大正解。パネトーネマザーで焼いたときと同じくらいに奇麗に膨らんだパンができた。

f:id:alasixOsaka:20200419152121j:plain
やわらかパンモードでやってみたら奇麗に膨らんだ
材料の割合は失敗したときと同じ。HBのモードだけ変更した。
味の方も非常においしく、大成功だった。個人的には白神こだまで焼いたパンの味が一番好きですね。

プチパン

ホシノ天然酵母でも焼いていましたが、白神こだまでプチパンを焼いてみました。
HBに1斤分の材料を投入。
小麦:春よ恋ブレンド240g
全粒粉:春よ恋 40g
砂糖:8g
塩:4g
白神こだま酵母G:5g
水:170cc
材料はシンプルです。
これをHBのねり+発酵モードで1次発酵を1時間。
1次発酵後取り出して、10個に分割して丸めて、ベンチタイム20分。
2次発酵はオーブンの発酵機能を使って35℃で30分。
2次発酵終了後、クープを入れて、オーブンのフランスパンモードで焼き上げて完成です。

総まとめ(アンドロイド地図アプリの開発 最終回)

長い間かかった地図アプリの開発ですが、基本的なところは完成しました。
alasixosaka.hatenablog.com

たぶん、細かい不具合などがあると思いますが、それについては使いながら修正していくという感じになるでしょう。
振り返れば、地図アプリを作ろうと思い立ったのが約1年前。
alasixosaka.hatenablog.com
いやあ、いろいろありました。アンドロイドアプリの開発はまったくやったことなくて、初めに本を買うところから始めて、なんとかゴールにたどり着いたという感じです。
ということで、アプリについてまとめてみたいと思います。ソースコードについては、途中の総括のところで載せていますので、今回はソースコードはなしということで書いていきます。

アプリの概略

はっきり言って、世間の一般的な地図アプリと比べるとクソのような仕様です。自分の必要な機能しか搭載していません。できることは、非常に限定的です。ベースはMapsforgeという地図アプリになります。地図のデータはmap形式というのを使います。使っているのはOpenStreetMapですが、地図形式はOSMでなくmapを使っています。これにも深い意味はないのですが、以前から使っているIpBikeというアプリでもMapsforgeの地図を使っていたのと、日本語でブログを書いている方がおられて、その方の記事を参考に開発を始めたのでそうなってしまったというのが実態です。
OpenStreetMapオープンソースの地図で、誰でも自由に使えるのが特徴です。もちろん日本の地図データも無料でダウンロードできます。
Mapsforgeは地図データとして独特のmap形式を使いますが、基本的にはOpenStreetMapを表示するためのアプリ(というかライブラリといったほうがよい)です。ただし、本家のサイトはとっても不親切で、GitHubにソースがアップされていますが、そこのReadMeを読んだだけでは全く使い方がわかりません(私のようなアプリ開発の初心者には)。
github.com

開発初期には日本語でブログを書いている方の記事を参考にまずは地図を表示させるところから始めました。
blog.mori-soft.com
ここのサイトのお陰でなんとか自分でもOpenStreetMapの地図が表示できるようになりました。ただ、本家のサイトに比べて非常にシンプルなソースコードでやっていることはわかりやすかったのですが、それ以上に複雑なことをしようとするとどうしても本家サイトのコードを読む必要があって、そこから進むのに苦労しました。何とかやっていくうちに仕組みが少しずつ分かってきたので欲しい機能を追加していくことができました。
しかし、プログラマーの皆さんって凄いですね。あれだけの情報で自分用のプログラムが書けるのですから。

次はアプリの機能の紹介です。

メイン画面

f:id:alasixOsaka:20200405183648j:plain
アプリを立ち上げたときに最小に表示される画面
まずアプリを立ち上げるとメイン画面が表示されます。まあ、メイン画面というかメニュー画面ですが。普通なら地図がぱっと表示されるところですが、このアプリではメニューが現れます。こういう仕様にしたのは、地図の画面ではできるだけボタン類の表示を減らして最低限にし、地図の部分を多く表示したかったからです。地図画面からはGoogleのコマンドバーの戻るでメイン画面に戻ってきます。

MAP FILE SELECT

一番上のボタンは地図ファイルの選択です。デフォルトはベルリンの地図になっています。あまり意味はなのですが、Mapsforgeのサンプルプログラムがベルリンの地図を表示するようになっているのでそのまま使っているのと、日本の地図は日本で一つのファイルになっていてサイズが1GBを越える巨大なサイズになっているので、あまり大きなファイルをデフォルトにするのもどうかと思ってそのままにしてあります。
ベルリンの地図はなくても大きな問題になりませんが、そのままだと地図が表示されないのとエラーになる(たぶん)ので、何らかの地図ファイルが必要です。ファイルのダウンロードは次のところからできます。
download.mapsforge.org
また、等高線付きの地図は次のサイトからダウンロードができる。(等高線の表示のしかたは下の方を見て下さい)
https://www.openandromaps.org/en
地図ファイルは端末のどこにおいても構わないですが、後で出てくる他のファイルと同じところに置いておく必要があります。私は、SDカードのルートディレクトリに置いています。
MAP FILE SELECTをタップしたら、ファイルマネジャーを選ぶ画面が出るので、OIファイルマネージャーを使って該当の地図ファイルを選択します。OIファイルマネージャーはあらかじめインストールしておく必要があります。別のファイルマネージャーも使えると思いますが、試していません。

GPX FILE SELECT

このボタンをタップするとGPXファイルを選択する画面になります。拡張子がGPXのファイルだけを選択できるようにしてあります。下位フォルダにはフォルダ名をタップすればたどっていけますが、上位フォルダには行けない(と思い)ます。デフォルトは地図ファイルの置いてあるディレクトリになるので同じディレクトリに置いておくのが無難です。GPXファイルはGaeminConnectやカシミール3D、ridewithGPSなどで作成したルートを利用できます。あらがじめ自分が行きたいルートを決めておいて、アプリに取り込んで表示させるためのものです。アプリ上では青色の線で表示されます。後で書きますが、高低図を表示させるためには高度の情報が必要ですので、例えばカシミール3Dで無料の地図である地理院地図を使ったりすると標高データが含まれていませんので、高低図が表示できないことになります。
GPXファイルのデフォルトはnull.gpxというファイル名になっています。null.GPXは中身が何もない空のファイルです、ただすっからかんだとファイルを開くときにエラーになるので何でもよいので最低一文字は入れておく必要があります。メモ帳なので適当にファイルを作って後で拡張子をGPXとすれば出来上がりです。GPXファイルは一度選択するとそのファイルのデータをいつまでも表示し続ける仕様になっているので、ルートを消去したい時には選択画面でnull.GPXを選択するととで消去することができます。

f:id:alasixOsaka:20190907113200p:plain
GPXファイルのルートを青く表示する。

POI FILE SELECT

これもGPX FILE SELECTと同じです。タップすると選択画面が現れて、拡張子がPOIのファイルを選択するようになります。POIファイルはPoint Of Interestの略で、地図上にアイコンを表示するのに使っています。自分の場合は、交差点を左右に曲がるポイントに矢印マークを表示するのに使っています(自分で好きなアイコンを追加することができますが今は矢印のみです)。自転車で走っていると曲がるポイントを行き過ぎてしまうということがよくあるので、この機能は絶対に必要と思っています。GarminのEdge800などのサイコンには矢印とか様々なアイコンを表示する機能があるのですが、残念ながら画面が小さくて老眼になってからは見づらくて使っていません。その後スマホをサイコン代わりに使えるIpBikeというアプリをメインに使っていましたが、IpBikeにはアイコンを表示する機能がないので残念に思っていました。POIファイルはカシミール3Dでアイコンを表示したいところにウェイポイント作成していってGPXファイルとして出力し、拡張子をPOIに変更することで使っています。

f:id:alasixOsaka:20200302163139j:plain
POIファイルで矢印を表示
POIも表示を消すためにはnull.POIというファイルを用意しておいて、これを選択することで消去するようにしています。

MAP VIEW

MAP VIEWをタップすると地図が表示されます。このボタンはその下のラジオボタンと連動していて、地図表示の形式をラジオボタンの選択で切り替えるようになっています。
ラジオボタンの内、上の行は、地図の回転と現在地の表示のOn/Offを切り替えます。Onにすると地磁気センサーと加速度センサーを使って端末の向きを割り出して、端末の向きに応じて地図を回転させます。と同時にGPSから得られた現在地点を地図の中心にしてそのポイントに水色の丸を表示します。

f:id:alasixOsaka:20190929094938p:plain
現在地点の表示
Offにすると地磁気センサーと加速度センサーはオフになり、地図の表示も北が上の表示になります。また、現在地点を表す水色の丸も表示されなくなります。
ラジオボタンの下の行は高低図表示のOn/Offを切り替えます。Onにすると高低図を表示し、Offにすると表示しません。高低図は坂を上っているときにあとどのくらい登りが続くのかわかるので絶対欲しい機能の一つでした。また、上の行の回転のOn/Offによって表示が変わり、Onの場合、高低図にも現在地点が水色の丸で表示されます。また、高低図の横軸のフルスケールは10㎞となり、現在地点に応じて横方向にスクロールします。
f:id:alasixOsaka:20200429121059j:plain
高低図にも現在地を表示
Offにすると現在地点の表示はなくなり、高低図の横軸スケールはルートの全体となり、全体像を見ることができます。
f:id:alasixOsaka:20200412173322j:plain
高低図の全体像を表示
ちなみに、高低図の縦軸はそのルートの最高点にオートスケールされます。ただし、最高点が500m以下の場合は拡大されすぎて見にくいので縦軸のフルスケールは500mになります。同様の理由で、横軸もコース全体の長さが10㎞以下ならフルスケールは10㎞になります。

SELECT THEME

これは地図の表示形式を定義したテーマファイルを読み込むためのボタンで、何も選択しないとデフォルトのテーマが選択されます。デフォルトテーマでは、たとえ等高線のデータ含まれている地図ファイルを選択しても等高線が表示されません。等高線を表示させるためにはそれに応じたテーマファイルを読み込む必要があります。
先に紹介したOpenAndroMapのサイトにテーマをダウンロードするリンクがあります。図の赤丸の部分をクリックするとElevate.ZIPというファイルがダウンロードされるのでそれを適当なフォルダに解凍し、その中にあるElevate.XMLというファイルを端末の地図ファイルと同じディレクトリにコピーします。

f:id:alasixOsaka:20200502145812j:plain
赤丸の部分をクリックするとテーマファイルがダウンロードされる
アプリの選択画面でLocusをタップするとこのElevate.XMLをテーマとして使用する設定になり等高線が表示されます。実はこのファイルはLocusMapというアプリで等高線を表示させるためのものですが、LocusMapはmap形式に対応しているので、Mapsforgeでも使用することができました。

バックグランドでの動作

アプリでMapViewをタップすると地図が表示されますが、どのオプションを選んでいてもバックグランドでの動作が可能なモードに入ります。バックグランドでアプリが動作中は左上のバーにコンパスのアイコンが表示されます。MapViewからホームボタンでホーム画面に戻ってもアプリ自体は動作を続けており、アイコンも表示されたままになります。スリープモードに入ってもアプリは動作し続けるので消費電力は大きくなります。これは、GPSでの位置を見失わないための処理で、バックグランド動作をしないと、スリープから復帰したときに端末が現在地を見失っており、GPSを掴むのにものすごく時間がかかるため、仕方なくバックグランドで動作するようにしました。バックグランド動作を止めるためには、地図画面でコマンドバーの戻るボタンをタップしてメイン画面に戻れば止まります。と同時にアイコンも消去されます。このアイコンはAndroid8では強制的に消去することはできませんが、Andorid7以前では上のバーをスワイプして通知画面を拡大して、通知を消去すれば消えてしまいます。ただ、通知が消えているだけでバックグランド動作は継続していますので注意が必要です。
冒頭の部分で書き忘れましたが、このアプリはGPSが動作していることが前提として作成されています。アプリ自身ではGPSのOn/OffをコントロールしていないのであらかじめGPSをオンにしておく必要があります。また、GPSはバックグランド動作にかかわらずオンの状態になるので(端末の設定画面から強制的にオフにすれば別ですが)、それなりに電力を消費します。

軌跡の表示

赤色でいままで通ったところの軌跡を表示します。これは、当初の構想にはなかった機能ですが、現在地点の表示が単なる丸印で方向がわからないので、これから行く方向と今まで通ってきた方向がすぐわかるように、軌跡を表示するようにしました。軌跡もバックグランドでも動作していますので、スリープに入っても軌跡は途切れることなく表示されます。また、軌跡はメイン画面に戻れば消去されます。そこから再びMapViewをタップして地図画面に戻った場合、そこから軌跡を描き始めます。

f:id:alasixOsaka:20200430142311j:plain
赤色で軌跡を表示。バックグランドでも描き続ける。

これからのこと

アプリはできましたが、バグが残っていることは十分に予想されます。バグとりは実際に使ってみないとわからない部分も多いので使っている中で改良していくという作業になりますが、コロナの影響で色々なところに行けないので気長にやるしかなさそうです。
ログ機能は搭載していません。GarminGPSウォッチをいつも使っているのでGPSのログはそっちで取るので必要性を感じていないからです。軌跡を描いているのでそのタイミングでファイルに書き込みを行っていけばログファイルを作ることができますが、そうするとどこでログをストップするかを考えて実装する必要が出てきます。現状では軌跡を消去するのにメイン画面に戻る方法を使っているのですが、そうでなくてログを止める別のボタンをメイン画面に追加するとかの方法も考えられます。ただ、そうするとバックグラウンド動作もそこでストップするといった処理が必要になり、結構ややこしいことになってしまいます。なのであまりやる気がありません。
また、等高線付きの地図ですが、ダウンロードできるmap形式の地図は等高線間隔が20mのものしかなく、10m間隔のものが使いたければ自分で作るしかなさそうです。自転車の場合はいいですが、登山やトレランなどにこのアプリを使おうと思ったら細かい地形を読むためには等高線間隔は10mくらいの方が読みやすいのでやる価値はあると思いますが手間との相談ですね。
Gamin用の地図をOpenStreetMapをベースに作っている人がいるのでそのやり方を参考にすればできそうですが、結構手間がかかるので気が向いたらやってみようかと思っています。ちなみにオリエンテーリングでは等高線間隔は標準で5m間隔です。これくらいでないと細かい地形が読みきれません。まあ、地図の縮尺も1:10000とかがメインなのでそうしないと等高線がスカスカになってしまいますけど。
その辺はやってみてうまくいったらまたブログに書いてみたいと思います。

ニュートンのMotion9を買いました。

普段のランに使っているシューズはメインがHokaOneOneのクリフトン5で、もう一足ニューバランスのシューズをサブで使っているのだけれど、これが今一つなので、新しいシューズが欲しいと思っていたところ、ランネットショップからの宣伝メールに触発されてニュートンのMotion9を買ってみた。この時期、お店に行くのも何なので、ネットショッピングで購入。
ニュートンというメーカーはそれまで聞いたことがなかったし、もちろん履いたこともなかったのでちょっと心配だったが、幸い自分の足に合ったので早速試しに走ってみた。


ニュートンのシューズは低ドロップが特徴で、フォアフットランには最適とのこと。ニュートンシューズのラインナップは基本的に2系統あって、スタビリティシリーズとニュートラルシリーズがあるとのこと。どちらが向いているかというのを簡単に見分けるのはかかとの減り具合で見分けることができるらしい。
recreation.pintoru.com
自分の場合は外側が減りやすいのでニュートラルシリーズが良いらしい。ニュートラルシリーズではディスタンスとモーションが該当するらしい。ディスタンスは軽量でどちらかというと早く走るのに向いた靴で、モーションはもう少し初心者向けといったところか。スピードはそんなに求めてないのでモーションを買うことにした。モーションの最新版はMotion9で、ステップスポーツさんから購入。実店舗は高槻にもあるけど、先にも書いたようになんせステイホームなので、ネットで購入。
ニュートンシューズのもう一つの特徴は、ちょうどフォアフットで着地するあたりに貼ってある黒いラバー。ここが出っ張っているので始めに履いたときはかなり違和感を感じた。注意書きにも最初はいきなり長距離を走らず、履きならしをしましょうというようなことが書いてある。
ということで、近所のランニングコースを7㎞ほど走ってみた。
気になるラバーの方は、平地をリズムよく走っているときはほとんど気にならない。着地するときに衝撃を吸収してくれるからだろうか? だが、少しスピードを落としたり、坂になったりすると少し違和感を感じる。具体的には何か石のようなものがシューズに挟まっているような気がする。止まって足の裏を見ても何もないので、これはソールのせいかと気づいた。
それ以外は特に気になることもなく、しいてあげれば少しストライドが大きくなる感じがするくらいか。これもソールに貼ってあるラバーがばねのような感じで効いているからだろう。

ついでにアプリの実機での検証も

開発中のアンドロイド地図アプリがようやく予定の機能を搭載できたので、XperiaZ4を持ってちゃんと動作するか確かめてみた。走っている間はスクリーンをオフにしてスリープモードにしておいたが、しっかり軌跡が記録されていて、バックグランドでもちゃんと動作していることが分かった。

高低図に現在地を表示する。(アンドロイド地図アプリの開発 その24)

GWになりましたが、ステイホームということであまりやることがありません。
Andoroidアプリの開発もいよいよ大詰めになってきましたので、完成してしまいたいと思います。

前回は高低図の全体を表示するとこまでやりました。
alasixosaka.hatenablog.com

今回は、高低図に現在地を表示するということやってみたいと思います。
その前に、前回はSDカードからGPXファイルを読み込んで解析したデータをグローバル変数に放り込んでいましたが、よくよく考えると、グローバル変数にする意味がないので、ローカル変数に変更しました。
グローバル変数を定義しているSamplesApplication.javaの冒頭のところでArrayListを定義している部分をコメントアウトし、RotateMapViewer.javaの冒頭に同じ記述を書いてこちらでArrayListを定義します。
グローバル変数は地図の回転のOn/Offを表すsetGPSと高低図の表示をOn/OffするsetHeightの2つになります。

public class SamplesApplication extends Application {

    public static final String TAG = "Mapsforge Samples";

   ~省略~

    boolean setGPS;
    boolean setHeight;
    
    //ArrayList<Double> GPXs = new ArrayList<>();

Viewを切り替える

前回の宿題になっていた、高低図ありのViewとなしのViewを切り替える作業です。
アプリの中でViewを動的に切り替えるわけではないので、”Map View”ボタンをクリックしたときに、高低図のありかなしかを判断して、それぞれのViewを割り当てるということをやればできることになります。
Viewはレイアウトファイルで定義されるので、以前の高低図のないレイアウトファイルをもう一つ用意して、高低図を表示しない場合はこちらを使うようにします。レイアウトファイルは"rotatemapviewer2.xml"としました。

SetContentViewがみあたらない

クラスとレイアウトファイルを関連付けるのは、通常はsetContentView(R.layout.xxxx)のように使います。ところが、MapViewをつかさどる”RotateMapViewer.java"にはsetContentViewがありません。その代わりと言っては何ですがgetLayoutIDコマンドに

    @Override
    protected int getLayoutId() {
        return R.layout.rotatemapviewer;
    }

という記述がありました。この部分が唯一レイアウトファイルについて書いてある部分なので、ここを次のように変更しました。setHeightがTrueの場合はrotatemapviewerを使い、Falseの場合はrotatemapviewer2を使います。

    @Override
    protected int getLayoutId() {
        if(globals.setHeight){
            return R.layout.rotatemapviewer;
        }else {
            return R.layout.rotatemapviewer2;
        }
        //return R.layout.rotatemapviewer;
    }

グローバル変数が機能しない。

ところが、この部分でエラーになってしまい、デバッグするとグローバル変数としてglobals.setHeightを実行するとnullとなってしまい、グローバル変数が機能していないことがわかりました。
デバッグで色々調べると、このRotateMapViewer.javaにはOnStartはあってもOnCreateがなく、レイアウトファイルの紐づけはOnStart以前に行っているらしくOnCreateを作って、そこの部分でグローバル変数を設定してやればうまくいくことがわかりました。また、globals = (SamplesApplication) getApplication()がsuper.onCreate(savedInstanceState)の前に書いてありますが、この部分は重要で、この順番が逆だとやっぱりグローバル変数が定義されないままでレイアウトファイルの紐づけに失敗します。理由はよくわかりませんが(笑)。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        globals = (SamplesApplication) getApplication();
        super.onCreate(savedInstanceState);

    }

これでうまくいくはずだったのですが、しょうもないことでいろいろ苦労したので、備忘録として書いておきます。
結論から言うと、rotatemapviewer2.xmlを昔のプロジェクトからコピペしたために、昔のプロジェクト名が残ったままになっていてここで、そんなのはないよというエラーになっていました。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mainView"
    
    ~省略~

    <com.example.XXXXX.MapScaleBarView
        android:id="@+id/mapScaleBarView"
        
 ~省略~

com.example.XXXXX.MapScaleBarViewのXXXXXの部分がコピペした元の古いプロジェクト名になっていたのがエラーの原因でした。ここを新しいプロジェクト名にすることでエラーが解消し無事にViewの切り替えができるようになりました。
それがわからずに、別のクラスを用意してそれぞれのクラスでレイアウトファイルの紐づけをやってみたりしましたが、全然エラーが解消せず、かなり悩みました。デバッグモードでエラーのコメントをよく読めばすぐに分かったことなんですが、エラーに焦ってしまって冷静にコメントまでしっかり読まず、あれこれ思いついたことをやったのがかえってよくありませんでした。エラーメッセージのコメントはちゃんと読むべきだと反省しています。

現在地が更新されたら高低図も更新する

さてここからは、いよいよ高低図の本体の処理の部分になります。
まず、現在地が更新された時に高低図を書き換えないといけないので、"OverlayMapViewer.java"にその処理を追記します。具体的にはdrawUserPositionMarkerの最後にif (globals.setHeight){ HeightMap2(); }と高低図の表示がOnならHeightMap2()を呼び出します。

     private void drawUserPositionMarker(Location location){
        if (globals.setGPS==true) {
            Latitude = location.getLatitude();
            Longitude = location.getLongitude();
            
   ~省略~

            this.mapView.setCenter(new LatLong(Latitude, Longitude));
            if (globals.setHeight){
                HeightMap2();
            }        

HeightMap2()は、

    private void HeightMap2() {
        ((PaintView)findViewById(R.id.height)).map(GPXs,elvmax,globals.setGPS,Latitude,Longitude);
    }

PaintView.javaのmap関数を呼び出します。引数はArrayListのGPXs、最高高度のelvmax、ここまでは、前回のHeightMap()と同じ、ここにglobals.setGPSと経度(Latitude)、緯度(Longitude)を加えています。呼び出し関数は同じですが、Javaでは引数の数を認識して適切なルーチンに飛ぶという機能があるので変数名は同じmapで問題ありません。
また、globals.setGPSは地図の回転表示と現在地表示のOn/Offを表すグローバル変数なのでこれで、高低図にも現在地表示の有り無しを伝えます。したがって、前回作製したHeightMap()にも引数としてglobals.setGPSを加えています。したがって、HeigtMapでは引数が3つでmapを呼び出し、HeightMap2では引数が5つでmapを呼び出しています。受け側のmapは次のようになります。単純に引数を変数に入れなおしてinvalidate()を実行して高低図を書き直すだけです。

 public void map(ArrayList arraylist,double emax,boolean GPS){
        arrays = arraylist;
        elvmax = emax;
        setGPS = GPS;
        invalidate();
    }

    public void map(ArrayList arrayList,double emax,boolean GPS, double Lati, double Longi){
        arrays = arrayList;
        elvmax = emax;
        setGPS = GPS;
        lat = Lati;
        lon = Longi;
        invalidate();
    }

現在地点の算出

GPSから現在地の緯度、経度が送られてきますが、これをもとに高低図のどの位置にいるかを算出し、プロットしなくてはいけません。高低図は横軸がスタート地点からの距離、縦軸が高度になっているので緯度、経度の情報を持っていません。しかし、arraysには、緯度、経度の情報のあるので、これをもとに計算します。arraysの中身はGPXファイルの各地点の緯度、経度、通算距離、標高がそれぞれ順番に格納されています。
そこで、面倒ですが、現在地の緯度、経度と各地点の緯度、経度から2点間の距離を計算し、その距離が最も小さい地点が現在地と推定することにします。この場合九十九折れの道などでGPSの精度が低いと現在地を誤認する可能性がありますが、この辺りは実際に使ってみてどの程度問題になるかを見てから対応を考えたいと思います。具体的な処理は次のようになります。ループ変数をiとし、lat1、lon1にGPXファイルの緯度、経度を取ってきて、GPSの現在地点の緯度、経度であるlat、lonとの距離を計算しています。計算ルーチンはGPXファイルから通算距離を計算したときと同じくライブラリを使っています。結果はdisに格納され、mdisと比較し、disがmdisよりも小さい場合はmdisとNposを更新します。結果的にNposが最も近いポイント、mdisがその距離になります。

           for (int i = 0 ; i < arrays.size()/4 ; i++) {
                double lat1 = arrays.get(i * 4);
                double lon1 = arrays.get(i * 4 + 1);
                float[] dis = getDistance(lat1, lon1, lat, lon);
                if (dis[0] < mdis) {
                    mdis = dis[0];
                    Npos = i;
                }
            }

現在地点のスタートから距離をNx、高度をNyとしてarraysからとってきます。

double Nx=arrays.get(Npos*4+2);
double Ny= arrays.get(Npos*4+3);

基本的にはこのNx、Nyを使って高低図上に現在地をプロットすれば良いことになります。

高低図のシフト

だだし、現在地を表示する場合は、高低図の全体をフルスケールにすると距離が長い場合に見づらくなるので、フルスケールを10㎞としています。
そうすると、現在地点に応じて高低図も動かさないとどんどん進んでいって10㎞を越えてしまうと現在地点が表示されなくなるので、高低図をシフトさせる処理を行います。
シフトの考え方は次のようにしました。

  • 現在地点がスタートから3㎞まではシフトしない。
  • ゴール地点が表示されるまでシフトしたらそれ以上はシフトしない。

そうすると、シフトが起こるのは、現在地点がスタート地点から3㎞以上でゴールまでの残り距離が7㎞以上ある場合になります。具体的な計算は下記のようになります。シフト量の単位は㎞として、変数ofsetに代入しています。

            //3㎞以上進んでいて残りが7㎞以上の時は進んだ距離-3㎞シフト
            if ((Nx > 3)&&((Tdis-Nx)>7)){
                ofset = Nx - 3;
                //進んだ距離が3㎞以下ならシフトはゼロ
            }else if (Nx <=3){
                ofset=0;
                //残りの距離が7㎞以下ならシフトをストップ
            }else {
                ofset = Tdis - 10;
            }

現在地点のプロット

これで現在地点のプロット位置を計算することができるようになったので、円をプロットします。だだし、迷ったり、コースを外れたりしたときに、全く見当はずれな位置にプロットするのは間抜けなので、一応コースとの距離が300m以内ならプロットすることにしました。プロット位置の計算は、int Cx = (int)((Nx-ofset)/10*cwidth)、int Cy = (int)(cheight-Ny/ymax*cheight)で求めています。プロットは外周が黒で、シアンで塗りつぶした円にしました。

            //ルート上の最近接地点と現在地の距離が300m以下なら円をプロットする
            if(mdis<300){
                int Cx = (int)((Nx-ofset)/10*cwidth);
                int Cy = (int)(cheight-Ny/ymax*cheight);
                paint.setColor(Color.CYAN);
                paint.setStyle(Paint.Style.FILL);
                canvas.drawCircle(Cx,Cy,20,paint);
                paint.setColor(Color.BLACK);
                paint.setStyle(Paint.Style.STROKE);
                paint.setStrokeWidth(4);
                canvas.drawCircle(Cx,Cy,20,paint);
            }

軸線もシフトする

これで一応高低図に現在地がプロットできるようになったのですが、縦の軸線は5㎞おきに描く事にしたので、フルスケールが10㎞だとセンターに線が表示されただけになってなんだか間抜けな感じがします。そこで、軸線もシフトするようにしました。現在地点に応じて軸線をシフトし、例えば、15㎞地点付近にいるとかをわかるようにしました。具体的な計算は次のようになります。ループ変数をiとし、float X = (float) (Nx - ofset - Nx % 5 + 5*i)で軸線のプロット位置を計算しています。iは-1から3まで計算し、Xが0から10の間、すなわち横軸のフルスケールのどこかに入った場合はプロットします。

            for (int i=-1;i<3;i++) {
                float X = (float) (Nx - ofset - Nx % 5 + 5*i);
                if ((X < 10)&&(X > 0)) {
                    canvas.drawLine(X / 10 * cwidth, 0, X / 10 * cwidth, cheight, paint);
                }
            }

こんな感じに表示されます。

f:id:alasixOsaka:20200429121059j:plain
現在地を高低図にプロット

実はバックグランドで動作してなかった。

高低図に現在地を表示することはできたが、動作確認の段階でバックグランド動作が機能していないことがわかりました。android studio をアップデートしたらエミュレータでもスリープモードが機能するようになって、スリープモードでエミュレータを使ってGPXファイルを読み込んで動かして見ると、軌跡がスリープしている間の分飛んでいる。android 8 からバックグランド動作に対する規制が厳しくなったので、サービスを設定して動かすだけではスリープするとアプリが停止するようだ。

f:id:alasixOsaka:20200430140533j:plain
スリープしている間の軌跡が飛んでしまっている。
startService でなく、startForegroundService コマンドを使い、5秒以内にStartForeground を実行しないとダメらしい。エミュレータandroid 8 になっているのでバックグランド動作ができてなかったようだ。実機の方はandroid 6 なので気にしなくてもいいことはいいが、将来、端末を交換したときのことを考えてandroid 8 にも対応させて見る。また、StartForegroundServiceはNotificationとセットになっているので、Notificationも実装する必要がある。NotificationもAndoroid 8から変更になっていて、チャンネルというのを使わないといけないことになっている。いろいろとめんどくさいが、Andoroidのバージョンを確認して処理を切り分けないといけない。
LocationService.javaのonStartCommandを下記のように変更した。Andoroidのバージョンを確認し、26以上ならチャンネルを使う設定に、25以下ならチャンネルを使わない設定にした。26以上の場合は、Notificationを事項た後に、StartForegroundを実行している。

public int onStartCommand(Intent i, int flags, int startId) {
        super.onStartCommand(i, flags, startId);


        int requestCode = 0;
        String channelId = "default";
        String title = "MyMap実行中";

        PendingIntent pendingIntent =
                PendingIntent.getActivity(context, requestCode,
                        i, PendingIntent.FLAG_UPDATE_CURRENT);


        NotificationManager notificationManager =
                (NotificationManager)context.
                        getSystemService(Context.NOTIFICATION_SERVICE);
        if (Build.VERSION.SDK_INT>=26) {
            NotificationChannel channel = new NotificationChannel(
                    channelId, title, NotificationManager.IMPORTANCE_DEFAULT);

            if (notificationManager != null) {
                notificationManager.createNotificationChannel(channel);
                Notification notification = new Notification.Builder(context, channelId)
                        .setContentTitle(title)
                        // android標準アイコンのコンパスを設定
                        .setSmallIcon(android.R.drawable.ic_menu_compass)
                        .setContentText("GPS")
                        .setAutoCancel(true)
                        .setContentIntent(pendingIntent)
                        .setWhen(System.currentTimeMillis())
                        .build();

                // startForeground
                startForeground(1, notification);
            }
        }else{
            NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
                    .setSmallIcon(android.R.drawable.ic_menu_compass)
                    .setContentTitle(title)
                    .setContentText("GPS")
                    .setAutoCancel(true)
                    .setContentIntent(pendingIntent);

            int mNotificationId = 001;
            notificationManager.notify(mNotificationId, notificationBuilder.build());
        }

        return Service.START_STICKY;

    }

アイコンを消去する

ところがこのままだと、アイコンがずっと出っ放しになっている。Android 8以降の場合、バインドを解除してサービスとストップするとアイコンは消えるようだ。そこで、RotateMapViewer.javaのonDestroyに処理を追加した。Intent intent = new Intent(getApplication(), LocationService.class)でLocationServiceのIntentを発行し、this.getApplication().unbindService(serviceConnection)でバインドを解除した。その上で、 stopService(intent)でサービスをストップしている。ところが、Andoroid 6(実機のXperiaZ4)では、この方法ではアイコンが消えないので、更に NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)とmNotificationManager.cancelAll()でアイコンを消去している。
ちなみに、Andoroid 6では、バーを下に引っ張ってアイコンを消去するボタンをタップすると、他のアイコンと一緒に消去されてしまう。消えないようにする方法もあるみたいだが、とりあえすこれで動きそうなのでこのままにしておくことにした。
この処理は、onDestroyに置いておかないと正しく動作しない。始めは、onStopに置いたら、スクリーンオフでスリープに入った時に処理が実行されてバックグラウンド動作がストップしてうまくいかなかった。

   public void onDestroy() {

        try {
            if (locationUpdateReceiver != null) {
                unregisterReceiver(locationUpdateReceiver);
            }

        } catch (IllegalArgumentException ex) {
            ex.printStackTrace();
        }
        Intent intent = new Intent(getApplication(), LocationService.class);
        this.getApplication().unbindService(serviceConnection);
        stopService(intent);

        NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationManager.cancelAll();

        super.onDestroy();
   }

マップ表示を終了してメイン画面(メニュー画面)に戻るとバックグラウンド動作が終了し、アイコンも消える。軌跡も消える。という動作になった。

f:id:alasixOsaka:20200430142311j:plain
バックグラウンドでも軌跡が書けている。
また、動作確認中に不具合がわかったので修正を行った。アプリを起動して、例えば、高低図も回転もオフにして地図を表示し、一旦メニュー画面に戻って、今度は高低図と回転をオンにすると、アプリがクラッシュすることが分かった。デバッグで確認するとRotateMapViewer.javaでHeightMap2コマンドの部分で、”on a null object reference”というエラーが出ている。これも結構悩んだ。中身のない変数を使っているよということらしいが、デバッガで見る限り、引数として使っている、GPXs、elvmax、globals.setGPS、Latitude、Longitudeはすべて値が格納されている。結局、エラーの原因はR.id.heightだったが、その前に、HeigtMap関数を呼んでいていこっちはエラーになっていないので、まさかR.id.heightが原因だとは思いもよらなかった。PaintView paintViewを始めに宣言して変数にしておいて、addOverlayLayerの最後の部分でHeigtMapを呼び出しているところで、paintView = findViewById(R.id.height)として一旦、paintViewにR.id.heightを格納してみた。すると、HeightMap2のところでpaintViewがnullの状態で呼び出されていることが分かった。

protected void addOverlayLayers(Layers layers) {

        SharedPreferences preferences = getSharedPreferences("DATA", MODE_PRIVATE);
        GPX_FILE = preferences.getString("gpx", "null.gpx");
        POI_FILE = preferences.getString("poi", "null.poi");
        Path = preferences.getString("path", sdPath);
        
  ~省略~

        layers.add(tappableCircle);
        paintView = findViewById(R.id.height);
        if (globals.setHeight){
            HeightMap();
        }
    }

深く追求するのもめんどくさいので、paintViewがnullなら処理をスキップするようにしたら、クラッシュすることなく動作するようになった。

    private void HeightMap2() {
        //((PaintView)findViewById(R.id.height)).map(GPXs,elvmax,globals.setGPS,Latitude,Longitude);
        if (paintView != null) {
            paintView.map(GPXs, elvmax, globals.setGPS, Latitude, Longitude);

        }
    }

これで、ようやくアプリが完成した(と思う)。あとは実機での検証で不具合があるかどうかを確認してみないといけないが。

参考にしたサイト
[Android] バックグラウンドでGPSログを取り続けるには
[Android] Service の使い方
サービスとNotification - 愚鈍人
Notificationを勉強し直す | Simple is Best
Android Oreoでサービスを使ってみる - Qiita
Android でバックグラウンド(Service)処理(Android8対応) – エンジニアブログ
[Android] Serviceクラス(bindService) - Qiita
[Android]バックグラウンドでセンサーなどのログを取得し続けるには | むあーるの雑記
android: 通知をAPI=26に対応させるには | Ninton
Androidアプリ開発 ステータスバーにNotificationを表示する プログラミングJava
【Android】 Notificationを実装する 【開発メモ】 : 明日のために、今日できることを。

P.S. 2020/5/1追記
一部不具合があったので修正しました。
GPXファイルを表示させない場合、null.gpxという空のファイルを読み込ませてルートを消去していますが、null.gpxを読み込んだ場合に、高低図がOnで現在地表示がOnのとき、無いデータを読み込もうとしてエラーになる不具合がありました。PaintView.javaの該当部分を下記のように変更し、エラーを回避するようにしました。現在表示がOnで現在地点データがある場合の判断に、idx>4を加えています。idxはarrayListの要素数で、GPXデータから解析した、緯度、経度、通算距離、高度が各地点で順番に格納されています。1点でもデータがあれば要素数が4になりますので、要素数が4より大きい場合を判断してデータがある場合のみプロットするようにしています。

//現在地表示がOnで現在地点データがある場合
        if ((setGPS)&&(lat+lon!=0)&&(idx>4)){

            for (int i = 0 ; i < arrays.size()/4 ; i++) {
                ~省略~

また、Andorid8の実端末AQUOS SENSE PLUSで試して気づいたのですが、アプリ側からの設定ではNotificationの通知音をどうやっても消せませんでした。一応、音を消すという設定になっているはずですが、音が鳴るのでいろいろなサイトを調べて試してみましたがどれも効果がなく音が鳴ってしまいました。最終的には端末側の設定で、アプリと通知の部分からアプリを選択し、通知の設定を変更することで音が鳴らないようにできました。今のところこれしか方法が無いようです。

高低図の全体表示(アンドロイド地図アプリの開発 その23)

前回はViewを分割して高低図の領域を作成しました。
alasixosaka.hatenablog.com


今回は、ここにGPXファイルから読み込んだ位置と標高のデータから高低図をプロットしてみたいと思います。
とりあえず、今のところ考えている機能としては、GPSの現在地を地図上に表示する場合は、高低図にも現在地を表示しようと思っています。その時は、高低図の横軸スケールは10㎞にしようと思っています。
GPSの現在地表示をしない場合、全体像を把握するため、横軸はコース全体の距離をスケールにしようと思っています。
縦軸に関しては、両方とも共通で、そのコースの最大標高に合わせようと思っています。まあ、そのあたりは使いながら使い勝手を見て必要があれば変更していこうと思っています。

GPXファイルから距離と標高を取ってくる。

今までは、GPXファイルから取り出すデータは、緯度と経度だけでしたが、GPXファイルには形式にもよりますが、標高が記載されています。
例えば、カシミール3Dで作成したGPXファイルは次のようになっています。

<?xml version="1.0" encoding="UTF-8"?>
<gpx
version="1.1"
creator="Kashmir3D 9.340 - http://www.kashmir3d.com"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:kashmir3d="http://www.kashmir3d.com/namespace/kashmir3d"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd
	http://www.kashmir3d.com/namespace/kashmir3d http://www.kashmir3d.com/namespace/kashmir3d.xsd">
<trk>
 <name>test4</name>
 <number>1</number>
 <extensions>
 <kashmir3d:line_color>0000ff</kashmir3d:line_color>
 <kashmir3d:line_size>2</kashmir3d:line_size>
 <kashmir3d:line_style>1</kashmir3d:line_style>
 <kashmir3d:icon>901001</kashmir3d:icon>
 </extensions>
<trkseg>
<trkpt lat="34.849862" lon="135.591704">
 <ele>-0.001000</ele>
 <time>2020-01-13T04:23:41Z</time>
</trkpt>
<trkpt lat="34.850909" lon="135.591318">
 <ele>-0.001000</ele>
 <time>2020-01-13T04:23:51Z</time>
</trkpt>

始めの方にぐじゃぐじゃと色々書いてありますが、肝心なのは"<trkseg>"以下の部分で、初めに緯度と経度が”<trkpt” 以下のlat="xxx"とlon="xxx"に記載されています。
今までは、この部分だけを読んでいました。標高は次の”<ele>”の部分に書かれています。
まず、グローバル変数としてArrayListを作成し名前をGPXsとします。そこに緯度、経度、スタートからの距離、標高の順番に記録していきます。
グローバル変数は、SamplesApplication.javaで定義しますので、ここの変数リストに

ArrayList<Double> GPXs = new ArrayList<>();

を加えてArrayListのGPXsをグローバル変数として作成します。

次に、OverlayMapViewer.javaのGPXファイル読み込みのルーチンを書き換えます。
まず、変数を追加します。

    private double Olati = 0;
    private double Olongi = 0;
    private double Tdistance = 0;
    private double elv = 0;
    private double elvmax=0;

Olatiは一つ前の地点の緯度、Olongiは一つ前の地点の経度で、距離の計算に使用します。Tdistanceはスタート地点からの通算の距離で高低図の横軸に使います。elvは標高、elvmaxはコースの最高標高です。
次に、GPXファイルの解析の部分に手を加えて通算距離を計算します。

//GPXファイルの解析
        try {
            String listXmlPath = Path + "/"+ GPX_FILE;
            is = new FileInputStream(new File(listXmlPath));
            BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
            
            //XMLファイルをまとめて読み込み
            XmlPullParser xpp = Xml.newPullParser();
            xpp.setInput(reader);
            //解析するXMLファイルの中身を渡す
            int eventType = xpp.getEventType();
            while (eventType != XmlPullParser.END_DOCUMENT) {
                switch (eventType) {
                    case XmlPullParser.START_DOCUMENT:
                        Log.i("MainActivity", "ドキュメント開始");
                        break;
                    case XmlPullParser.START_TAG:
                        XPname = xpp.getName();
                        Log.i("MainActivity", XPname + "要素開始");
                        int attrCount = xpp.getAttributeCount();
                        for (int i = 0; i < attrCount; ++i) {
                            AtName = xpp.getAttributeName(i);
                            AtValue = xpp.getAttributeValue(i);

                            Log.i("MainActivity", "    " +
                                    i + "番目の属性 = " + xpp.getAttributeName(i));
                            Log.i("MainActivity","    " +
                                    i + "番目の値 = " + xpp.getAttributeValue(i));
                            if(AtName.equals("lat")){
                                lati = parseDouble(AtValue);
                                globals.GPXs.add(lati);
                            }
                            if( AtName.equals("lon")) {
                                longi = parseDouble(AtValue);
                                globals.GPXs.add(longi);
                                if(Olati+Olongi!=0) {
                                    float[] distance =
                                            getDistance(Olati, Olongi, lati, longi);

                                    Tdistance += distance[0]/1000;
                                }
                                Olati=lati;
                                Olongi=longi;
                                globals.GPXs.add(Tdistance);
                                LatLong latLong = new LatLong(lati, longi);
                                latLongs.add(latLong);
                            }
                        }
                        break;
                    ~省略~

緯度を読み込むところで、globals.GPXs.add(lati);とし、経度を読み込むところで globals.GPXs.add(longi);として緯度経度を順番にGPXsに加えていきます。
また、一つ前の地点の緯度経度をそれぞれ、Olati, Olongiとして保存しておき、getdistanceを呼び出して距離を計算します。getdistanceは次のようになっていて

public float[] getDistance(double x, double y, double x2, double y2) {
        // 結果を格納するための配列を生成
        float[] results = new float[3];

        // 距離計算
        Location.distanceBetween(x, y, x2, y2, results);

        return results;
    }

アンドロイドのライブラリを呼び出して2地点間の距離を計算し、値を返しています。距離はfloat[0]に格納されています。
距離が計算出来たらTdistanceに加算して通算距離をGPXsに追記します。
次に標高ですが、標高については、この読み込みルーチンでは要素の値としてではなく、テキストとして読みだされますので、case XmlPullParser.TEXT:以下の部分を変更しています。

                case XmlPullParser.TEXT:
                        String t = xpp.getText();
                        Log.i("MainActivity", "テキスト = " + t);
                        if (XPname.equals("ele")){
                            elv = parseDouble(t);
                            if (elv>elvmax){
                                elvmax = elv;
                            }               
                            globals.GPXs.add(elv);                           
                            XPname="";
                        }
                        break;
                    case XmlPullParser.END_TAG:
                        Log.i("MainActivity", xpp.getName() + "要素終了");
                        break;
                }
                eventType = xpp.next();
                //次のトークンに進む
            }
            Log.i("MainActivity", "ドキュメント終了");

if (XPname.equals("ele")){で標高のデータかどうかを判断し、標高データなら、String tに入っている標高値をストリングデータから倍精度変数に変換してelvに格納します。また、elvがそれまでの最高標高より大きければelvmaxを書き換えて更新するとともに、GPXsに標高データを格納します。
これでデータの準備はできました。

全体高低図の表示

 高低図に現在地を表示するためには、また計算が必要になるので、今回はとりあえず全体図を表示するところまでやります。
高低図の表示はprotected void addOverlayLayers(Layers layers) { }の最後に

        if (globals.setHeight){
            HeightMap();
        }

を書き加えて実行しています。globals.setHeightはグローバル変数で型式はboolean、メイン画面でHeight MapをOnにしたときにTrueになります。
また、HeigtMap()を次のように加えます。

    private void HeightMap() {
        ((PaintView)findViewById(R.id.height)).map(globals.GPXs,elvmax);
    }

これで、PaintView.javaのmap()が呼び出されます。map()に渡す引数は、各地点の経度、緯度、標高と通算距離が格納されているArrayListのglobals.GPXsと最高標高のelvmaxです。
PaintView.javaは次のようになっています。

public class PaintView extends View {
    static ArrayList<Double> arrays = new ArrayList<>();
    private int xpos;
    private int xposo;
    private int ypos;
    private int yposo;
    private int ofset=0;
    private double elvmax;

    public PaintView(Context context, AttributeSet attribute){
        super(context,attribute);
    }
    @Override
    public void onDraw(Canvas canvas){
        Paint paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(3);
        int cheight = canvas.getHeight();
        int cwidth = canvas.getWidth();
        float mdis = 1000000;
        int Npos=0;

        double ymax=0;
        double xmax=0;

        if (elvmax>500){
            ymax = elvmax;
        }else {
            ymax = 500;
        }
        int idx = arrays.size();
        xmax = arrays.get(idx-2);
        if (xmax<10){
            xmax=10;
        }
        int xdiv = (int) (xmax/5);
        int ydiv = (int)(ymax/500);

        try {
            for (int i=0;i<xdiv;i++){
                canvas.drawLine(i*cwidth/xdiv,0,i*cwidth/xdiv,cheight,paint);
            }
            canvas.drawRect(new Rect(0, 0, cwidth, cheight), paint);
            for (int i=0;i<ydiv;i++){
                canvas.drawLine(0,cheight-i*cheight/ydiv,cwidth,cheight-i*cheight/ydiv,paint);
            }
            //canvas.drawLine(0,0,cwidth,cheight,paint);
        }catch (Exception e){
            Log.e("PaintView","error");
        }
        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(6);
        try {
            //canvas.drawRect(new Rect(10,10,100,100),paint);

            for (int i = 0; i < arrays.size() / 4; i++) {

                Double x = arrays.get(i * 4 + 2);
                Double y = arrays.get(i * 4 + 3);
                xpos = (int) (x / xmax * cwidth) + ofset;
                ypos = (int) (cheight - (y / ymax * cheight));

                if (xpos>=cwidth){
                    Log.e("PaintView","Over X");
                    break;
                }

                if (i != 0) {
                    canvas.drawLine(xposo, yposo, xpos, ypos, paint);
                }
                xposo = xpos;
                yposo = ypos;
            }
        }catch (Exception e){
            Log.e("PaintView","error");
        }
        //canvas.drawPath(path,paint);
    }
    public void map(ArrayList arraylist,double emax){
        arrays = arraylist;
        elvmax = emax;
        invalidate();
    }
}

OverLayMapviewer.javaで呼び出しているmap()は一番最後の部分で次のように、GPXsに格納された配列をarraysで受けています。GPXsはグローバル変数なのでわざわざ引数で受け取る必要が無いのですが、一応お作法として引数で受け取っています。あまり大きな意味はありません。また最高標高をelvmaxに受けています。そして、invalidate()を実行して、Ondraw()を呼び出して、グラフを表示しています。

    public void map(ArrayList arraylist,double emax){
        arrays = arraylist;
        elvmax = emax;
        invalidate();
    }

Ondrawが高低図表示の本体部分で、変数のofsetは今回は使っていませんが、後々横にスクロールするときに使う予定です。Paint paint = new Paint();でpaintをnewして作成します。ここにいろいろな線を書いていくことになります。
paint.setColor(Color.BLACK);で色を黒に、paint.setStyle(Paint.Style.STROKE);で線を実線にしています。paint.setStrokeWidth(3);は線の太さを設定しています。これらは、グラフの軸線を書くための設定です。
int cheight = canvas.getHeight();でCanvasの高さ方向のドット数、int cwidth = canvas.getWidth();でCanvasの横方向のドット数を取得し計算に使用しています。

縦軸、横軸のスケールを決める。

縦軸の最大値はymaxで、elvmaxつまりコースの最高標高が500m以下なら500m、500m以上なら最高標高をymaxにして、スケールを決めています。最高標高が500m以下の場合はymaxを500mにして、過度に拡大表示をしないようにしています。

       if (elvmax>500){
            ymax = elvmax;
        }else {
            ymax = 500;
        }

横の軸線は500mごとに1本引くようにし、全体スケールに合わせて本数を計算します。 int ydiv = (int)(ymax/500);で軸線の本数を計算しydivに代入しています。
また、縦の軸線については、前回は、Canvasをただ4分割して縦線を引いていましたが、次のように、コースの通算距離を横軸のスケール(xmax)として計算でもとめています。ただし、距離が10㎞以下の場合は、xmaxを10㎞にします。コースの通算距離はarraysで受け取ったグローバル変数GPXsの最後から2番目の値になりますので、int idx = arrays.size();で配列の数を受け取り、xmax = arrays.get(idx-2);で通算距離をxmaxに代入しています。そして、xmaxが10より小さい場合はxmaxを10(㎞)に設定しています。

        int idx = arrays.size(); 
        xmax = arrays.get(idx-2);
        if (xmax<10){
            xmax=10;
        }

また、横軸の軸線は5㎞ごとに引くこととし、int xdiv = (int) (xmax/5);のように計算してxdivに代入しています。

軸線を描く

軸線を描く部分は次のようになっています。

       try {
            for (int i=0;i<xdiv;i++){
                canvas.drawLine(i*cwidth/xdiv,0,i*cwidth/xdiv,cheight,paint);
            }
            canvas.drawRect(new Rect(0, 0, cwidth, cheight), paint);
            for (int i=0;i<ydiv;i++){
                canvas.drawLine(0,cheight-i*cheight/ydiv,cwidth,cheight-i*cheight/ydiv,paint);
            }
            //canvas.drawLine(0,0,cwidth,cheight,paint);
        }catch (Exception e){
            Log.e("PaintView","error");
        }

先ほど計算した、xdivが横軸の軸線数で、for文でループし、
canvas.drawLine(i*cwidth/xdiv,0,i*cwidth/xdiv,cheight,paint);で線を描いています。
canvas.drawLine( )が線を引く命令で、第一引数が始点のX座標、第二引数が始点のY座標、第三引数が終点のX座標、第四引数が終点のY座標、第五引数がCanvasです。
始点のX座標は、i*cwidth/xdivで全体をいくつで割るかがxdivで例えば4つに割る場合で、横軸のドット数cwidthが800なら、800/4で200ドットごとに線を引きます。
y軸も同様です。

高低図の表示

高低図を描いているのは

        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(6);

以下の部分で、色を青にして、線幅を6にし、軸線よりもやや太くしています。
高低図は for (int i = 0; i < arrays.size() / 4; i++) { }で配列数を4で割った数だけループします。配列には、一つの地点に着き、緯度、経度、通算距離、標高の4つのデータが格納されているので4で割った数が地点の数になります。ここは2次元配列にしてもよかったのですが、ArrayListを2次元にするのがややこしいので今回はシンプルに1次元配列にしました。
X地点はDouble x = arrays.get(i * 4 + 2); Y地点はDouble y = arrays.get(i * 4 + 3);として配列arraysがら読みだしています。
そして、xpos = (int) (x / xmax * cwidth) + ofset;でX座標を計算しています。フルスケールがxmaxなのでxをxmaxで割り、cwidthを掛けるとX座標になります。ofsetはスクロールするときの変数で今回は0のままです。
Y座標の計算は少しややこしくて、Canvasの座標は左上がゼロになるので、そのままプロットすると上下逆さの図になってしまいます。そのため、左下が高さゼロになるように計算をしています。ypos = (int) (cheight - (y / ymax * cheight));として左下からのドット数y/ymax*cheightを高さ方向のドット数cheightから引いて計算しています。
これで、各ポイントのX座標、Y座標が求まりました。
始めのポイントは線が引けませんので、i=0の時だけスキップするため、if (i != 0) { canvas.drawLine(xposo, yposo, xpos, ypos, paint); }とし、2番目のポイント以降で線を引いています。
xposo、yposoはそれぞれ前の地点のX座標、Y座標です。プロットした後、xposo = xpos; yposo = ypos; として代入しています。
また、何故かわかりませんが、一度メイン画面に戻って、再度地図画面に戻ると高低図の最後の地点から左下に直線が引かれてしまうという不具合があったので、

                 if (xpos>=cwidth){
                    Log.e("PaintView","Over X");
                    break;
                }

として、右端にプロットが来た時点でループを抜け出すようにしています。この不具合はデバッグで確認をしてみましたが、理由がよくわかりませんでした。ですが、一応この処理を入れたことで解消されています。
また、この処理は、現在地表示をするときに横軸のスケールを最大で10㎞にしようと思っているので、処理を途中で抜け出すのにも使えるので、まあいいかという感じで入れています。表示させるとこんな感じになります。
嵐山~二ノ瀬まで行った時のルート図です。最高標高が500m未満なので縦軸の軸線がありません。最後に登っているのがよくわかります。前回はここでへばってしまいました。

f:id:alasixOsaka:20200412173322j:plain
嵐山~二ノ瀬までのルートと高低図
次回は、現在地を表示させることをやってみようと思います。また、今回は高低図を表示するために、Viewを変更したままになっているので、高低図はいらないよというときはViewを戻して、全面地図のViewを表示させないと無意味なCanvasが下に表示されたままになります。そのあたりもやっていかないといけません。