プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

OpenStreetMap で地図を編集してみよう!

はじめに

先日のOSC名古屋2017 にて、 OpenStreetMap のセッションを聞いてきました。 で、このセッション中に、地図が自由に編集できますよ、という話がありました。 自由にというとちょっと語弊がありますね。OpenStreetMap は

OpenStreetMap(OSM)は、道路地図などの地理情報データを誰でも利用できるよう、フリーの地理情報データを作成することを目的としたプロジェクトです。誰でも自由に参加して、誰でも自由に編集でき、誰でも自由に利用する事が出来ます。

OpenStreetMap 日本語サイト より引用

というもので、自分が知っている地域の情報が地図に反映されていない場合は、それを反映することができますよ、といったほうがいいかと思います。

ということで、 OpenStreetMap の地図を編集した話をメモしておきたいと思います。

ちなみに、OpenStreetMap なんて知らねーぜ、という方も多いかと思いますが、実は、Facebookの場所の地図表示はこの OpenStreetMap を使っています(地図のどこか下のほうかな?にクレジットがあります)。なので、知らず知らずのうちに使っていることも多いかもですよ。

アカウントの取得

地図を編集するなら、まずアカウントを取得する必要があります。 OpenStreetMapのページを開いて、『地図を編集』を押します。 アカウントを持っていなければ、『今すぐ登録』で登録します。 登録には、有効なメールアドレスとパスワード、表示名を入力します。

登録すると確認メールが来るので、問題なければメール中のリンクをクリックすればOKです。

チュートリアル

ログインすると、下記の様な画面が表示されます。

f:id:junichim:20170612165642p:plain

初めてなので、チュートリアルをやってみました。 チュートリアルでは、場所(ポイント)の指定方法、ライン(道路など)の指定方法、エリア(建物など)の指定方法、を教えてくれます。 いずれも、直感的な操作なので、すぐにわかる事かと思います。

ま、チュートリアルの詳細は紹介するよりも、実際にやってみてもらうほうがよくわかると思いますので、割愛します。

ただ、残念ながら、ポイントの説明までは日本語なのですが、エリアとラインの説明が英語でした。そんなに難しくないので、ポイントをやってみれば、エリアとラインのチュートリアルもなんなくこなせるかと思います。

地図を編集

さて、実際に地図を編集してみましょう。 OpenStreetMapにログインした状態だと、画面上部の『編集』ボタンをクリックすることでそのまま編集ができます。

f:id:junichim:20170612171150p:plain

編集画面では、上空からの画像(衛星写真?航空写真?)の上に、登録済みの地物(先ほどのポイント、ライン、エリア)が重ねて表示されています。

伊勢近辺の方ならわかりますが、上記の画像でも ダイムスタジアム伊勢(以前は、倉田山公園野球場)や最古の厄除観音で知られる松尾観音寺といった、超メジャーな場所ですらまだ登録されていません。 最初に登録すれば、登録者としての名前が一生(どころかOpeNStreetMapがある限りずっと)残るので、これは登録しない手はありません。

ということで、私が登録してしまいます。 登録すると、こんな感じです。

f:id:junichim:20170612173100p:plain

上空からの写真だと、伊勢市の消防本部・防災センターが建設中のようで、敷地がよくわからかったので、ポイントとして登録しています。 この状態だと、まだ正式に登録されていません。画面上部の『保存』ボタンを押すと、

f:id:junichim:20170612173620p:plain

のように、登録時の情報入力画面が表示されますので、適切なコメントを記入してから、『アップロード』を押します。 問題がなければ、データがアップロードされます。アップロード完了後、わりとすぐに反映されます(審査とかないので)。

f:id:junichim:20170612173959p:plain

これ、編集しだすと、『あ、ここの神社がない。よし、登録してしまえ。』とか『ここの川岸の形が違ってる。よし、なおしてやれ。』とか、簡単に編集できるので、結構くせになりそうです。

最後に

