アンドロイド地図アプリの開発

トレランとは直接関係ないですが、今、アンドロイド用の地図アプリを開発中です。

まだまだ、開発途上ですが、備忘録を兼ねてやっていることを書いてみたいと思います。

そもそも、何でそのようなことをしようとしたかというと、ロードバイクで走っているときにルートを確認するために、オフラインで使える地図アプリを使っているからで、もう少し自分用にカスタマイズしたのが欲しいと思ったのがきっかけです。以前は、ガーミンのedge800という地図が表示できるサイクルコンピュータを使っていたのですが、老眼が進んで地図が見辛くなってきたので、もっと画面の大きなものにしたいということで、スマホのアプリを使うようになりました。今使っているアプリはipバイクというアプリで、これはこれで十分多機能なのですが、多機能すぎて使いづらい部分があるので、もっとシンプルに自分の使いたい機能に特化したアプリがあればということで探したのですが、あまり良いのがなさそうなので、いっそのこと作ってしまえという軽い気持ちで始めたところ、始めたのはいいのですが、アンドロイドのアプリは作ったことがないので悪戦苦闘の連続です。

まず、これから作るアプリの機能ですが、ざっとこんな感じです。

  1. オフラインで地図が使えること
  2. GPSで現在地がわかること
  3. ルートラボなどであらかじめ作っておいたルートを表示できること
  4. 高低図を表示して、自分がどの辺まで登ったかがわかること
  5. GPSのログは不要
  6. ANT+などのセンサー対応も不要

1~3は地図アプリなので必須の機能。個人的には4の機能がipバイクに無いので欲しい。峠を登っているときとかにあとどのくらい登らないといけないかわかると励みになるし、ペースをコントロールするにもやり易いので。ただ、簡単にはいきそうにないのでここが一番苦労しそうなポイントかと。

ちなみに、トレラン(主に練習の時)だと、ヤマップが最強のアプリかなと思っています。走るルートのところの地図があればという前提つきですけど。Googleマップ地理院地図に載っていない登山道、ハイキングルートが書いてあって、地形図が表示できて、オフラインで使えて、現在地もわかるので自分的にはこれで十分かなと思っています。

ただ、ヤマップだと地図があってもルートが載ってないエリアに関しては、地理院地図と同じになってしまうので、開発中のアプリもそういうシチュエーションでは使える可能性はあると思っていますが。

さて、肝心のアプリの開発状況ですが、ようやく地図を表示できるところまでできました。なんせアンドロイドのアプリ開発は初めてだし、アンドロイドは仕様がコロコロ変わるので、ネットに載っている情報も古いものは役に立たないものが多く、アンドロイドの仕様には振り回されっぱなしです。

javaのプログラミングは経験があるのでそんなに難しくないだろうと思ったのが甘かったようです。

一応、javaの経験がある人向けの入門書を買って勉強はしたんですが、やっぱり、本に載っているサンプルコードをポチポチ入力して動かしても本当のところは身に付かないですね。自分で考えてプログラミングしないと。

 

基礎&応用力をしっかり育成!  Androidアプリ開発の教科書 なんちゃって開発者にならないための実践ハンズオン (CodeZine BOOKS)

基礎&応用力をしっかり育成! Androidアプリ開発の教科書 なんちゃって開発者にならないための実践ハンズオン (CodeZine BOOKS)

 

 

というわけで、自分の失態を晒すようなものですが、やったことを書いておきます。

開発環境は以下の通り。

オフラインの地図としては、オープンストリートマップを使うことにした。こいつは、文字通りオープンソースな地図で、オフラインマップとしてはとってもメジャー。ただ、オープンストリートマップ自体は地図のデータだけなので、アプリとして端末に地図を表示させるためには、他のAPIが必要。今回はmapsforgeを使うことにした。ipバイクでも使われているし、参考にさせていただいた日本語のサイトもあったので、取っ掛かりとしては良いのかなと言う判断。ちなみに、オープンストリートマップのWikiでは、色々なアプリやAPIが紹介されている。

wiki.openstreetmap.org

 

まずは、Android Studioのインストール。

詳しい紹介は、色々なサイトでされているので、ここでは省略。

ところで、Android Studioというのが曲者で、古い記事だと2.xで書かれたりしていて、3.xとはだいぶ違う。これに気づくのにずいぶん時間がかかった。

mapsforgeを動かす

mapsforgeを使うのに、こちらのサイトを参考にさせてもらった。このサイトによると、地図を表示するだけなら、mapsforgeのサイトのgetting-startedの指示通りにすれば良いということだったが、貼ってあるリンクが切れている。

