プログラマーのメモ書き

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

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