今回地図を編集してみようかな、と思い立ったきっかけは、OSM名古屋2017で聞いたのもあるのですが、たまたま自分の実家近辺を見たときに、近所の目印となるようなものが全くなかったためです。まあ、田舎だから仕方ないのですが、せっかくその場所に関する情報をもっていて、OpenStreetMapというものも知っているので、登録しない手はない、と思い立った次第です。

あと、普段は、OpenStreetMap を使ってばかりですが、一度昔々に地図作成ってどうなんだろうか?と思って、OpenStreetMap系の話を聞いたときは、(その時の話の主旨をよくわかってなかったのもあるかもしれませんが)GPSロガーが必要だったような印象を受けて、これは敷居高いな、と思った印象がありました。

でも、今回試して上空からの写真を元に、気軽に地図を編集できるので、これはやらない手はない、という印象でした。 登録したものが間違っていたらどうしよう、とかは気にせず、間違っていたら誰かが直してくれるはずなので、気にせずいきましょう。

少しでも、興味があれば、ぜひ試してみることをお勧めします。

ちなみに、上記の地図編集は、iDというエディタをブラウザ上で使った場合の地図編集となるようです。OpenStreetMapの地図編集は、これも上記の方法一通りというのではなく、JOSMという上級者向け(?)のツールなどもあるようです(こっちはまだ試せてません)。

地図編集するほどではない方に

あと、今回初めて知ったのですが、 OpenStreetMap の画面から、修正要望のようなメモを残す機能があります。

f:id:junichim:20170612174843p:plain

画面右端の列の吹き出しようなアイコンをクリックすると、

f:id:junichim:20170612232851p:plain

のようなメモ編集蘭が表示されます。

これは、自分で修正するほどではないけど、『こうしてほしい』、とか、『ここ間違ってるよ』いったような要望を伝えることができる機能のようです。 便利なのが、アカウントがなくてもメモを残すことができます。もちろん、アカウントでのログインがあれば、その人に紐づいたメモになります。

気になることがあるけど、地図を編集するほどではない、とか、アカウント作るのなんかいやだ、という場合、こういう機能を利用するのもありかと思います。

AWS CLI 複数の認証情報の設定について

以前、EC2のインスタンスを t1.micro から t2.nano へ移行する作業について書きました。

blog.mori-soft.com

この中で、S3 へバックアップを行うという設定を行っていたのですが、月一動作だったので、改めて動作確認をしてみると、どうもうまく動いていません。 こんな感じのエラーです。

2017-06-01 03:05:01 JST: aws s3 cp バックアップファイル名 s3://バケット名/
upload failed: バックアップファイル名 to s3://バケット名/ファイル名 An error occurred (AccessDenied) when calling the CreateMultipartUpload operation: Access Denied

アクセス拒否のようです。

aws cli はいろんな方法で認証情報を指定できるので、これが間違っているのだろうと予想されます。 いろいろと調べてみると、 aws configure list で現在の認証情報の設定状況を確認できることがわかりました。

dev.classmethod.jp

早速、試してみると

bitnami@ip-172-30-0-73:~/.aws$ aws configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ****************AAAA shared-credentials-file    
secret_key     ****************aaaa shared-credentials-file    
    region                <not set>             None    None
bitnami@ip-172-30-0-73:~/.aws$ 

あれ?なんかおかしいぞ。

実は、aws cli を使う際、EC2 用のユーザーと S3 用のユーザーを分けていました。 で、上記のアクセスキーなどは、EC2ユーザーのものです。 おまけに、 region も設定されていない。

ということで、認証情報の設定方法を見直しました。

間違っていた点

恥ずかしならが、調べた結果、いろいろと間違っていました。まず現在設定していた方法は、

  • EC2 用のユーザーは aws configure コマンドで設定する
    • .aws/config と .aws/credentials に内容が書かれている
  • S3 用のユーザーは .aws/config.s3 ファイルにすべての内容を記述する
  • S3 用のユーザーを使う場合は、 環境変数で AWS_DEFAULT_CONFIG=.aws/config.s3 を指定する

というものでした。

で、まず大きく間違っていたのが、下記の説明にある、

docs.aws.amazon.com

この部分、

