地図を利用する
android GoogleMapV2
【目次】 GPS の基本 ... 3 GPS とは ... 3 AGPS とは ... 3 Google Map V2 とは ... 3 現在位置の地図を表示し、緯度経度を表示する ... 4 まずはGoogle Developer で ID を取得します... 4 android の関連ファイルを作成します ... 5 レイアウトを作成 ... 5 プログラムの仕様 ... 6 AndroidManufest.xml ... 7 ソースコード ... 9 緯度経度を入力して、地名に変換する ... 17 layout を作成する ... 17 WEB サービス呼び出しのお作法 ... 18 ソースコード ... 19
GPS の基本
android では GPS を搭載した端末がほとんどです。この GPS を利用して位置情報を取得し たり、地図を表示したりできます。 その基本的な流れを勉強しましょう。GPS とは
GPS とは、アメリカ軍が飛ばしている GPS 衛星を使って、現在位置を正確に取得しようと いうものです。 GPS 衛星の位置は正確に計算できます。そのため、現在の位置で何番の GPS 衛星からの信 号がどの程度で受信できるかがわかれば、逆に地球上での位置を知ることができるのです。 理論的には、3つの衛星の電波が綺麗に受信できれば緯度と経度を4つあれば高度までわ かるようになっている。AGPS とは
ところが、GPS だけで計測すると実は 30 秒以上かかってしまいます。そこで、携帯電話や スマホでは、電話基地局から本来はGPS 衛星から受信すべきデータをデータ通信で受信し てしまい、計測を速くしています。この方法をAGPS といいます。Google Map V2 とは
さて、それらの地図情報を利用するには,android では Google Map を利用します。そして、
現在のバージョンはV2 です。もうすぐ V3 らしいですが、、、
GoogleMap のライブラリをダウンロードしてライブラリに組み込み、さらに Google Developer に登録して、地図サービスが受信できる様にすれば、android で地図情報を利用 することができます。
現在位置の地図を表示し、緯度経度を表示する
では、実際に android のプログラムを作って行きましょう。画面に現在位置の地図を表示
し、その上部に現在位置の緯度、経度を表示します。
まずは Google Developer で ID を取得します
Google Developer のアカウントはあらかじめ取っておいてください
Google Developer のコンソールにログインして、API Project をクリックします。 eclipse のウィンドウメニューの設定ボタンで設定画面を出して
ビルドメニューのSHA1 フィンガープリントをコピーしてから、コンソールの公開 API へ
のアクセスの「新しいキーを作成」ボタンを押して、そこに貼り付けます。そして、その
文字の後ろに;(セミコロン)を打ってから、プロジェクト名を入力します。ここでは
このAPI キーはのちほど AndroidManufest.xml ファイルで使用します。このキーは基本1 プロジェクトで1キー作成します。
android の関連ファイルを作成します
新規のandroid プロジェクト・アプリケーションを作成します。 まず最初に、android のライブラリーを組み込みます。 パッケージ・エクスプローラで作成するプロジェクトを右クリックして、メニューからプ ロパティを選択します。 その Android メニューをクリックして、下段のライブラリーの追加ボタンを押して、 google-play-service_lib を追加します。 これでgoogle map 関連のライブラリーが組み込まれます。レイアウトを作成
次に、画面のレイアウトを作成します。 今回は、一番上の段に緯度経度を表示。そしてその下に地図を表示します。 緯度経度 地図これをres フォルダーの layout で xml ファイルに設定します。 activity_current_map.xml 上のTextView に緯度経度を表示し、下の fragment に地図をだします。
プログラムの仕様
作成するプログラムは、起動されたらまずは、2 時間以内の GPS の履歴のうち一番誤差が 少ない位置を探り、あればその位置で地図を出します。なければ鹿児島市役所を初期値と して表示します。 地図はGoogle Map V2 のライブラリーを使用し、拡大縮小、現在地への移動ができるよう にしておきます。地図上に現在位置を水色の丸に表示し、誤差半径を薄い水色で囲みます。 地図を表示したまま場所を移動すると、10 分に一度または 100m の移動のたびに現在位置 の水色の丸の移動および、緯度経度の表示が更新されます。 ただし、位置の更新は指定した頻度よりは多くなっています。 <TextView android:id="@+id/textview1" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <fragment xmlns:map="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/textview1" class="com.google.android.gms.maps.SupportMapFragment" />AndroidManufest.xml
プロジェクトの実行設定 AndroidManufest.xml つづく こ れ ら の 使 用 許 可 を 設 定 し て お か な い と 、 実 行 時 エ ラ ー に な り ま す 。 ACCESS_COARSE_LOCATION が wifi、ACCESS_FINE_LOCATION が GPS になりま <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="19" /> <permission android:name="jp.co.etlab.map.CurrentMap.permission.MAPS_RECEIVE" android:protectionLevel="signature"/> <uses-permission android:name="jp.co.etlab.map.CurrentMap.permission.MAPS_RECEIVE"/> <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/ > <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.INTERNET"/> <!-- External storage for caching. --><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <!-- My Location --> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <!-- Maps API needs OpenGL ES 2.0. -->
<uses-feature
android:glEsVersion="0x00020000" android:required="true"/>
また、Google Developer で取得した API キーを application の中で meta-data として宣言 します。これが無いと、google の正規ユーザーと認められず、サービスが受けられません。 gms.version は、本等には書いてありませんが、これがないとエラーになる場合があります。 <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <meta-data android:name="com.google.android.maps.v2.API_KEY" android:value="AIzaSyCLKR01biPi5PBa5Jv0TUnahXDYnUe17H4"/> <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
ソースコード
CurrentMap.java つづく package jp.co.etlab.map.CurrentMap; import java.util.List; import android.app.Dialog; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.widget.TextView; import android.widget.Toast; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesClient.ConnectionCallbacks ; import com.google.android.gms.common.GooglePlayServicesClient.OnConnectionFailedL istener; import com.google.android.gms.common.GooglePlayServicesUtil; import com.google.android.gms.location.LocationClient; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.LatLng;FragmentActivity を 引 き 継 ぎ 、 移 動 の 割 り 込 み を 受 け 取 る LocationListener 等 を implements します。 googleMap が 地 図 表 示 、 mLocationClient が 地 図 位 置 取 得 の コ ン ト ロ ー ラ ー 、 locationManager が GPS の位置情報マネージャ _allProvider は、位置情報を取得するプロバイダーで GPS,Wifi などがあり、GPS が取得 できなくてもWifi で大体の位置を取得できる場合もあります。 そして、現在の位置を_location,過去の位置を_beforeLocation に保存します。 /* * 最初に2時間以内の一番正確な場所の地図を表示 * 10分に1回または100mに1回割り込み発生 * resume時には前回取得位置の地図をzomm17で出し、カレントのポイントだけが変化する */
public class CurrentMap extends FragmentActivity implements
OnConnectionFailedListener, LocationListener, ConnectionCallbacks {
GoogleMap googleMap;
private TextView _textview1;
private LocationClient mLocationClient = null; LocationManager locationManager; List<String> _allProvider; Location _location; Location _beforeLocation; SupportMapFragment mapfragment; @Override
public void onCreate(Bundle savedInstanceState) { double[] latlng;
latlng = new double[6];
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_current_map);
onCreate メソッドで起動時の初期化を記述します。
latlng は private メソッドの 10 進の緯度経度を 60 進に変換するための配列です。 _textview1 が、緯度経度を表示するテキストビューになります。
まず、初期化で GooglePlayService が使えるかどうか確認をとります。取れなければエラ
ーを表示して終了です。
サービスが使用できれば、layout の map を地図用に取得して、googlemap を設定します。 また現在位置取得などのコントローラーを画面に表示します。
//Google Play Servicesが使えるかどうかのステータス int status =
GooglePlayServicesUtil.isGooglePlayServicesAvailable(getBaseContext());
if(status!=ConnectionResult.SUCCESS){ // Google Play Services が使えない場合 int requestCode = 10;
Dialog dialog = GooglePlayServicesUtil.getErrorDialog(status, this, requestCode);
dialog.show();
}else {
// Google Play Services が使える場合
//activity_main.xmlのSupportMapFragmentへの参照を取得 SupportMapFragment fm = (SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map); //fragmentからGoogleMap objectを取得 googleMap = fm.getMap(); //Google MapのMyLocationレイヤーを使用可能にする googleMap.setMyLocationEnabled(true);
位置情報の管理情報を取得して、使用できるプロバイダーの中で過去 2 時間以内のもっと も精度が高かった位置を初期位置に取得するメソッドを呼び出します。 取得できたら、その位置を60 進に変換するメソッドを呼び出し、_textview1 に表示します。 2 時間以内にない場合は、鹿児島市役所を初期位置にします。 //システムサービスのLOCATION_SERVICEからLocationManager objectを取得 locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); //可能なプロバイダーをリスト取得 _allProvider = locationManager.getAllProviders(); //2時間以内のできるだけ正確な位置を取得 _beforeLocation = checkKnownLocation(_allProvider,1000*60*120); if(_beforeLocation != null){ latlng = binToMS(_beforeLocation); _textview1.setText("緯 度:"+(int)latlng[0]+"°"+(int)latlng[1]+"'"+(int)latlng[2]+"¥" 経 度:"+(int)latlng[3]+"°"+(int)latlng[4]+"'"+(int)latlng[5]+"¥""); _location = _beforeLocation; } else {
_location = new Location("dummyprovider");
_location.setLatitude(31.596651); //鹿児島市役所に初期化 _location.setLongitude(130.557103); } } }
このメソッドは、使用可能なプロバイダーのリストを検索し、その中で指定時間以内のも っとも精度が高かった位置を返すものです。
このメソッドは、使用可能なプロバイダーに対して、10 分または100m以上の変化に対 して、通知を依頼するものです。resume のタイミング等で使用します。
//指定の時間以内でもっとも、精度の高いものを取得する
private Location checkKnownLocation(List<String> allProvider ,long time){ Location rlocation = null;
for(int i = 0 ; i < allProvider.size() ; i++){ Location tmplocation =
locationManager.getLastKnownLocation(allProvider.get(i)); if(tmplocation != null){ if(tmplocation.getTime() > System.currentTimeMillis() - time){ if(rlocation == null){ rlocation = tmplocation; }else if(rlocation.getAccuracy() > tmplocation.getAccuracy()){ rlocation = tmplocation; } } } } return rlocation; } //使用可能なプロバイダのupdateの依頼をする
private void requestUpdates(List<String> allProvider){
for(int i = 0 ; i < allProvider.size() ; i++){ //10分か100m locationManager.requestLocationUpdates(allProvider.get(i), 1000*60*10, 100, this);
} }
ポーズされた時は、GPS の検索を中止します。
逆に、リジュームした時は、一旦地図の更新を止めてから、再度割り込みの設定をして、 取得済みの現在の場所に地図の中心を合わせます。
@Override
public void onPause(){ super.onPause();
locationManager.removeUpdates(CurrentMap.this); if (mLocationClient != null) {
// Google Play Servicesを切断 mLocationClient.disconnect(); }
}
@Override
public void onResume() { super.onResume();
mLocationClient.connect();
locationManager.removeUpdates(CurrentMap.this); requestUpdates(_allProvider);
//起動毎にその場所に合わせる
CameraPosition cameraPos = new CameraPosition.Builder() .target(new LatLng(_location.getLatitude(), _location.getLongitude())) .zoom(17.0f) .bearing(0) .build(); googleMap.animateCamera(CameraUpdateFactory.newCameraPosition(came raPos)); }
このモジュールは、10 進の緯度経度を 60 進に変換するものです。 /* * 緯度経度の10進を60進に変換する * 入力:Location 10進 * 出力:doubleの配列 [0]:緯度の度 [1]:緯度の分 [2]:緯度の秒 [3]:経度の度 [4]: 経度の分 [5]:経度の秒 */
private double[] binToMS(Location plocation) { double[] p; p = new double[6]; double latW; double lngW; p[0] = plocation.getLatitude(); p[3] = plocation.getLongitude(); latW = p[0] - (int)p[0]; lngW = p[3] - (int)p[3]; p[1] = latW * 60.0; p[4] = lngW * 60.0; latW = p[1] -(int)p[1]; lngW = p[4] -(int)p[4]; p[2] = latW * 60.0; p[5] = lngW * 60.0; return p; }
これらのメソッドは、implements で必要になったものです。 これで1プロジェクト全部です。 地図を表示すること自体は、googlemap がやってくれますので、それほど心配はいりませ んが、プロバイダーの処理等が面倒です。 https://dl.dropboxusercontent.com/u/62148262/index.html こちらからapk をダウンロードできます。 @Override
public void onConnectionFailed(ConnectionResult result) {
Toast.makeText(this, "Connection Failed", Toast.LENGTH_LONG).show(); }
@Override
public void onConnected(Bundle connectionHint) { }
@Override
public void onDisconnected() {
Toast.makeText(this, "Disconnected", Toast.LENGTH_LONG).show(); }
@Override
public void onProviderDisabled(String provider) { }
@Override
public void onProviderEnabled(String provider) { }
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
} }
緯度経度を入力して、地名に変換する
では、次に任意の緯度経度(10 進)を入力して、その場所の地名を取得するプログラムを 作ってみましょう。layout を作成する
10進の(31.5858254 等)緯度と経度を入力して、地名取得ボタンを押すと、ダイアログ で地名を出力して終了します。 <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/lbIdo" /> <EditText android:id="@+id/etLat" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ems="10" android:inputType="numberDecimal">" <requestFocus /> </EditText> </LinearLayout> 緯度 経度 地名取得WEB サービス呼び出しのお作法
このプロジェクトでは、前回のプロジェクトと違って、WEB サービスに対して文字を送っ て、その結果を受け取るという流れになっています。 このようなプログラムを作るときには、結果が送られてくるまで時間が掛かります。その ため、それを待っているとプログラムがハングした状態になり、もしも返事が返ってこな ければ、だまったきりになってしまいます。 そこで、そのようなプログラムではAsyncTask という非同期の通信を受信するクラスを呼 び出します。今回は呼び出し側をReverseGeo.java として、AsynTask 側を AsyncHttpRequest.java と
して作ります。 AsyncTask 側は、実行依頼時、実行中、終了時にそれぞれモジュールがあり、それらのパ <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/lbKeido" /> <EditText android:id="@+id/etLng" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ems="10" android:inputType="numberDecimal" />" </LinearLayout> <Button android:id="@+id/bt" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/buttonText" />
今回は、実行依頼時にurl を渡します。これは Google の地名変換サービスの url とそのサ ービスへのパラメータが文字列だからです。そして、実行中はなし、終了時にjson データ が返ってくるので、それを文字列で受信します。 時間の掛かる処理をdoInBackground で、終了時が onPostExecute です。
呼び出し側ソースコード
では、呼び出し側のreveseGeo からソースを見てみましょう。 reverseGeo.java自分で作成するAsyncTask を Extends したクラスを AsyncHttpRequest として、そのオブ
ジェクト変数を作成しておきます。 Layout を取り込み、エラーが発生しないように一度 AsyncTask にタッチしておきます。 package jp.co.etlab.map; import android.app.Activity; import android.os.Bundle; import android.text.SpannableStringBuilder; import android.view.View; import android.view.View.OnClickListener; import android.widget.EditText;
public class ReverseGeo extends Activity { AsyncHttpRequest task;
@Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_reverse_geo); //とりあえず一度 AsyncTask に触れてやる try{ Class.forName("android.os.AsyncTask"); }catch(ClassNotFoundException e){}
ボタンを押した時の割り込み処理を onClick で宣言し、その内部で入力された緯度経度を 文字列に変換し、google API に送る文字列を作り、AsyncHttpRequest の execute に渡し ます。場合によりますが1秒程度掛かります。
呼び出し側は以上です。
task = new AsyncHttpRequest(this);
findViewById(R.id.bt).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO 自動生成されたメソッド・スタブ EditText editLat =
(EditText)findViewById(R.id.etLat);
SpannableStringBuilder spLat = (SpannableStringBuilder)editLat.getText();
String strLat = spLat.toString(); EditText editLng = (EditText)findViewById(R.id.etLng); SpannableStringBuilder spLng = (SpannableStringBuilder)editLng.getText(); String strLng = spLng.toString(); String uri = "http://maps.googleapis.com/maps/api/geocode/json" + "?latlng=" + strLat + "," + strLng + "&sensor=true&language=ja"; task.execute(uri); } }); } }
非同期側ソースコード
AsyncHttpRequest.java AsyncTask を拡張した AsyncHttpRequest を宣言します。 コンストラクタで呼び出し元の画面コントロールをもらえば、画面への出力もできます。 package jp.co.etlab.map; import java.io.IOException; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.util.EntityUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.os.AsyncTask;public class AsyncHttpRequest extends AsyncTask<String, Void, String> { public Activity owner;
private String ReceiveStr;
public AsyncHttpRequest(Activity activity) { owner = activity;
帰ってきたjson コードからパースした文字列を終了処理に return します。 @Override
protected String doInBackground(String... uri) { HttpClient httpClient = new DefaultHttpClient(); HttpGet request = new HttpGet(uri[0]);
try {
httpClient.execute(request, new ResponseHandler<String>() { @Override
public String handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
// response.getStatusLine().getStatusCode()でレスポンスコード を判定する。
// 正常に通信できた場合、HttpStatus.SC_OK(HTTP 200)となる。 switch (response.getStatusLine().getStatusCode()) {
case HttpStatus.SC_OK:
// レスポンスデータを文字列として取得する。 // byte[]として読み出したいときは
EntityUtils.toByteArray()を使う。 ReceiveStr =
json_parse(EntityUtils.toString(response.getEntity(), "UTF-8")); return ReceiveStr;
case HttpStatus.SC_NOT_FOUND:
throw new RuntimeException("データないよ!"); //FIXME default:
throw new RuntimeException("なんか通信エラーでた"); //FIXME } } }); } catch (ClientProtocolException e) { throw new RuntimeException(e); //FIXME } catch (IOException e) {
throw new RuntimeException(e); //FIXME } finally { // ここではfinallyでshutdown()しているが、HttpClientを使い回す場合 は、 // 適切なところで行うこと。当然だがshutdown()したインスタンスは通信で きなくなる。 httpClient.getConnectionManager().shutdown(); } return ReceiveStr; }
json コードとは Javascript で使用するデータコードで括弧を多重化してデータを表現して
いるためxml と同様の表現ができる。
上のjson コードが、Google service から帰ってきた json コードで、地名の変換ができれば
この中の formatted_address が変換された地名となります。
{
"results" : [
{
"address_components" : [
{
"long_name" : "16",
"short_name" : "16",
"types" : [ "sublocality_level_4", "sublocality",
"political" ]
},
{
"long_name" : "2",
"short_name" : "2",
"types" : [ "sublocality_level_3", "sublocality",
"political" ]
},
{
"long_name" : "松原町",
"short_name" : "松原町",
"types" : [ "sublocality_level_1", "sublocality",
"political" ]
},
{
"long_name" : "鹿児島市",
"short_name" : "鹿児島市",
"types" : [ "locality", "political" ]
},
"formatted_address" : "日本, 鹿児島県鹿児島市松原町2",
"geometry" : {
"bounds" : {
"northeast" : {
"lat" : 31.5867563,
"lng" : 130.5577355
},
"southwest" : {
"lat" : 31.585326,
"lng" : 130.5566022
}
},
"location" : {
"lat" : 31.5859353,
"lng" : 130.5571133
},
json コードの formatted_address から文字列を抽出するのが下のモジュール,json_parse で
抽出した文字列がReceiveStr に代入されます。
private String json_parse(String str) { try {
String res = "";
JSONObject rootObject = new JSONObject(str);
JSONArray eventArray = rootObject.getJSONArray("results"); for (int i = 0; i < eventArray.length(); i++) {
JSONObject jsonObject = eventArray.getJSONObject(i); res = jsonObject.getString("formatted_address"); if (!res.equals("")){ return res; } } } catch (JSONException e) {
// TODO Auto-generated catch block e.printStackTrace();
}
return ""; }
onPostExecute で終了時の処理を行い、さきほどのdoInBackground から return で戻され た String をダイアログ表示して、終了します。
protected void onPostExecute(String result) { //親のスレッドに出す場合
// TextView tv = (TextView)owner.findViewById(R.id.tv);
// tv.setText(ReceiveStr);
// owner.finish();
AlertDialog.Builder alertDialog=new AlertDialog.Builder(owner); // ダイアログの設定 alertDialog.setTitle("地名"); //タイトル設定 alertDialog.setMessage(result); //内容(メッセージ)設定 // OK(肯定的な)ボタンの設定 alertDialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { // OKボタン押下時の処理 owner.finish(); } }); alertDialog.show(); }