読者です 読者をやめる 読者になる 読者になる

vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Android】和歌山トイレマップで遊んでみる 1

今年は国体があるからか、和歌山のニュースを目にすることが多いです(もちろん住んでいるから、というのが大きいでしょうが)。

特に個人的にも気になっていたのが、GitHubを使った情報公開。
今回はその中の一つ、トイレマップを使って何かやってみたいと思います。

github.com

やったこと

  • 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行目から取得したいデータが入っているわけではなく、「観光地の公衆トイレ一覧,,,,,,,,,,〜」のようなデータが入っている
  2. 取得したいデータが途中で改行されている場合があり、単純に1行ずつ読み込むと正しいデータとして取得できない場合がある
  3. 「観光地の公衆トイレ一覧,,,」という文字列を「,」で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の読み込み

正規表現

別スレッドで実行

住所から緯度・経度を取得