Storing Credentials in Config
The AWS CLI will also read credentials from the config file. If you want to keep all of your profile settings in a single file, you can. If there are ever credentials in both locations for a profile (say you used aws configure to update the profile’s keys), the keys in the credentials file will take precedence.

(頑張って意訳)

Config ファイルへの認証情報の保存について
AWS CLI は config ファイルから認証情報を読むこともできます。プロファイル設定のすべてを1ファイルに保存することも可能です。もし、プロファイルの情報が複数個所にある場合は、credentialsファイルのキーが優先されます。

です。

この前半部分を読んで、configファイルに認証情報全部を書けると思い込んでしまいました。でも、上記の意訳からもわかるように、書けることはかけるんですが、(同じプロファイル)に対する認証情報が複数ある場合、credentialsファイルのものが優先的に使われるそうです。 やられました。

更なる間違い

上記の問題が大きいので、些細なことですが、さらに、ついでに間違っていたのが、config.s3 ファイルの書き方です。 config.s3 ファイルを、

bitnami@ip-172-30-0-73:~/.aws$ cat config.s3
[s3]
aws_access_key_id = アクセスキー
aws_secret_access_key = シークレットアクセスキー
region = ap-northeast-1
bitnami@ip-172-30-0-73:~/.aws$ 

と書いており、いつつけたか覚えのない s3 というプロファイル名がついていました。 これは、プロファイル名になるはずなので、切り替えたければ、 –profile s3 のようにコマンド実行時にプロファイル名も指定する必要がありました。

仮にここを [default] に修正すると、

bitnami@ip-172-30-0-73:~/.aws$ aws configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ****************AAAA shared-credentials-file    
secret_key     ****************aaaa shared-credentials-file    
    region           ap-northeast-1      config-file    /home/bitnami/.aws/config.s3
bitnami@ip-172-30-0-73:~/.aws$ 

のように、regionは正しく認識されました(もっとも、上記の二重に認証情報がある場合の問題が残っているので、これでも使えませんがね)。

最終的な設定内容

で、結局、認証情報を使い分けるために、プロファイルを追加することとしました。ここで、プロファイル名は s3user としています。 設定後のファイルはこんな感じになります。

~/.aws/credentials

[default]
aws_access_key_id = EC2アクセス用のアクセスキー
aws_secret_access_key = EC2アクセス用のシークレットアクセスキー

[s3user]
aws_access_key_id = S3アクセス用のアクセスキー
aws_secret_access_key = S3アクセス用のシークレットアクセスキー

~/.aws/config

[default]
region = ap-northeast-1

[profile s3user]
region = ap-northeast-1

–profile s3user をつけてコマンドを実行すると、S3 用ユーザーの認証情報が使われています。

bitnami@ip-172-30-0-73:~/.aws$ aws configure list --profile s3user
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                   s3user           manual    --profile
access_key     ****************BBBB shared-credentials-file    
secret_key     ****************bbbb shared-credentials-file    
    region           ap-northeast-1      config-file    ~/.aws/config
bitnami@ip-172-30-0-73:~/.aws$ 

念のために、 –profile なしを試すと、ちゃんと EC2 用ユーザーの認証情報が使われます。

bitnami@ip-172-30-0-73:~/.aws$ aws configure list                                                                                                                     
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ****************AAAA shared-credentials-file    
secret_key     ****************aaaa shared-credentials-file    
    region           ap-northeast-1      config-file    ~/.aws/config
bitnami@ip-172-30-0-73:~/.aws$ 

この認証情報を使うようにバックアップスクリプトを修正したら、問題なく動作しました。 めでたし、めでたし。

mapsforge でポップアップするマーカーを試す

下記の記事で、 Android でのオフライン地図表示を試しましたが、

blog.mori-soft.com

本格的に進めるために、まずは地図表示ライブラリの mapsforge をもう少し詳しく使ってみたいと思います。

簡単なサンプル

まず最初に、簡単なサンプルとして、下記のリンク先の説明に従って、地図を表示してみます。

https://github.com/mapsforge/mapsforge/blob/master/docs/Getting-Started-Android-App.md

英語ですが、書いてある通りにすればよいので、そんなに難しくはありません。

地図データのダウンロード

