Google Places API 웹 서비스 예
Google Places API를 사용하여 주변 장소를 찾을 수 있습니다. 이 자습서에서는 현재 위치에서 대략적인 거리 및 시간과 함께 선택한 주변 장소를 표시하는 애플리케이션을 개발할 것입니다. 애플리케이션에서 Distance Matrix API와 함께 Google Places API 웹 서비스를 사용할 것입니다.
Google 장소 API
Google Places API 웹 서비스를 사용하면 장소 유형, 장소가 현재 영업 중인지 여부 등과 같은 몇 가지 매개변수를 기반으로 장소를 쿼리할 수 있습니다. Nearby Search 요청은 다음 형식의 HTTP URL입니다.
https://maps.googleapis.com/maps/api/place/nearbysearch/output?parameters
json
은 권장되는 출력
이고 다른 하나는 xml
입니다. 필수 매개변수는 다음과 같습니다.
- 키(API 키)
- 위치
- rankby=distance 또는 radius : 하나를 사용하면 다른 하나를 사용할 수 없습니다.
참고: rankby=distance
에는 다음 매개변수 중 하나를 지정해야 합니다.
- 이름: 값은 mcdonalds, kfc 등일 수 있습니다.
- 유형: 값은 레스토랑, 카페 등일 수 있습니다.
- 키워드
선택적 매개변수는 opennow
, pagetoken
등일 수 있습니다. 자세한 내용은 이 페이지를 참조하세요.
Google 거리 매트릭스 API
Distance Matrix API
는 둘 이상의 지점 사이의 거리와 시간을 계산하는 데 사용됩니다. 거리 행렬 API URL은 다음과 같은 형식입니다.
https://maps.googleapis.com/maps/api/distancematrix/outputFormat?parameters
필수 매개변수는 origins
, destinations
및 key
입니다. origins — 여기에는 이동 거리 및 시간을 계산하기 위한 시작점이 포함됩니다. 파이프라인(|)으로 구분된 두 개 이상의 좌표 집합을 전달할 수 있습니다. 좌표 대신 주소/장소 ID를 전달할 수도 있으며 서비스에서 자동으로 위도-경도 좌표로 변환하여 거리와 기간을 계산합니다. 샘플 코드:
https://maps.googleapis.com/maps/api/distancematrix/json?origins=Washington,DC&destinations=New+York+City,NY&key=YOUR_API_KEY
선택적 매개변수는 다음과 같습니다.
- mode :
driving
,bicycling
,walking
,transit
사이의 값을 기대합니다.\n - avoid :
통행료
,실내
등과 같은 경로 제한을 도입
자세한 내용은 이 페이지를 방문하세요.
API 키 활성화
https://console.developers.google.com/
으로 이동하여 다음 API를 활성화합니다.
- Google 지도 거리 매트릭스 API
- Google Places API 웹 서비스
- Android용 Google Places API
자격 증명으로 이동하여 새 키를 만듭니다. 지금은 키 제한을 없음으로 설정하십시오. 이 튜토리얼의 비즈니스 끝으로 이동하겠습니다. 우리는 현재 위치를 기반으로 주변 장소를 검색하고 RecyclerView에 장소를 표시할 수 있는 애플리케이션을 개발할 것입니다. EditText에 입력되고 공백으로 구분되는 유형 및 이름 키워드를 기반으로 장소를 검색합니다. 예: 레스토랑 도미노 또는 카페 채식주의자
Google Places API 예제 프로젝트 구조
Google Places API 예제 코드
build.gradle
파일 내에 다음 종속성을 추가합니다.
compile 'com.google.android.gms:play-services-location:10.2.1'
compile 'com.google.android.gms:play-services-places:10.2.1'
compile 'com.google.code.gson:gson:2.7'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
compile 'com.squareup.okhttp3:okhttps:3.4.1'
compile 'io.nlopez.smartlocation:library:3.3.1'
compile 'com.android.support:cardview-v7:25.3.0'
compile 'com.android.support:recyclerview-v7:25.3.0'
compile io.nlopez.smartlocation:library:3.3.1
은 상용구 코드를 줄이는 LocationTracking 타사 라이브러리입니다. APIClient.java 코드는 다음과 같습니다.
package com.journaldev.nearbyplaces;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class APIClient {
private static Retrofit retrofit = null;
public static final String GOOGLE_PLACE_API_KEY = "ADD_YOUR_API_KEY_HERE";
public static String base_url = "https://maps.googleapis.com/maps/api/";
public static Retrofit getClient() {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder().readTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).addInterceptor(interceptor).build();
retrofit = null;
retrofit = new Retrofit.Builder()
.baseUrl(base_url)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
return retrofit;
}
}
ApiInterface.java 코드는 다음과 같습니다.
package com.journaldev.nearbyplaces;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
public interface ApiInterface {
@GET("place/nearbysearch/json?")
Call<PlacesPOJO.Root> doPlaces(@Query(value = "type", encoded = true) String type, @Query(value = "location", encoded = true) String location, @Query(value = "name", encoded = true) String name, @Query(value = "opennow", encoded = true) boolean opennow, @Query(value = "rankby", encoded = true) String rankby, @Query(value = "key", encoded = true) String key);
@GET("distancematrix/json") // origins/destinations: LatLng as string
Call<ResultDistanceMatrix> getDistance(@Query("key") String key, @Query("origins") String origins, @Query("destinations") String destinations);
}
PlacesPOJO.java는 Places API의 응답을 저장하는 파일입니다. 코드는 다음과 같습니다.
package com.journaldev.nearbyplaces;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class PlacesPOJO {
public class Root implements Serializable {
@SerializedName("results")
public List<CustomA> customA = new ArrayList<>();
@SerializedName("status")
public String status;
}
public class CustomA implements Serializable {
@SerializedName("geometry")
public Geometry geometry;
@SerializedName("vicinity")
public String vicinity;
@SerializedName("name")
public String name;
}
public class Geometry implements Serializable{
@SerializedName("location")
public LocationA locationA;
}
public class LocationA implements Serializable {
@SerializedName("lat")
public String lat;
@SerializedName("lng")
public String lng;
}
}
ResultDistanceMatrix.java 클래스는 Distance Matrix API의 응답을 보유합니다. 코드는 다음과 같습니다.
package com.journaldev.nearbyplaces;
import com.google.gson.annotations.SerializedName;
import java.util.List;
public class ResultDistanceMatrix {
@SerializedName("status")
public String status;
@SerializedName("rows")
public List<InfoDistanceMatrix> rows;
public class InfoDistanceMatrix {
@SerializedName("elements")
public List elements;
public class DistanceElement {
@SerializedName("status")
public String status;
@SerializedName("duration")
public ValueItem duration;
@SerializedName("distance")
public ValueItem distance;
}
public class ValueItem {
@SerializedName("value")
public long value;
@SerializedName("text")
public String text;
}
}
}
activity_main.xml 파일은 다음과 같습니다.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#212121"
tools:context="com.journaldev.nearbyplaces.MainActivity">
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:textColor="@android:color/white"
android:textColorHint="@android:color/white"
android:text="restaurant mcdonalds"
android:hint="type name"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@+id/button"
android:layout_toStartOf="@+id/button" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:text="Search" />
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/editText"
android:scrollbars="vertical" />
</RelativeLayout>
MainActivity.java
클래스 코드는 다음과 같습니다.
package com.journaldev.nearbyplaces;
import android.annotation.TargetApi;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.location.Location;
import android.os.Build;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.google.android.gms.maps.model.LatLng;
import java.util.ArrayList;
import java.util.List;
import io.nlopez.smartlocation.OnLocationUpdatedListener;
import io.nlopez.smartlocation.SmartLocation;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
public class MainActivity extends AppCompatActivity {
private ArrayList<String> permissionsToRequest;
private ArrayList<String> permissionsRejected = new ArrayList<>();
private ArrayList<String> permissions = new ArrayList<>();
private final static int ALL_PERMISSIONS_RESULT = 101;
List<StoreModel> storeModels;
ApiInterface apiService;
String latLngString;
LatLng latLng;
RecyclerView recyclerView;
EditText editText;
Button button;
List<PlacesPOJO.CustomA> results;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
permissions.add(ACCESS_FINE_LOCATION);
permissions.add(ACCESS_COARSE_LOCATION);
permissionsToRequest = findUnAskedPermissions(permissions);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (permissionsToRequest.size() > 0)
requestPermissions(permissionsToRequest.toArray(new String[permissionsToRequest.size()]), ALL_PERMISSIONS_RESULT);
else {
fetchLocation();
}
} else {
fetchLocation();
}
apiService = APIClient.getClient().create(ApiInterface.class);
recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
recyclerView.setNestedScrollingEnabled(false);
recyclerView.setHasFixedSize(true);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
editText = (EditText) findViewById(R.id.editText);
button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String s = editText.getText().toString().trim();
String[] split = s.split("\\s+");
if (split.length != 2) {
Toast.makeText(getApplicationContext(), "Please enter text in the required format", Toast.LENGTH_SHORT).show();
} else
fetchStores(split[0], split[1]);
}
});
}
private void fetchStores(String placeType, String businessName) {
/**
* For Locations In India McDonalds stores aren't returned accurately
*/
//Call<PlacesPOJO.Root> call = apiService.doPlaces(placeType, latLngString,"\""+ businessName +"\"", true, "distance", APIClient.GOOGLE_PLACE_API_KEY);
Call<PlacesPOJO.Root> call = apiService.doPlaces(placeType, latLngString, businessName, true, "distance", APIClient.GOOGLE_PLACE_API_KEY);
call.enqueue(new Callback<PlacesPOJO.Root>() {
@Override
public void onResponse(Call<PlacesPOJO.Root> call, Response<PlacesPOJO.Root> response) {
PlacesPOJO.Root root = response.body();
if (response.isSuccessful()) {
if (root.status.equals("OK")) {
results = root.customA;
storeModels = new ArrayList<>();
for (int i = 0; i < results.size(); i++) {
if (i == 10)
break;
PlacesPOJO.CustomA info = results.get(i);
fetchDistance(info);
}
} else {
Toast.makeText(getApplicationContext(), "No matches found near you", Toast.LENGTH_SHORT).show();
}
} else if (response.code() != 200) {
Toast.makeText(getApplicationContext(), "Error " + response.code() + " found.", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<PlacesPOJO.Root> call, Throwable t) {
// Log error here since request failed
call.cancel();
}
});
}
private ArrayList<String> findUnAskedPermissions(ArrayList<String> wanted) {
ArrayList<String> result = new ArrayList<>();
for (String perm : wanted) {
if (!hasPermission(perm)) {
result.add(perm);
}
}
return result;
}
private boolean hasPermission(String permission) {
if (canMakeSmores()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED);
}
}
return true;
}
private boolean canMakeSmores() {
return (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1);
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case ALL_PERMISSIONS_RESULT:
for (String perms : permissionsToRequest) {
if (!hasPermission(perms)) {
permissionsRejected.add(perms);
}
}
if (permissionsRejected.size() > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (shouldShowRequestPermissionRationale(permissionsRejected.get(0))) {
showMessageOKCancel("These permissions are mandatory for the application. Please allow access.",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(permissionsRejected.toArray(new String[permissionsRejected.size()]), ALL_PERMISSIONS_RESULT);
}
}
});
return;
}
}
} else {
fetchLocation();
}
break;
}
}
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
new AlertDialog.Builder(MainActivity.this)
.setMessage(message)
.setPositiveButton("OK", okListener)
.setNegativeButton("Cancel", null)
.create()
.show();
}
private void fetchLocation() {
SmartLocation.with(this).location()
.oneFix()
.start(new OnLocationUpdatedListener() {
@Override
public void onLocationUpdated(Location location) {
latLngString = location.getLatitude() + "," + location.getLongitude();
latLng = new LatLng(location.getLatitude(), location.getLongitude());
}
});
}
private void fetchDistance(final PlacesPOJO.CustomA info) {
Call<ResultDistanceMatrix> call = apiService.getDistance(APIClient.GOOGLE_PLACE_API_KEY, latLngString, info.geometry.locationA.lat + "," + info.geometry.locationA.lng);
call.enqueue(new Callback<ResultDistanceMatrix>() {
@Override
public void onResponse(Call<ResultDistanceMatrix> call, Response<ResultDistanceMatrix> response) {
ResultDistanceMatrix resultDistance = response.body();
if ("OK".equalsIgnoreCase(resultDistance.status)) {
ResultDistanceMatrix.InfoDistanceMatrix infoDistanceMatrix = resultDistance.rows.get(0);
ResultDistanceMatrix.InfoDistanceMatrix.DistanceElement distanceElement = infoDistanceMatrix.elements.get(0);
if ("OK".equalsIgnoreCase(distanceElement.status)) {
ResultDistanceMatrix.InfoDistanceMatrix.ValueItem itemDuration = distanceElement.duration;
ResultDistanceMatrix.InfoDistanceMatrix.ValueItem itemDistance = distanceElement.distance;
String totalDistance = String.valueOf(itemDistance.text);
String totalDuration = String.valueOf(itemDuration.text);
storeModels.add(new StoreModel(info.name, info.vicinity, totalDistance, totalDuration));
if (storeModels.size() == 10 || storeModels.size() == results.size()) {
RecyclerViewAdapter adapterStores = new RecyclerViewAdapter(results, storeModels);
recyclerView.setAdapter(adapterStores);
}
}
}
}
@Override
public void onFailure(Call<ResultDistanceMatrix> call, Throwable t) {
call.cancel();
}
});
}
}
위의 코드에서는 런타임 권한을 요청한 다음 SmartLocation 라이브러리를 사용하여 현재 위치를 가져오는 것으로 시작합니다. 일단 해당 위치에 있으면 유형의 EditText에서 첫 번째 단어를 전달하고 결국 Google Places API 웹 서비스를 호출하는 fetchStores()
메서드의 이름 매개변수에서 두 번째 단어를 전달합니다. 우리는 검색 결과를 10개로 제한합니다. 각 결과에 대해 fetchDistance()
메서드 내에서 상점으로부터의 거리와 시간을 계산합니다. 모든 상점에 대해 완료되면 StoreModel.java
데이터 클래스를 사용하여 RecyclerViewAdapter.java
클래스 내부의 데이터를 채웁니다. StoreModel.java 코드는 다음과 같습니다.
package com.journaldev.nearbyplaces;
public class StoreModel {
public String name, address, distance, duration;
public StoreModel(String name, String address, String distance, String duration) {
this.name = name;
this.address = address;
this.distance = distance;
this.duration = duration;
}
}
RecyclerView의 각 행에 대한 레이아웃은 아래 xml에 제공됩니다. store_list_row.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/activity_horizontal_margin"
android:orientation="vertical">
<android.support.v7.widget.CardView xmlns:card_view="https://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardCornerRadius="0dp"
card_view:cardElevation="5dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="5dp">
<TextView
android:id="@+id/txtStoreName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="5dp"
android:textColor="#212121" />
<TextView
android:id="@+id/txtStoreAddr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="5dp"
android:textColor="#212121" />
<TextView
android:id="@+id/txtStoreDist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="5dp" />
</LinearLayout>
</android.support.v7.widget.CardView>
</LinearLayout>
RecyclerViewAdapter.java 코드는 다음과 같습니다.
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.MyViewHolder> {
private List<PlacesPOJO.CustomA> stLstStores;
private List<StoreModel> models;
public RecyclerViewAdapter(List<PlacesPOJO.CustomA> stores, List<StoreModel> storeModels) {
stLstStores = stores;
models = storeModels;
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.store_list_row, parent, false);
return new MyViewHolder(view);
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
holder.setData(stLstStores.get(holder.getAdapterPosition()), holder, models.get(holder.getAdapterPosition()));
}
@Override
public int getItemCount() {
return Math.min(5, stLstStores.size());
}
public class MyViewHolder extends RecyclerView.ViewHolder {
TextView txtStoreName;
TextView txtStoreAddr;
TextView txtStoreDist;
StoreModel model;
public MyViewHolder(View itemView) {
super(itemView);
this.txtStoreDist = (TextView) itemView.findViewById(R.id.txtStoreDist);
this.txtStoreName = (TextView) itemView.findViewById(R.id.txtStoreName);
this.txtStoreAddr = (TextView) itemView.findViewById(R.id.txtStoreAddr);
}
public void setData(PlacesPOJO.CustomA info, MyViewHolder holder, StoreModel storeModel) {
this.model = storeModel;
holder.txtStoreDist.setText(model.distance + "\n" + model.duration);
holder.txtStoreName.setText(info.name);
holder.txtStoreAddr.setText(info.vicinity);
}
}
}
Call call = apiService.doPlaces(placeType, latLngString,"\""+ businessName +"\"", true, "distance", APIClient.GOOGLE_PLACE_API_KEY);
Google Places API 예제 프로젝트 다운로드