GPXファイルを読み込む(アンドロイド地図アプリの開発 その13)

前回の記事でようやくGPSで現在地が表示できるようなったことを書きました。
alasixosaka.hatenablog.com


これで、一応地図アプリとしての最低限の機能はするようになった。
そこで、次のステップとして、ルートラボなどからGPX形式で書きだしたルートデータを読み込んで表示させる機能を追加してみようと思う。
ルートを表示する部分は簡単で、すでにOverlayMapViewerに経度と緯度を打ち込んで線を書くという例が載っているのでその機能を使えばよい。
GPXのデータからポイントの経度、緯度が読み込めれば問題なくルートが引けるはずだ。
問題は、GPXファイルをどうやって読むかということになる。

GPXファイルの形式はXMLと同じ

そこで、GPXファイルの形式について調べてみたが、基本的にはXMLファイルと同じ形式になっているとのこと。
試しにカシミール3Dで簡単なGPXファイルを作ってみたら次のようになっていた。

<?xml version="1.0" encoding="UTF-8"?>
<gpx
version="1.1"
creator="Kashmir3D 9.340 - http://www.kashmir3d.com"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:kashmir3d="http://www.kashmir3d.com/namespace/kashmir3d"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd
	http://www.kashmir3d.com/namespace/kashmir3d http://www.kashmir3d.com/namespace/kashmir3d.xsd">
<rte>
 <name>test</name>
 <number>1</number>
 <extensions>
 <kashmir3d:line_color>0000ff</kashmir3d:line_color>
 <kashmir3d:line_size>2</kashmir3d:line_size>
 <kashmir3d:line_style>1</kashmir3d:line_style>
 </extensions>
<rtept lat="34.854105" lon="135.473469">
 <ele>-0.001000</ele>
 <time>2019-09-21T07:12:51Z</time>
 <name>AA0001</name>
 <extensions>
 <kashmir3d:icon>901001</kashmir3d:icon>
 </extensions>
</rtept>
<rtept lat="34.855633" lon="135.480765">
 <ele>-0.001000</ele>
 <time>2019-09-21T07:13:01Z</time>
 <name>AA0002</name>
 <extensions>
 <kashmir3d:icon>901001</kashmir3d:icon>
 </extensions>
</rtept>
</rte>
</gpx>

ぐちゃぐちゃっといっぱい書いてあるが、肝心なのは、rteptと/rteptの間の部分。lat="xxxxx", lon="xxxxx"となっている部分が緯度と経度を表している。
ele xxxxx /eleの部分は標高を表しているが、今回はカシミール3Dの地図を無料で使える地理院地図にしたので、標高のデータがないために、ほぼ0mの値となっている。
標高については、後々考察したいと考えているが、今回はとにかく経度と緯度だけ読めればそれで問題ない。
AndorioでGPXファイルを読み込むアプリを書いた記事が見つけられなかったので、XMLファイルを読み込む記事を参考にした。
XMLファイルの読み込みはAPIがちゃんとあってそれを使えば割と簡単にできそうだ。

GPXファイルを読み込んで、緯度と経度を表示してみる。

とりあえず、参考サイトのプログラムを参考にして、GPXファイルの中身を解析してListViewに表示するプログラムを書いてみた。
MainActivityは下記のようになった。

public class MainActivity extends AppCompatActivity {

    ListView listView;

    private File file;
    private final int REQUEST_PERMISSION = 1000;

    private String AtName;
    private String AtValue;
    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);



        File path = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
        String testfile = "test.gpx" ;
        file = new File(path, testfile);

        // Android 6, API 23以上でパーミッシンの確認
        if (Build.VERSION.SDK_INT >= 23) {
            checkPermission();
        } else {
            setUpReadWriteExternalStorage();
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    private void checkPermission() {
        // 既に許可している
        if (ActivityCompat.checkSelfPermission(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
                PackageManager.PERMISSION_GRANTED){
            setUpReadWriteExternalStorage();
        }
        // 拒否していた場合
        else{
            requestLocationPermission();
        }
    }
    private void requestLocationPermission() {
        if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
            ActivityCompat.requestPermissions(MainActivity.this,
                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_PERMISSION);

        } else {
            Toast toast =
                    Toast.makeText(this, "アプリ実行に許可が必要です", Toast.LENGTH_SHORT);
            toast.show();

            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,},
                    REQUEST_PERMISSION);
        }
    }


    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    private void setUpReadWriteExternalStorage() {
        listView = (ListView) findViewById(R.id.listview);

        ArrayList<Food> list = new ArrayList<>();
        MyAdapter myAdapter = new MyAdapter(MainActivity.this);

        myAdapter.setFoodList(list);
        listView.setAdapter((ListAdapter) myAdapter);
        try {
            String listXmlPath = Environment.getExternalStorageDirectory().getPath() + "/test.gpx";
            String content = new Scanner(
                    new File(listXmlPath)).useDelimiter("\\z").next();
            //XMLファイルをまとめて読み込み
            XmlPullParser xpp = Xml.newPullParser();

            xpp.setInput(new StringReader(content));
            //解析するXMLファイルの中身を渡す
            int eventType = xpp.getEventType();
            while (eventType != XmlPullParser.END_DOCUMENT) {
                switch (eventType) {
                    case XmlPullParser.START_DOCUMENT:
                        Log.i("MainActivity", "ドキュメント開始");
                        break;
                    case XmlPullParser.START_TAG:
                        Log.i("MainActivity", xpp.getName() + "要素開始");
                        int attrCount = xpp.getAttributeCount();
                        for (int i = 0; i < attrCount; ++i) {
                            AtName = xpp.getAttributeName(i);
                            AtValue = xpp.getAttributeValue(i);
                            Log.i("MainActivity", "    " +
                                    i + "番目の属性 = " + xpp.getAttributeName(i));
                            Log.i("MainActivity","    " +
                                    i + "番目の値 = " + xpp.getAttributeValue(i));
                            if(AtName.equals("lat")||AtName.equals("lon")) {
                                Food food = new Food();
                                food.setName(xpp.getAttributeName(i));
                                food.setPrice(xpp.getAttributeValue(i));
                                list.add(food);
                                myAdapter.notifyDataSetChanged();
                            }
                        }
                        break;
                    case XmlPullParser.TEXT:
                        Log.i("MainActivity", "テキスト = " + xpp.getText());
                        break;
                    case XmlPullParser.END_TAG:
                        Log.i("MainActivity", xpp.getName() + "要素終了");
                        break;
                }
                eventType = xpp.next();
                //次のトークンに進む
            }
            Log.i("MainActivity", "ドキュメント終了");
        } catch (XmlPullParserException e) {
            Log.e("MainActivity", "XMLの解析失敗.");
        } catch (IOException e) {
            Log.e("MainActivity", "XMLファイルの読み込みに失敗.");
        }
    }
    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override
    public void onRequestPermissionsResult(
            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == REQUEST_PERMISSION) {
            // 使用が許可された
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                setUpReadWriteExternalStorage();
            } else {
                // それでも拒否された時の対応
                Toast toast =
                        Toast.makeText(this, "何もできません", Toast.LENGTH_SHORT);
                toast.show();
            }
        }
    }
}

