Google Maps Android Ver.2 をさわってみた

Google Maps Andoroid Ver.2 がリリースされました。
公式の文書は https://developers.google.com/maps/documentation/android/ ですが英語なので、ひとつここは何か書いてやろうと思っていたのですが、http://www.adamrocker.com/blog/334/google-maps-android-api-v2.html が既に出ているので、これを参考にすると良いと思います。仕事早い…。

ということで予定を変更して、ちょっとヘンなことをしたいと思います。

さきにご注意

Google Play開発者サービス (Google Play services)」というアプリが必要です。どうもGoogleさんの持ってるサービスを利用するためのものっぽい(http://chimtty.blogspot.jp/2012/10/google-play-androidjp.html)。Playストアから無償でダウンロード可能です。

が、エミュレータだとPlayストア自体入ってなくってアウトみたいです。

MapViewを使う

公式のガイドでは Fragment を使うよう誘導されますが、MapViewも用意してくれてあります。ただし次のことに注意が必要です。

  • MapViewのメソッド onCreate, onDestroy, onLowMemory, onPause, onResume, onSaveInstanceState を、Activityのイベントハンドリングにあわせて実行しなければなりません。書くだけなんで別にいいんですが面倒と言えば面倒。
  • CameraUpdateFactory のスタティックな関数を実行しようとすると、初期化されてないとかで例外を投げられたので、Activity#onCreate 時にMapsInitializer.initialize(this); を実行しました。

ネット公開されているタイル画像を取ってくる

そのままだと巨大である地図画像を、小さいサイズの画像に分割されたものです。それぞれの画像は特定の規則のもとでリソース位置が命名されています。…よく説明できないので https://developers.google.com/maps/documentation/javascript/maptypes?hl=ja#CustomMapTypes あたり参照。

JavaScript版ではカスタム地図が作れたのですが、Android版v1ではちょっと大変でした。

が、v2になって、GoogleMap#addTileOverlay でタイル画像を取ってくるオーバレイを作成できるようになりました。

手順は次の通り。

  • TileProvider(またはその派生クラス)を作る
  • TileOverlayOptionsを作り、TileProviderをセットする
  • GoogleMap#tileProvider(TileOverlayOptions)でレイヤを追加 (このメソッド内でオーバレイが生成され返される)

TileProvider自体はTileインスタンスを生成して返すものですが、これから派生したUrlTileProviderは、getTileUrl(int,int,int)をオーバライドしてURLを返せば勝手にダウンロードしてくれます。nullを返すとダウンロードを試みないようで、NullPointerExceptionは投げられません。

コード等の一部

MapViewを使って、東京図測量原図を見たいとします。

layout/activity_main.xml はこんなかんじ



    


MainActivity はこんなかんじ

public class MainActivity extends Activity {

    private GoogleMap mMap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        try {
            MapsInitializer.initialize(this);
            MapView mapView = (MapView) this.findViewById(R.id.map);
            mapView.onCreate(savedInstanceState);
            this.setupIfNeeded();
        } catch (GooglePlayServicesNotAvailableException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onDestroy() {
        MapView mapView = (MapView) this.findViewById(R.id.map);
        mapView.onDestroy();
        super.onDestroy();
    }

    @Override
    public void onLowMemory() {
        MapView mapView = (MapView) this.findViewById(R.id.map);
        mapView.onLowMemory();
        super.onLowMemory();
    }

    @Override
    protected void onPause() {
        MapView mapView = (MapView) this.findViewById(R.id.map);
        mapView.onPause();
        super.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        MapView mapView = (MapView) this.findViewById(R.id.map);
        mapView.onResume();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        MapView mapView = (MapView) this.findViewById(R.id.map);
        mapView.onSaveInstanceState(outState);
    }

    private void setupIfNeeded() {
        if (this.mMap == null) {
            MapView mapView = (MapView) this.findViewById(R.id.map);
            this.mMap = mapView.getMap();
            if (this.mMap != null) {
                this.setup();
            } else {
                Toast.makeText(this, "Cannot start !", Toast.LENGTH_SHORT).show();
            }
        }
    }
    
    private void setup() {
        this.mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); // Googleさんの通常の地図を表示
        // this.mMap.setMapType(GoogleMap.MAP_TYPE_NONE);  // Googleさんの地図を表示しない
        TileProvider tileProvider = new UrlTileProvider(256, 256) {
            @Override
            public URL getTileUrl(int x, int y, int zoom) {
                String url = "http://www.finds.jp/ws/tmc/1.0.0/Tokyo5000-900913-L/"
                        + zoom + "/" + x + "/" + y + ".png";

                try {
                    return new URL(url);
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                    return null;
                }
            }
        };
        TileOverlay tileOverlay = this.mMap
                .addTileOverlay(new TileOverlayOptions()
                        .tileProvider(tileProvider));
        this.mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(
                35.6872, 139.7704), 17.5F));
    }
}

