【Android】和歌山トイレマップで遊んでみる 1
今年は国体があるからか、和歌山のニュースを目にすることが多いです(もちろん住んでいるから、というのが大きいでしょうが)。
特に個人的にも気になっていたのが、GitHubを使った情報公開。
今回はその中の一つ、トイレマップを使って何かやってみたいと思います。
やったこと
- CSVファイルとして公開されているトイレマップを読み込み、登録されている名称、住所を取り出す
- 取得した住所から緯度・経度を取得する
- 取得した緯度・経度を使って地図上にマーカーを設置する
はじめに
以前Google Map APIを使ってみたことがありましたが、今回はこれを利用します。
なお、APIキー取得の際に使用するフィンガープリント(SHA1)を出力するときに「keytool -v -list -keystore ~/.android/debug.keystore」を実行しますが、この時聞かれるパスワードは「android」なのだそうです。
使っているPCのパスワードなのかと思い、あれこれ試行錯誤していました汗。
Google Developers ConsoleのUIが変わっていましたが、 Google Map APIを有効にして、フィンガープリント(SHA1)を入力して、という手順は変更ありませんでした。
ソースコード
package jp.searchwakayamatoilet; import android.content.res.AssetManager; import android.location.Address; import android.location.Geocoder; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.support.v4.app.FragmentActivity; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.MarkerOptions; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.List; import java.util.Locale; import java.util.regex.Pattern; public class MainActivity extends FragmentActivity{ private GoogleMap mMap; private MainActivity mMain; private String mStrToiletName; private double mDblLatitude; private double mDblLongitude; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mMain = this; if (mMap == null) { // マップの表示. mMap = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map)).getMap(); mMap.setMyLocationEnabled(true); } // CSVの読み込みとマーカー設置.処理が重いので別スレッドで実行. HandlerThread handlerThread = new HandlerThread("AddMarker"); handlerThread.start(); Handler handler = new Handler(handlerThread.getLooper()); handler.post(new Runnable() { @Override public void run() { Geocoder geocoder = new Geocoder(mMain, Locale.getDefault()); AssetManager asmAsset = mMain.getResources().getAssets(); try { // CSVの読み込み. InputStream ipsInput = asmAsset.open("toilet-map.csv"); InputStreamReader inputStreamReader = new InputStreamReader(ipsInput); BufferedReader bufferReader = new BufferedReader(inputStreamReader); String strLine = ""; String[] strSplited; Pattern p = Pattern.compile("^[0-9]+"); // 1行ずつ読み込む. while ((strLine = bufferReader.readLine()) != null) { // とりあえず数値から始まっている行のみ if (p.matcher(strLine).find()) { // とりあえずSplit後に4件以上データがある行のみ. strSplited = strLine.split(","); if (strSplited.length >= 4) { // とりあえず名称と住所のみ. List addrList = geocoder.getFromLocationName(strSplited[3], 1); if (addrList.isEmpty()) { Log.d("swtSearch", "list is empty"); } else { Address address = addrList.get(0); // UIスレッドで取得したデータを受け取れるようにする. mMain.mStrToiletName = strSplited[1]; mMain.mDblLatitude = address.getLatitude(); mMain.mDblLongitude = address.getLongitude(); // UIスレッドでマーカー設置. getCsvHandler.sendEmptyMessage(1); } } } } bufferReader.close(); ipsInput.close(); } catch (IOException e) { Log.d("swtSearch", "Exception 発生"); } } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } private Handler getCsvHandler = new Handler() { public void handleMessage(Message msg) { addMarker(); } }; public void addMarker() { if (mMap != null) { // 表示したマップにマーカーを追加する. mMap.addMarker(new MarkerOptions().position( new LatLng(mMain.mDblLatitude, mMain.mDblLongitude)).title(mMain.mStrToiletName)); } } }
- とにかく「とりあえず」が多いコメントですが、まずは動けば良い、という精神で作成した結果こうなりました...。
CSVファイルの読み込み
まずtoilet-map.csvをダウンロードしてきて、app > src > mainの下に「assets」というフォルダを作成し、その中に置きます。
そうすると、「AssetManager」を使ってファイルを読み込むことができるようになります。
〜省略〜 AssetManager asmAsset = mMain.getResources().getAssets(); try { // CSVの読み込み. InputStream ipsInput = asmAsset.open("toilet-map.csv"); InputStreamReader inputStreamReader = new InputStreamReader(ipsInput); BufferedReader bufferReader = new BufferedReader(inputStreamReader); 〜省略〜 // 1行ずつ読み込む. while ((strLine = bufferReader.readLine()) != null) { 〜省略〜
これで、「strLine」で「観光地の公衆トイレ一覧,,,,,,,,,,〜」のように1行ずつ分離されたデータが入るようになるので、「strLine.split(",")」で分割してやれば個別のデータが取得出来ます。
問題
ここで以下のような問題につまづきました(未解決)。
- 1行目から取得したいデータが入っているわけではなく、「観光地の公衆トイレ一覧,,,,,,,,,,〜」のようなデータが入っている
- 取得したいデータが途中で改行されている場合があり、単純に1行ずつ読み込むと正しいデータとして取得できない場合がある
- 「観光地の公衆トイレ一覧,,,」という文字列を「,」でSplitした場合、要素数は1となる
まぁ1は項目名など、不要な行は決まっているのでそこを省く(単純にカウンターをつけてその部分の処理をスキップするとか)ことができます。
が、2と3が厄介で、せめてSplit後の要素数が必ず同じなのであれば対処しやすかったのですが...。
とはいえ方法はあるでしょうから、あとはどれだけシンプルな方法が取れるか、ということになりそうです。
今回は正規表現で行の頭が数値で始まっていること、Split後の要素数が4以上であること(取得したい名称が2番目、住所が4番目に入っているため)、後述する住所から緯度・経度を取得する処理で、住所が取得できることを条件として、
取得できたデータのみを扱っています。
取得した住所から緯度・経度を取得する
Geocoderを使って住所から緯度・経度を取得することができます。
Geocoder geocoder = new Geocoder(mMain, Locale.getDefault()); 〜省略〜 // とりあえず名称と住所のみ. List addrList = geocoder.getFromLocationName(strSplited[3], 1); if (addrList.isEmpty()) { Log.d("swtSearch", "list is empty"); } else { Address address = addrList.get(0); // UIスレッドで取得したデータを受け取れるようにする. mMain.mStrToiletName = strSplited[1]; mMain.mDblLatitude = address.getLatitude(); mMain.mDblLongitude = address.getLongitude(); 〜省略〜
UIスレッドでマーカー設置
CSVからのデータロード、緯度・経度の取得、地図上にマーカー設置という処理が、数が多いこともあって結構時間がかかるため、
別スレッドを使って処理を行っています。
ただし、地図上にマーカーを設置するにはUIスレッドで実行する必要があるため、HandlerのsendMessageを使って実行してやります。
〜省略〜 // UIスレッドでマーカー設置. getCsvHandler.sendEmptyMessage(1); } } } } 〜省略〜 private Handler getCsvHandler = new Handler() { public void handleMessage(Message msg) { addMarker(); } }; public void addMarker() { if (mMap != null) { // 表示したマップにマーカーを追加する. mMap.addMarker(new MarkerOptions().position( new LatLng(mMain.mDblLatitude, mMain.mDblLongitude)).title(mMain.mStrToiletName)); } }
課題
これでなんとか(読み込めたデータは)マーカーを置くことができます。
ただ全然関係ないところに(ひどいものは沖縄や東北の方まで)飛んでいっているものもあるので、もう少し精度を良くしたいところです。
また、自動で現在地付近を表示するようにしたり、読み込んだデータをDBに保存するなどして検索できるようにしたり、というところに挑戦してみようと思います。
参考
フィンガープリント(SHA1)の取得
CSVの読み込み
正規表現
別スレッドで実行
- 別スレッドでキュー管理(Handler, Looper, HandlerThread) - プログラマってこんなかんじ??
- Handlerクラスの正しい使い方(Androidでスレッド間通信) - ちくたく