地図データについては、ベルリンの地図を表示してもピンとこないので、日本の地図をダウンロードしておきます。 ちなみに、今回は multilingual 以下にある地図を使いました。

今回のアプリは実機(Nexus5, Android 6.0)で試そうと思います。なので、実機にファイルを転送しておきます。

C:\> adb push japan-multi.map /sdcard/Download/

ファイルの保存先(今回は /sdcard/Download/ )は、実装する予定のコードに合わせます。

依存関係の記述

開発環境として Android Studio を使っているので、ライブラリファイルの依存関係を下記 Integration Guide を参考に

https://github.com/mapsforge/mapsforge/blob/master/docs/Integration.md

build.gradle に書いておきます。 基本的に、 Core と Android のセクションのものを書いておけばいいはずです。下記のようになりました。

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.2.0'

    compile 'org.mapsforge:mapsforge-core:0.8.0'
    compile 'org.mapsforge:mapsforge-map:0.8.0'
    compile 'org.mapsforge:mapsforge-map-reader:0.8.0'
    compile 'org.mapsforge:mapsforge-themes:0.8.0'
    compile 'net.sf.kxml:kxml2:2.3.0'

    compile 'org.mapsforge:mapsforge-map-android:0.8.0'
    compile 'com.caverock:androidsvg:1.2.2-beta-1'
}

コードの記述

で、リンク先の指示に従ってサンプルコードを書くのですが、今回試した実機(Nexus5)がたまたま Android 6.0 だったため、外部ストレージ領域へのアクセスに手間取りました。 というのも、API レベル 23 以上がターゲットの場合、パーミッションをインストール時に許可するのではなく、実行時に許可するようになってました。

developer.android.com

ここだけ気をつければ、あとは大きな問題なく動作するはずです。 動作時のイメージはこんな感じです。

f:id:junichim:20170530172659p:plain

なお、参考までにサンプルコードGitHubにあげておきます。

https://github.com/samples-of-junichim/MapsforgeSample.git

このリポジトリのタグ simple になります。

サンプルプログラムの動作

さて、次はマーカーを表示してみたいと思います。

ただ、参考になるものが、GitHub の mapsforge の説明にはみあたらないので、一度 mapsforge のリポジトリを clone して、この中に含まれている mapsforge-samples-android を動かしてみます。

androidのサンプルプログラムは、リポジトリを clone 後、 Android Studio にインポートすればOKです。 なお、インポートの際は、 mapsforge-samples-android ではなく、リポジトリのトップディレクトリを指すようにしてください(mapsforge-samples-android を指してもうまく認識されません)。

問題がなくプロジェクトが立ち上がったら、実機を接続して実行してみてください。なお、このサンプルの実行には、germany.map の地図ファイルが必要になりますので、別途ダウンロードして、実機の /sdcard にコピーしておきます。

このサンプルプログラムを動作させるといろいろなデモを見ることができます。

マーカーの表示には、このうち、OverlayMapViewer.java を参考にしました。

f:id:junichim:20170530173634p:plain

実行するとこんな感じの表示になります。

f:id:junichim:20170530173800p:plain

なお、このサンプルでは、いろんなものを地図上に表示する方法を示しています。円や折れ線、画像(いわゆるマーカー)、画像で領域を埋める、などです。参考になりますね。

マーカーの表示

さて、上記のソースコードをざっと読んで、実際に試してみましょう。上記のソースコードから、マーカーを表示するためには、 Marker というクラスをインスタンス化して、MapView に追加すれば良さそうです。

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

Marker をインスタンス化する際には、表示したい画像を Drawable から、 Bitmap(org.mapsforge.core.graphics.Bitmap)に変換してやる必要があります。 このあたりの処理も、先ほどのサンプルコードに載っていて、

        Drawable drawable = getResources().getDrawable(resource);
        Bitmap bitmap = AndroidGraphicFactory.convertToBitmap(drawable);

とすればOKです。 なお、AndroidGraphicFactory.convertToBitmap メソッドは、android.graphics.Bitmap を内部に保持する AndroidBitmap クラス(org.mapsforge.core.graphics.Bitmap インターフェースを実装)を返しています。