こんなのが出ます。

…ちょっとヘンですね。ただこれはGoogleさんの地図を表示しないようにすれば解決されます。

感想

第一印象は「すごい」でした。
Googleな地図アプリを作っているのですが、これは店じまいせにゃならんと思いました。

v1は自前のカスタム地図の追加はかなり大変なのですが、今回はキャッシュも含めて面倒見てくれます。カメラ姿勢については今回は手を入れませんでしたが、これも簡単にできそうです。高機能かつ扱いやすい、と。

あと、個人的には、GoogleMap.MAP_TYPE_NONEがあるところもポイント高い。地図まで自前というのにも対応できます。

イヤだと感じたところは次の通り。

  • TileOverlayOptionsでzIndexを指定できますが、あくまで自前で追加した地図のみの並び順変更のようです。「Googleさんの通常の地図」は、道路や建物等のオーバレイと、地名やコンビニアイコン等のオーバレイとが分かれていて、自前で追加した地図は必ずその間に入ることになります。このため、下から 道路や建物等、自前で追加した地図、地名やコンビニアイコン等、の順に重ね合わせられ、変更がききません。
  • setMapType(GoogleMap.MAP_TYPE_NONE); とすると、自前の地図のみを表示可能になります。が、出典表示がゼンリンさん(Googleさんの地図の出典表示)のままです。さらには自前で追加した地図の出典表示ができないです。自前の地図を追加するのが必要な場合には、ひっかかるところです。
  • タイル地図オーバレイは透明度の設定ができません。

また、次のような問題点がありました。

  • 地図を激しく動かしてロードしすぎるとメモリ不足で落ちる
  • メモリリークが起きてるっぽい (自分のところが悪い可能性もあるので「っぽい」まで)

本格的な地図アプリのエンジンとしては移行するのに躊躇しています。

余談

  • サーバに伝えられるユーザエージェントは "Dalvik ..." でした。これどうにかならんのかな…。
  • getTileUrl()で"content:..."は指定できませんでした。ていうか new URL(String) の時点で例外投げられた。

Google Play開発者サービスが入ってないっぽい場合

Google Play開発者サービス」が入ってなかったんです、手持ちの端末には。

https://play.google.com/store/apps/details?id=com.google.android.gms&hl=ja によると、「通常、Android搭載端末ではGoogle Playサービスが自動的に更新されます。」とのこと。
さらには、ぐぐってたら「勝手にインストールされた」の声が多かったようです。

手持ちの端末はIS-03で、電池の持ちが悪くて同期をさせてなかったためなのかも知れないです。

とりあえず入れるために端末のPlayストアアプリを開いて検索しても引っかかってきません。

私の場合は、リモートインストールを使ってやりました。パソコンのブラウザで、端末で使っているアカウントでGoogleにログインして、Google Play開発者サービスのページを開いて、「インストール」をクリックして、しばらく待つと端末にインストールされました。