どくぴーの備忘録

真面目なことを書こうとするクソメガネのブログ。いつ投げ捨てられるのかは不明

Retrofit 2 + Google Maps API v2 for Android + Google Places API Web Serviceを使って指定座標付近のスポットを検索する

PlacePickerというものがある

https://developers.google.com/places/android-api/placepicker?hl=ja

PlacePicker には、地理的住所やローカル ビジネスに一致するプレイスを含むインタラクティブなマップや近隣のプレイスの一覧を表示する UI ダイアログが用意されています。ユーザーがプレイスを選択すると、アプリでそのプレイスの詳細を取得できます。

ということで,intentで専用のダイアログに飛ばして,その選択結果(1件のみ)を forActivityResult() で受け取って処理したりするのだが,わざわざ検索で別の画面に飛ばして選択させて帰ってこさせるのがなんか回りくどいと思ったのと,一度に複数件アプリ側のMap上にスポットのピンを立てたい!ということで,直接WebAPIの方を叩けば周辺nメートルのスポットを検索できたりするので,今回試してみることに

まずはじめに

兎にも角にも.Google Maps API v2 for AndroidGoogle Places API Web ServiceをGoogle Developer Consoleで有効にしましょう.Google Maps APIにはAndroidキーが,Google Places APIには(恐らく直接RESTでAPIを叩くため)ブラウザキーが必要です.

有効化ができたら.app側のbuild.gradleのdependenciesに次の記述を追加して,okhttpとretrofit,google play servicesを導入します.

dependencies {
    compile "com.squareup.okhttp:okhttp:2.4.0"
    compile "com.squareup.okhttp:okhttp-urlconnection:2.4.0"

    compile "com.squareup.retrofit:retrofit:2.0.0-beta2"
    compile 'com.squareup.retrofit:converter-gson:2.0.0-beta2'

    compile 'com.google.android.gms:play-services:8.1.0'
}

その後,AndroidManifest.xmlパーミッションを追加します

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

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

また,Google Maps APIを使うためのmeta-dataをapplicationタグ内に追加したりします

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="@string/google_maps_key" />
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

    <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true" />

Responseの受け皿となるPOJOクラスを用意する

Google Places API Web Serviceのレスポンス例は公式サイトで確認できます.

developers.google.com

ということで,これを受け取るためのPOJOクラスを作成します.今回は,後述しますがGsonConverterを使うので,各クラスのメンバには"m"はつけず,レスポンスのJSONの属性名に合わせます.例えば,nameであれば,mNameとするのではなく,nameのままで記述します.

まず,一番深いところにあるLocation.java

public class Location {
    private static final String TAG = Location.class.getSimpleName();
    private final Location self = this;

    private double lat;
    private double lng;

    public Location(double lat, double lng) {
        this.lat = lat;
        this.lng = lng;
    }

    public double getLat() {
        return lat;
    }

    public void setLat(double lat) {
        this.lat = lat;
    }

    public double getLng() {
        return lng;
    }

    public void setLng(double lng) {
        this.lng = lng;
    }
}

続いて,その外側にあるGeometory.java

public class Geometry {
    private static final String TAG = Geometry.class.getSimpleName();
    private final Geometry self = this;

    private Location location;

    public Geometry(Location location) {
        this.location = location;
    }

    public Location getLocation() {
        return location;
    }

    public void setLocation(Location location) {
        this.location = location;
    }
}

更に外側にあるResult.java

public class Result {
    private static final String TAG = Result.class.getSimpleName();
    private final Result self = this;

    private Geometry geometry;
    private String icon;
    private String id;
    private String name;
    private String place_id;
    private String rating;
    private String reference;
    private String[] types;
    private String vicinity;

    public Result(Geometry geometry, String icon, String id, String name, String place_id, String rating, String reference, String[] types, String vicinity) {
        this.geometry = geometry;
        this.icon = icon;
        this.id = id;
        this.name = name;
        this.place_id = place_id;
        this.rating = rating;
        this.reference = reference;
        this.types = types;
        this.vicinity = vicinity;
    }

    public Geometry getGeometry() {
        return geometry;
    }

    public void setGeometry(Geometry geometry) {
        this.geometry = geometry;
    }

    public String getIcon() {
        return icon;
    }

    public void setIcon(String icon) {
        this.icon = icon;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPlace_id() {
        return place_id;
    }

    public void setPlace_id(String place_id) {
        this.place_id = place_id;
    }

    public String getRating() {
        return rating;
    }

    public void setRating(String rating) {
        this.rating = rating;
    }

    public String getReference() {
        return reference;
    }

    public void setReference(String reference) {
        this.reference = reference;
    }

    public String[] getTypes() {
        return types;
    }

    public void setTypes(String[] types) {
        this.types = types;
    }

    public String getVicinity() {
        return vicinity;
    }

    public void setVicinity(String vicinity) {
        this.vicinity = vicinity;
    }
}

最後に,一番外側のResponse.java

public class Response {
    private static final String TAG = Response.class.getSimpleName();
    private final Response self = this;