今回作成したサンプルの動作としては、画面上をロングタップすると、その位置にマーカーを設置する、というものです。 最初のサンプルでも出てきた、TileRendererlayer の onLongPress をオーバーライドして、そこからマーカーを設置するメソッドを呼び出しています。

        TileRendererLayer trl = new TileRendererLayer(tileCache, mds, mapView.getModel().mapViewPosition, AndroidGraphicFactory.INSTANCE) {
            @Override
            public boolean onLongPress(LatLong tapLatLong, Point layerXY, Point tapXY) {
                return setMarker(tapLatLong);
            }
        };

動作イメージは下記のようになります。

f:id:junichim:20170530175048p:plain

この部分のソースコードは、リポジトリ

https://github.com/samples-of-junichim/MapsforgeSample.git

の marker タグを見てください。

ポップアップを追加

次は、マーカーをタップすると、ポップアップを表示するという処理を試したいと思います。

これって、地図系のアプリでは当たり前のように使われてるとおもうんだけど、ライブラリとかだとたまにサポートされていなかったりします。 で、mapsforge はどうかというと、ああ、残念、組み込み済みの便利な実装はありませんでした。 というわけで、自分で実装します。

ポップアップの挙動もいろんな方法があると思うのですが、今回は、次のように動作するポップアップを作成します。

  • マーカーをタップするとポップアップが表示される
  • ポップアップが表示中の場合は、再度同じマーカーがタップされるとポップアップを閉じる
  • マーカーのない場所をタップするとポップアップが閉じる
  • ポップアップ表示中に、別のマーカーをタップすると後者のマーカーのポップアップのみを表示する(つまり、ポップアップの表示をマップ全体で最大一つのマーカーだけに限定する)

さて、実装に先立ち、ヒントになるものを、 mapsforge-samples-android から探すと、BubbleOverlay.java と ViewOverlayViewer.java が参考になりそうです。

BubbleOverlay.java を見ると、マップ上に吹き出しが表示されています。この吹き出しは、いったんTextViewを作成して、それをビットアップに変換して、Makerにして表示するというものです。

ViewOverlayViewer.java でやってるのは、View(Button)を作って、これをMapView(ViewGroupの派生)に追加して表示させるというものです。

今回は、BubbleOverlay の方法を参考にしました。 やり方はいたってシンプルで、最初に地図上をロングタップして Marker を表示するところは同じです。で、その Marker のタップイベントのリスナーにおいて、ポップアップを表示する処理を行います。

具体的には、 Marker を extends したクラスを作り onTap にて

    @Override
    public boolean onTap(LatLong geoPoint, Point viewPosition, Point tapPoint) {

        Log.d(TAG, "LoaLong: " + geoPoint.getLatitude() + ", " + geoPoint.getLongitude());
        Log.d(TAG, "viewPos: " + viewPosition.x + ", " + viewPosition.y);
        Log.d(TAG, "tapPos : " + tapPoint.x + ", " + tapPoint.y);

        if (contains(viewPosition, tapPoint)) {
            Log.d(TAG, "contains: " + true);

            if (! TextUtils.isEmpty(mText)) {
                Log.d(TAG, "text is exist");

                if (null == mBalloonMarker) {
                    Log.d(TAG, "balloon is null");

                    Bitmap bmp = createBalloon(mMapView.getContext());
                    mBalloonMarker = new Marker(MarkerWithBubble.this.getLatLong(), bmp, 0, - bmp.getHeight() / 2 - BALLOON_VERTICAL_OFFSET );
                    mMapView.getLayerManager().getLayers().add(mBalloonMarker);
                } else {

                    if (null != mBalloonMarker) {
                        mBalloonMarker.setVisible(!mBalloonMarker.isVisible());
                    }
                }
            }
            Log.d(TAG, "text is null");
        } else {
            Log.d(TAG, "contains: " + false);

            if (null != mBalloonMarker) {
                mBalloonMarker.setVisible(false);
            }
        }
        return super.onTap(geoPoint, viewPosition, tapPoint);
    }

のようにします。