mapsforge本体のサイトを徘徊して、どのプログラムを使ったら良いのか探し当てるのに一苦労。結論は

mapsforge/GettingStarted.java at master · mapsforge/mapsforge · GitHubにあった。

そこまでは良いが、アンドロイドのアプリはjavaソースコードをただ、コピペすれば動くものではなく、java本体とは別に、xmlという画面の設計等を記述したファイルも書く必要があるし、ライブラリをインポートする場合に、build.gradleにも記述が必要。ところが、mapsforgeのサイトにはどれとどれが対応しているかという記述がないので(少なくとも自分にはわからなかった) 、これを探し当てるのにも一苦労。mapsforgeのサイトには親切なチュートリアルというのがないので初心者にはハードルが高い。とりあえず、参考サイトの著者の方がgithubにサンプルプログラムを置いてくださっていたので、そのプログラムと、mapsforge本体(といってもこっちもgithubのサイトでしかないが)のプログラムを見比べながら、半分コピペで、作ったソースがこれ。

まずは、java本体。

package com.example.mspsforgetest3;

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;

import org.mapsforge.core.model.LatLong;
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
import org.mapsforge.map.android.util.AndroidUtil;
import org.mapsforge.map.android.view.MapView;
import org.mapsforge.map.datastore.MapDataStore;
import org.mapsforge.map.layer.cache.TileCache;
import org.mapsforge.map.layer.renderer.TileRendererLayer;
import org.mapsforge.map.reader.MapFile;
import org.mapsforge.map.rendertheme.InternalRenderTheme;

import java.io.File;

import static android.os.Environment.getExternalStorageDirectory;

public class MainActivity extends AppCompatActivity {

    // Name of the map file in device storage
    private static final String MAP_FILE = "berlin.map";

    private final static int PERMISSION_REQUEST_CODE = 1;


    private MapView mapView;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        AndroidGraphicFactory.createInstance(getApplication());
        mapView = new MapView(this);
        //setContentView(R.layout.activity_main);
        setContentView(mapView);
            /*
             * We then make some simple adjustments, such as showing a scale bar and zoom controls.
             */
            mapView.setClickable(true);
            mapView.getMapScaleBar().setVisible(true);
            mapView.setBuiltInZoomControls(true);

            /*
             * To avoid redrawing all the tiles all the time, we need to set up a tile cache with an
             * utility method.
             */

            final int permission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
            if (permission != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
            } else {
                displayMap();
            }




    }
    @Override
    protected void onDestroy() {
        /*
         * Whenever your activity exits, some cleanup operations have to be performed lest your app
         * runs out of memory.
         */
        mapView.destroyAll();
        AndroidGraphicFactory.clearResourceMemoryCache();
        super.onDestroy();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        switch(requestCode) {
            case PERMISSION_REQUEST_CODE:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    displayMap();
                    return;
                }
            default:
        }

        // アプリを終了
        this.finish();
    }

    private void displayMap() {

        try {

            TileCache tileCache = AndroidUtil.createTileCache(this, "mapcache", mapView.getModel().displayModel.getTileSize(), 1f, mapView.getModel().frameBufferModel.getOverdrawFactor());
            File mapFile = new File(getExternalStorageDirectory().getPath(), MAP_FILE);
            MapDataStore mds = new MapFile(mapFile);
            TileRendererLayer trl = new TileRendererLayer(tileCache, mds, mapView.getModel().mapViewPosition, AndroidGraphicFactory.INSTANCE) {

            };

            trl.setXmlRenderTheme(InternalRenderTheme.DEFAULT);

            mapView.getLayerManager().getLayers().add(trl);

            //mapView.setCenter(new LatLong(34.491297, 136.709685)); // 伊勢市駅
            mapView.setCenter(new LatLong(52.517037, 13.38886));
            mapView.setZoomLevel((byte) 12);
        }catch (Exception e) {
            /*
             * In case of map file errors avoid crash, but developers should handle these cases!
             */
            e.printStackTrace();
        }
    }
}

 ちなみに、外部ストレージを使うので、パーミッションが必要になります。

パーミッションの記述はmanifest.xmlに次の一文を追記し、

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />


Java本体で、パーミッションの確認を行う必要があります。詳しいことは次回にでも書くつもりです。

次に、activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<!--suppress ALL -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".com.example.mapsforgetest3.MainActivity">

    <org.mapsforge.map.android.view.MapView
        android:id="@+id/mapView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />

    <TextView
        android:id="@+id/attribution"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="@dimen/attribution_margin"
        android:gravity="center"
        android:singleLine="true"
        android:text="@string/copyright_osm"
        android:textColor="#000"
        android:textSize="@dimen/attribution_size" />