こちらも結構長いが、大半はストレージのパーミッション関係の記述で、肝心な部分はtry以下の部分。また、Logに関する記述もいっぱい書いてあるが、参考サイトをそのままコピーさせてもらったためこういう感じになっている。
String listXmlPath = Environment.getExternalStorageDirectory().getPath() + "/test.gpx"でtest.gpxというテスト用のgpxファイルを指定している。
String content = new Scanner(new File(listXmlPath)).useDelimiter("\\z").next()の部分は今一理解できていないが、後でインスタンスを発行するときに使う文字列を規定しているみたいだ。
XmlPullParser xpp = Xml.newPullParser()でインスタンスを発行し、xpp.setInput(new StringReader(content))で中身をxppに読み込んでいる。
その下のループの部分は、int eventType = xpp.getEventType()でイベントタイプを読み込んで、イベントタイプがEND_DOCUMENT、すなわちファイルの最後になるまで読み込みを続ける。
ループの中では、START_TAGというのを探して、AtName = xpp.getAttributeName(i)でAtNameにタグの名前を読み込み、値はAtValue = xpp.getAttributeValue(i)として読み込む。
名前が”lat"なら緯度、名前が”lon"なら経度なので、そのどちらかの場合、foodというアレイリストに名前と値を追記している。foodというのはアレイリストを表示させるのに参考にしたサイトのプログラムをそのまま持ってきたためで別に意味はない。値のところもpriceとなっているのは同様の理由。
Foodの処理に関しては、Food.javaというクラスを別に作成している。

public class Food {
    long id;
    private String name;
    private String price;

    public long getId() {
        return id;
    }

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

    public String getName(){
        return name;
    }

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

    public String getPrice(){
        return price;
    }

    public void setPrice(String price){
        this.price=price;
    }
}

アレイリストに追記したらアダプターに渡して、ListView表示をしている。
アダプターについても別クラスを作っておく。

public class MyAdapter extends BaseAdapter {
    Context context;
    LayoutInflater layoutInflater = null;
    ArrayList<Food> foodList;

    public MyAdapter(Context context) {
        this.context = context;
        this.layoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    public void setFoodList(ArrayList<Food> foodList) {
        this.foodList = foodList;
    }

    @Override
    public int getCount() {
        return foodList.size();
    }

    @Override
    public Object getItem(int position) {
        return foodList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return foodList.get(position).getId();
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        convertView = layoutInflater.inflate(R.layout.foodrow,parent,false);

        ((TextView)convertView.findViewById(R.id.name)).setText(foodList.get(position).getName());
        ((TextView)convertView.findViewById(R.id.price)).setText(String.valueOf(foodList.get(position).getPrice()));

        return convertView;
    }

}

レイアウトファイルも2つあって、Activity_Main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ListView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/listview"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

foodrow.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="40dp"
            android:layout_height="wrap_content"
            android:text="New Text"
            android:id="@+id/name"
            android:textSize="20dp"
            android:layout_weight="2" />

        <TextView
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:text="New Text"
            android:id="@+id/price"
            android:textSize="20dp"
            android:layout_weight="1" />
    </LinearLayout>

</LinearLayout>

実際に表示させてみるとこんな感じ。
f:id:alasixOsaka:20190923150940p:plain

参考にしたサイト
http://android-note.open-memo.net/sub/system__parse_xml_file.html
AndroidアプリのXMLの読み込み方法 | mucchinのAndroid戦記
Android開発でList Viewを使おう! - Qiita
[Android] 外部ストレージにファイルを保存する WRITE_EXTERNAL_STORAGE