今回は、ポップアップの表示をマップ全体で最大一つのマーカーだけに限定したいというのがあったため、タップイベントが一つのMakerで終わらないように、ポップアップの処理を行っても、戻り値として true を返していません。 ここが個人的には、ちょっと気になるところです。もっといい実装方法はないかなー。

さて、上記関数で呼び出している createBalloon は

    private Bitmap createBalloon(Context c) {
        TextView tv = (TextView) LayoutInflater.from(mMapView.getContext()).inflate(R.layout.popup_marker, null);
        tv.setTextColor(mColor);
        tv.setTextSize(mTextSize);
        tv.setText(mText);

        return Utils.viewToBitmap(c, tv);
    }
public class Utils {

    public static Bitmap viewToBitmap(Context c, View view) {
        view.measure(View.MeasureSpec.getSize(view.getMeasuredWidth()),
                View.MeasureSpec.getSize(view.getMeasuredHeight()));
        view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
        view.setDrawingCacheEnabled(true);
        Drawable drawable = new BitmapDrawable(c.getResources(),
                android.graphics.Bitmap.createBitmap(view.getDrawingCache()));
        view.setDrawingCacheEnabled(false);
        return AndroidGraphicFactory.convertToBitmap(drawable);
    }

}

のように、さきほど挙げた BalloonOverlay.java で使っていた処理を参考にTextViewをBitmapに変換しています。

実際に動かしてみると、

f:id:junichim:20170530215347p:plain

のようになります。

参考

このポップアップするマーカーですが、最初は、 ViewOverlayViewer.java を参考に、バルーンを背景に持つTextViewをMapViewに追加して実装しようと考えました。 ポップアップの表示や複数マーカーに対する切り替えはうまくいったのですが、表示位置をずらすことがうまくできなくてこちらの方法ではあきらめました。

MapView に View を追加する際に、 MapView.LayoutParams でレイアウトパラメータを指定しています。このクラスは ViewGroup.LayoutParams の派生クラスで、オフセットを与えることができないようです。 さらに、 MapView#onLayout では子 View が MapView.LayoutParams を持っているのを前提に処理を行っているようなので、あきらめました。

リポジトリの 62f3131 あたりにこの方法で試した痕跡があるので、気になる方はぜひチャレンジしてみてください。

課題

一応、これで mapsforge でマーカーの表示とタップによるポップアップ表示ができるようになりました。 今回のサンプルでは、ロングタップでマーカーを設定する、という組み方をしたため、マーカーを設定した場所の情報がなく、そのため、画面回転で再描画が行われるとマーカーが消えてしまうという問題があります。 実際の利用時は、この辺りはリスト等で管理してあげないといけなさそうです。

OSC名古屋 2017 に参加してきました

先日(2017年5月27日(土))に行われた、オープンソースカンファレンス名古屋に伊勢志摩コミュニティ共同ブースとして参加してきました。

元々、出展側で参加する気はなかったのですが、伊勢IT交流会によく来られている Kapper さんからお話を振られて、コミュニティでも参加できるよ、ってことを教えていただいたので、ほとんどノリだけで参加してきました。 とはいっても、伊勢IT交流会 単独で参加してもそんなにアピールするものもないので、この地域で活動している他のコミュニティさんも巻き込んで共同ブースという形で出てきました。 ちょうど、『名古屋勉強会・コミュニティ協同ブース 』さんが出展していたので、それを真似させていただきました。

当日の様子

共同ブースは、

の3コミュニティで参加してきました。 チーム伊勢さんと伊勢ギークフェアさんはチラシのみで参加。

こんな感じです。

f:id:junichim:20170529122037j:plain

正直、一地方のコミュニティなんで、そんなに人も来ないだろうと思っていました。 ですが、名古屋という場所柄か、三重県に縁のある方が結構来られて、なんと、準備していたチラシがすべてなくなってしまいまい、急ぎで1階の事務室を借りてコピーを増刷することになりました。うれしい悲鳴ですね。

懇親会でもいろんな方とお話ができてよかったです。

次回の開催予定は秋ごろを考えているのですが、これをきっかけに県内から(もちろん県外からも)来る人が増えるといいなと思います。

