プログラマーのメモ書き

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

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でのデモの説明ページに行きます。

https://github.com/graphhopper/graphhopper/blob/master/docs/android/index.md

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 を実行したディレクトリに kansai-latest.osm.pbf および japan.map ファイルがあるものとします

$ git clone git://github.com/graphhopper/graphhopper.git graphhopper

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

$ cd graphhopper
$ ./graphhopper.sh import ../kansai-latest.osm.pbf

3. kansai-latest.osm-gh ディレクトリ内のすべてのファイル(edges, geometry など)を、androidの外部ストレージ領域にコピーします。

C:\Users\mor> adb push kansai-latest.osm-gh/edges /sdcard/Download/graphhopper/maps/kansai-gh/

この際、コピー先に kansai-gh (地域名-gh)のフォルダを用意しておきます。

4. japan.map もandroidの外部ストレージにコピーします

C:\Users\mor> adb push japan.map /sdcard/Download/graphhopper/maps/kansai-gh/

5. japan map を kansai.map にリネームします

C:\Users\mor> adb shell
$ cd /sdcard/Download/graphhopper/maps/kansai-gh/
$ mv japan.map kansai.map

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

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

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

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

https://github.com/graphhopper/graphhopper/blob/master/docs/android/android-studio-setup.md

ただ、自分の環境だと、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"));

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

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

まとめ

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

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

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