</RelativeLayout>

 そして、build.gradleです。

    implementation 'org.mapsforge:mapsforge:0.6.1'
    implementation 'org.mapsforge:mapsforge-core:0.11.0'
    implementation 'org.mapsforge:mapsforge-map:0.11.0'
    implementation 'org.mapsforge:mapsforge-map-reader:0.11.0'
    implementation 'org.mapsforge:mapsforge-themes:0.11.0'
    implementation 'net.sf.kxml:kxml2:2.3.0'

    implementation 'org.mapsforge:mapsforge-map-android:0.11.0'

ここでのポイントは、android studio 3.xではライブラリを読み込むときの記述が2.xとは違っているということ。参考サイトの著者も書いてはいないが2.xで作られている用なので、まずここでハマった。android studio 2.xで compile …と書くところは、android studio 3.xでは implementation...と書くらしい。

次のハマりどころはファイルの読み込みで、当然地図データを読み込む必要があるわけで、mapsforgeではオープンストリートマップのデータをマップ形式という独自の形式にしたものを使うらしい。データは

http://download.mapsforge.org/

にいけばダウンロードできる。

当然マップデータが読み込めなければ、地図は表示されない。はじめ、エミュレータでファイルをどこに置くかわからずに、実機(Xperia Z4)でSDカードにデータを置いたが、地図が表示されずに随分悩んだ。いろいろなサイトをあたると、SDカードのディレクトリを引っ張てくる方法が色々書いてあるが、どれをやってもうまくいかない。

結局、ある方法を使って解決することができたが、これについてはまた日を改めて書くことにする。

今回はエミュレータ上でファイルをしかるべき場所に置くことで読み込みができた。どうやったかというと、外部ファイルを扱う短いプログラムを書いて、デバッガでファイルパスの見当をつけた。詳しく書くと長くなるので省略するが、

getExternalStorageDirectory().getPath()

でパスが取れるので、適当なストリング変数に代入し、デバッガでその変数を読んでみた。それによると/storage/emulated/0/がファイルパスになっていた。ただし、それだけでは解決しないのがアンドロイドの困ったところだ。

今回のプログラムでは、java本体の下の方にある

private void displayMap() {
省略
File mapFile = new File(getExternalStorageDirectory().getPath(), MAP_FILE);

の部分がそれに相当する。

で、エミュレータでしかるべき場所にファイルを置いてやればちゃんと読み込むことができるのだが、ここでもハマった。

エミュレータで読み込む外部ファイルをどこにおけば良いかわからなかったのでさんざん探すはめになった。Windowsエクスプローラでそれらしきフォルダを覗いても見当たらないし、ネットで検索してもddmsを使うんだとか書いてあったが、今度はその実行ファイルが見つからない。

そこで仕方ないので、アンドロイドの簡単なアプリを使って、外部フォルダにファイルを書き込んで、そのファイルを探すということをしてみた。例えば、test.txtみたいなファイルをつくって、外部フォルダに書き込んで、Windowsエクスプローラで探してみたが、これも失敗。エミュレータはそのままの形でファイルを持っているのではなく、何らかの変換をしていて、エクスプローラからは直接見えない形になっているらしい。

結論は、わかってしまえばなんのことはないのだが、andoroid studio 3.xでは、Device File Explorerというのを使うらしい。これを使えばWindowsの任意のフォルダからファイルをコピーすることができる。ここの記事をみつけてやっと解決した。

やり方は、android studio で右下のDevice File Explorerのアイコンをクリックするか、メニューからView→Tool Windows→Device File Explorerと進んで起動する。その時に、エミュレータはあらかじめ起動しておく。外部ファイルのパスは、さっき調べたようにStorage/emulated/0/なので、適当なマップファイルをここに保存しておく。やり方は、ターゲットのフォルダを右クリックしてUPloadをクリックすれば別ウィンドウが開いてファイル選択ができるので、Windowsの任意のフォルダに置いたファイルをコピーすることができる。ちなみに、日本の地図であるJapam.mapはファイルサイズが1GBもあって大きすぎてコピーすることができなかった。仕方ないのでBerlin.mapを使って表示させてみた。

f:id:alasixOsaka:20190622114851j:plain

ベルリンの地図が表示された

これが表示された時は本当に嬉しかった。とりあえず、1番めの項目はこれでクリアできた。でも、道のりはまだまだ遠く。はたしてどこまでたどり着けるのだろうか?