参考

いろんな方が参加レポートや出展レポートを書かれているので、興味ある方はぜひ読んでみてください。 ここでは、今回の参加のきっかけとなった Kapper1224 さんのブログを貼っておきますね。

kapper1224.sblo.jp

オフラインでの地図表示と経路検索 (OpenStreetMap と Graphhopper)

ある android アプリを作ろうと思い立ったのですが、そのためには、オフラインでの地図表示と経路検索を行う必要が出てきました。 当初は、Google Map での実現を考えたのですが、オフライン地図に対応していない地域もあるため、あきらめました。

で、次にあがった候補が、 OpenStreetMap です。OSM の Wiki を調べてみるとオフライン動作についての情報がまとまったページがありました。

ここを見ると、経路検索をオフラインで行えるものとして、 Graphhopper というライブラリが使えそうです。 日本語の情報もあまりないようなので、Graphhopper のデモapkを動かすところまで試したことをまとめておきます。

作業環境は以下の通りです。

  • Windows 10 Pro, 1607, 64bit なお、Bash on Ubuntu on Windows (以下 BoW) も併用してます
  • Android Studio
  • Nexus 5 (6.0.1)

Graphhopperの準備

Graphhopper を見ると商用サービスのように思えますが、オープンソース版があります。 このページのリンクをたどると、Github のプロジェクトページにたどり着きます。

github.com

ここの『For Mobile Apps』から、androidでのデモの説明ページに行きます。

graphhopper/index.md at master · graphhopper/graphhopper · GitHub

android版のデモapkはgraphhopperのリポジトリに含まれているようです。 さて、デモを動かすには、デモ版のソースコードと各種データが必要になりますが、この説明ページの内容が少しわかりにくいので、自分の行った手順とまとめておきます。

まずは、Maps のセクションを参考に、デモapkに必要なデータを準備します。

  1. OpenStreetMap の raw データをダウンロードします。今回は、三重県を含むデータで試したので、kinki のデータを落としました。 なお、いろいろと書いてありますがよくわからない面もあったので、標準的と思われる、 kansai-latest.osm.pbf というファイルをダウンロードしました。
  2. map データを落とします。ダウンロードサイトから japan.map をダウンロードしました。なお、mapデータとはあとでも出てくる mapsforge という地図表示ライブラリで使うデータになります。

以下の作業は、Graphhopperのリポジトリをcloneしてから行います。なお、この部分は BoW 上で作業しています。また、シェルスクリプトを動作させるために、java と zip/unzip を BoW 上でインストールしています。

  1. $ git clone git://github.com/graphhopper/graphhopper.git graphhopper
    
    なお、 git clone を実行したディレクトリに kansai-latest.osm.pbf および japan.map ファイルがあるものとします

  2. $ cd graphhopper
    $ ./graphhopper.sh import ../kansai-latest.osm.pbf
    
    kansai-latest.osm-gh というディレクトリが作成されて、中に必要なデータが作成されます

  3. C:\Users\mor> adb push kansai-latest.osm-gh/edges /sdcard/Download/graphhopper/maps/kansai-gh/
    
    kansai-latest.osm-gh ディレクトリ内のすべてのファイル(edges, geometry など)を、androidの外部ストレージ領域にコピーします。 この際、コピー先に kansai-gh (地域名-gh)のフォルダを用意しておきます。

  4. C:\Users\mor> adb push japan.map /sdcard/Download/graphhopper/maps/kansai-gh/
    
    japan.map もandroidの外部ストレージにコピーします

  5. C:\Users\mor> adb shell
    $ cd /sdcard/Download/graphhopper/maps/kansai-gh/
    $ mv japan.map kansai.map
    
    japan map を kansai.map にリネームします

ここまでできれば、データの準備は完了です。

なお、説明ページではzip圧縮したファイルを使うこともできるとあったので、androidへのコピー処理が簡単にできるかと思い、試したのですが、最終的なデモ動作がうまくいきませんでした。

デモapkプロジェクトの読み込み

以下のページに Android Studio でのデモapkを動作させる手順があります。