    private List<Result> results;

    public Response(List<Result> results) {
        this.results = results;
    }

    public List<Result> getResults() {
        return results;
    }

    public void setResults(List<Result> results) {
        this.results = results;
    }
}

このように記述することで,ネストしているJSONレスポンスでもGsonConverterでうまいことパースしてくれるようになります.

RetrofitでAPIを叩く仕組みを作る

まず,使うAPIを記述したServiceのInterfaceを記述します.

/**
 * URL Sample:
 * https://maps.googleapis.com/maps/api/place/search/json
 * ?types=cafe
 * &location=37.787930,-122.4074990
 * &radius=5000
 * &sensor=false
 * &key=YOUR_API_KEY
 */
public interface PlaceApiService {
    @Headers("Accept-Language: ja")
    @GET("/maps/api/place/search/json")
    Call<Response> requestPlaces(@Query("types") String types,
                                     @Query("location") String location,
                                     @Query("radius") String radius,
                                     @Query("sensor") String sensor,
                                     @Query("key") String key);
}

そして,このServiceを利用してPlaces APIにアクセスするためのクラスを用意します.

public class PlacesApiHelper {
    private static final String TAG = PlacesApiHelper.class.getSimpleName();
    private final PlacesApiHelper self = this;

    private Context mContext;

    public PlacesApiHelper(Context context) {
        mContext = context;
    }

    public void requestPlaces(String types, LatLng latLng, int radius, Callback<Response> callback) {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(mContext.getString(R.string.places_api_url))
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        PlaceApiService service = retrofit.create(PlaceApiService.class);

        Call<Response> call = service.requestPlaces(types,
                String.valueOf(latLng.latitude) + "," + String.valueOf(latLng.longitude),
                String.valueOf(radius),
                "false",
                mContext.getString(R.string.google_maps_key_browser));
        call.enqueue(callback);
    }
}

R.string.places_api_url にはPlaces APIのURL(https://maps.googleapis.com)を,R.string.google_maps_key_browser にはDeveloper Consoleで取得したブラウザキーを記述しておきましょう.

ActivityにGoogle Mapを表示させ,ボタンをクリックしたらスポット検索させるようにする.

次のようなLayoutを用意します. SupportMapFragment を利用します.EditTextがありますが今回は使わなかったりします.

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

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/edit_search_word"
        android:layout_toStartOf="@+id/btn_search"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Search"
        android:layout_alignParentTop="true"
        android:layout_alignParentEnd="true"
        android:id="@+id/btn_search"/>

    <fragment xmlns:android="http://schemas.android.com/apk/res/android"
              android:id="@+id/map"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:layout_below="@+id/edit_search_word"
              android:layout_marginTop="5dp"
              android:name="com.google.android.gms.maps.SupportMapFragment"/>

</RelativeLayout>

そしてMainActivityを記述します.今回は始点の座標は決め打ちです.

public class MainActivity extends AppCompatActivity implements OnMapReadyCallback {
    private static final String TAG = PasteActivity.class.getSimpleName();
    private final PasteActivity self = this;

    EditText mSearchEdit;
    Button mSearchButton;

    PlacesApiHelper mHelper;

    private GoogleMap mGoogleMap;
    private SupportMapFragment mMapFragment;
    private LatLng mCurrentLatLng;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mSearchEdit = (EditText) findViewById(R.id.edit_search_word);
        mSearchButton = (Button) findViewById(R.id.btn_search);
        mSearchButton.setOnClickListener(mOnSearchButtonClickListener);

        mHelper = new PlacesApiHelper(this);

        mMapFragment = (SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map);
        mMapFragment.getMapAsync(this);
    }

    @Override
    public void onMapReady(GoogleMap googleMap) {
        mGoogleMap = googleMap;
    }

    private View.OnClickListener mOnSearchButtonClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            mCurrentLatLng = new LatLng(34.694487, 135.19517);
            // Places APIへリクエスト.5000は現在位置からの半径(m)
            mHelper.requestPlaces("food", mCurrentLatLng, 5000, mResultCallback);
        }
    };

    // レスポンスの処理
    private Callback<Response> mResultCallback = new Callback<Response>() {
        @Override
        public void onResponse(retrofit.Response<Response> response, Retrofit retrofit) {
            mGoogleMap.clear();
            // レスポンスからResultのリストを取得
            List<Result> results = response.body().getResults();
            // Resultの数だけピンを立てる
            for(Result r : results) {
                Location location = r.getGeometry().getLocation();
                LatLng latLng = new LatLng(location.getLat(), location.getLng());
                String name = r.getName();
                mGoogleMap.addMarker(new MarkerOptions().position(latLng).title(name));
            }
            mGoogleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(mCurrentLatLng, 15));
        }

        @Override
        public void onFailure(Throwable t) {
            t.printStackTrace();
        }
    };
}

と,ここまでやれば地図に周辺の検索数だけピンを打てるようになります.