graphhopper/android-studio-setup.md at master · graphhopper/graphhopper · GitHub

ただ、自分の環境だと、Android Studio が Windows 上にあるので、 BoW 上の先ほどcloneしたリポジトリとは別に、graphhopperのリポジトリをcloneしておきます。 また、リポジトリのブランチをmasterではなく、 0.8.2 のタグに切り替えておきます。

あとは、説明ページと同様に、 graphhopper/android をオープンするプロジェクトとして指定すれば完了です。

サンプルapkの動作

オープンしたプロジェクトを実行します。正常に実行できると下記の様な画面が表示されると思います。

f:id:junichim:20170515005829p:plain:w300

Local のラベルの隣のスピナーで、オフラインデータとして設定した地域名(今回の場合は kansai)を選択して、OKボタンを押すと地図が表示されます (ただ、最初に表示される場所は、海の中のため、適当にズームアウトしないと陸地が表示されません)。なお、地図で日本を表示しようとした場合、後述の対策が必要です。

地図が表示されれば、ロングタップで経路を求める始点を指定します。

f:id:junichim:20170515005844p:plain:w300

赤線で囲んだ緑の旗(伊勢神宮の外宮前あたりを指しています)が始点を表しています。なお、この際、親切にメッセージを表示したり、音やバイブレーションで設定完了を教えてくれないので注意深く画面を見てください。始点が表示されていれば、設定できた、という感じです。

経路を求める終点は同じく、ロングタップで設定します。 終点を設定すると自動的に経路を求めて表示します。下記画面内の赤線で囲った赤の旗が終点、ちょっと見にくいですが37という数字が振られた道路を経由している緑の点線が経路です。

f:id:junichim:20170515005857p:plain:w300

トーストでも情報を出してくれますが、消えるのが早すぎて何を出しているのか正直分かりません(距離ぐらいはあったかな?)。

次の経路を求めたければ、地図上のどこかの地点をロングタップして、再度始点・終点を指定することになります。 一応、これで、オフラインの動作ができているということになりそうです。

なお、このデモapkの他の動作は次のようになっているようです。 Remoteスピナーから、地域名を選択して『downloding』ボタンを押すと、該当地域の地図データおよび経路データをダウンロードしてくれます。 で、ダウンロード後のデータは、ローカルに保存され、次回起動からはローカルデータとしてアクセスできます。 (デモだけなら、データの準備いらないんじゃ・・・)

注意

実は、デモapkそのままでは、日本のデータ(japan.map)が対象の場合、正しく日本の地図が表示されません。

f:id:junichim:20170515005909p:plain:w300

Android Studio につないで実行してみると、どうも地図データのブロックサイズが制限オーバーの様です。

f:id:junichim:20170515003734p:plain

調べてみると、このGraphhopperのデモアプリでは、地図の表示に mapsforge というライブラリを利用しているようなのですが、そのライブラリ側のデータ読み込み時の制限に引っかかっているようです。 ネットにも類似の議論がありました。

github.com

上記の記事からもわかるように、動的に読み込みバッファサイズを変更できるようなので、その部分だけ手直ししてみます。 具体的には、デモapkの MainActivity.java の 346行目付近に MapFile を生成する処理があったので、その直前にバッファサイズを変更するメソッドを追加してみました。

    void loadMap(File areaFolder) {
        logUser("loading map");
        ReadBuffer.setMaximumBufferSize(7000000); // for test
        MapDataStore mapDataStore = new MapFile(new File(areaFolder, currentArea + ".map"));

なお、指定のバッファサイズは、上記ログ画面で出力されているサイズをカバーするように適当に設定しています。

この状態で試すと問題なく表示できました。 ただし、レンダリングに結構時間がかかっているので、テスト時にはご注意ください。

まとめ

今回試してみた印象としては、

  • オフラインでの地図表示、経路検索はできそう
  • 経路検索の条件指定などは詳しく調べる必要がある
  • 地図のレンダリングは思っていたより遅い(端末のせいか?)
  • 地図データのセットアップ方法を考える必要がありそう

という感じでした。必要に応じて詳しく調べていきたいと思います。