POIファイルを読み込み、地図に表示する。(アンドロイド地図アプリの開発 その15)

地図アプリシリーズは、結構久しぶりの更新となってしまいました。
前回は、GPXファイルからルートを表示することをやりました。
alasixosaka.hatenablog.com
今回は、GPX形式のファイルですが、POIを表示することをやってみたいと思います。
POIとは、Point of Interestの略で、文字どおり訳せば興味のある地点という感じでしょうか?
今回するのは、ルート上に目印をつけていくというようなイメージです。具体的に言うと、交差点を右折するのか左折するのかということを明示するということやってみます。
地図上にマークを表示するのは、Mapsforgeのサンプルファイルにも書いてあるので比較的簡単にできると思っていましたが、Java初心者の罠に引っかかって思ったよりも手こずってしまいました。Javaプログラミングに慣れた人なら何のことはないことなんでしょうが、その辺は素人の悲しさで、いろいろと調べながら試行錯誤を繰り返してしました。

まずは、画像ファイルを用意する。

地図上に表示する矢印は、パワーポイントの矢印を使いました。ただ、このままだと背景が白色で、地図が見づらくなるので、背景を透明にする必要があります。
色々試しましたが、パワーポイントではうまくやる方法が見つからずに、パワーポイント画像を一旦ペイントに読み込んで、背景を透明にする処理を行いました。
画像は、矢印の方向が固定になるので、右折、左折ともに8方位、計16種類の画像を用意しました。といっても、パワーポイント上で回転しただけですが。

POIファイルの作成をどうするか

どこに画像を配置するかを教えてやるために、ファイルを作成する必要がありますが、基本はGPX形式のファイルを使います。GPXファイルを出力できるソフトはいくつかありますが、使い勝手を考えてカシミール3Dを使うことにしました。
カシミール3Dは自作アイコンを表示することができるので、まずは、自作のアイコンを登録します。アイコンは24×24ビットにする必要があるので、作成した矢印をサイズ変更して24×24ビットにし、それを8つ並べたファイルを作成しました。そして、それぞれのアイコンの名称を例えば、右折1、右折2というようにして、8つ書き込んだテキストファイルを用意します。
データが用意できたら、カシミールのサポートページのやり方を見てアイコンを登録します。
カシミール / アイコンの設定
別にアイコンを登録しなくても、地図アプリ上に画像を表示することはできますが、カシミール上でも同様の画像が見れた方が作業はやり易いです。
カシミール3Dを起動して、地図を適当に表示し、画像を置きたいポイントで右クリックをします。新規作成、ウェイポイントの作成を選択します。

f:id:alasixOsaka:20200302152608j:plain
カシミール3Dでポイント右クリック
そうすると、ウェイポイントのプロパティが表示されますので、ここでアイコンを選択します。下の図は左折の1番目のアイコン「左折1」を選んでいるところです。
f:id:alasixOsaka:20200302153037j:plain
ウェイポイントのプロパティでアイコンを選択
とりあえず2つほど作ってファイルに出力してみます。ファイル出力はGPSデータ編集の画面から、ウェイポイントのフォルダを選んで、選択したGPSデータの書き出しをクリックします。
f:id:alasixOsaka:20200302153343j:plain
ウェイポイントのファイルを出力
ちょっとややこしいですが、ここで出力されるファイルをPOIファイルとここでは呼ぶことにします。ただ、このままでは普通のGPXファイルと区別がつかないので、拡張子をPOIに変更します。
できたファイルの中身は次のようになっています。

<?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">
<wpt lat="35.303822" lon="135.254339">
 <ele>-0.001000</ele>
 <time>2020-03-02T06:13:45Z</time>
 <name>新規</name>
 <extensions>
 <kashmir3d:icon>1001001</kashmir3d:icon>
 </extensions>
</wpt>
<wpt lat="35.305778" lon="135.253589">
 <ele>-0.001000</ele>
 <time>2020-03-02T06:14:32Z</time>
 <name>新規</name>
 <extensions>
 <kashmir3d:icon>1001009</kashmir3d:icon>
 </extensions>
</wpt>
</gpx>

始めの方は、ファイルがカシミール3Dで作成されたことを表しています。肝心なのは<wptと書かれたところから、latとlonはそれぞれ緯度と経度を表し、eleは高度を表します。今回は高度情報のない地理院地図を使っているので高度は(ほぼ)ゼロの値が入っています。timeは作成された時間、nameはウェイポイントの名前です。カシミール3D上で適当な名前を付けることができます。今回はテストで何もしなかったので「新規」となっています。アイコンの区別は、kashimir3D:iconのところで行います。右折1は"1001001"、左折1は"1001009"でした。つまり、右折が1001001から1001008まで、左折が1001009から1001016まで連番になっています。一見すると何進数か区別がつきにくいですが10進数のようです。この値は、それぞれの環境で異なっていると思われますので、それぞれの環境に応じてアンドロイドのプログラムで変更する必要があります。

Androidアプリを変更する。

基本的に変更するところは3カ所です。

  1. メニュー画面にPOIファイルを選択するボタンの追加
  2. 画像ファイルの追加
  3. POIファイルの読み込みと解析処理

1についてはレイアウトファイル”activity_main.xml"ファイルを編集するだけです。詳細は省略します。

f:id:alasixOsaka:20200302160852j:plain
メニュー画面の一番下にPOI fileSelectボタンを追加
2は作成した画像ファイルをresの下のDrawableに追加します。コピペで追加可能です。
3については、編集するファイルはOvarlayMapViewer.javaになります。ここで、線やら画像やらいろいろなものを地図に重ね書きをしています。現状はサンプルプログラムをコピーしてそこをごちゃごちゃいじっているのですごく汚いですが、ここにマーカーを表示するという部分があります。

Marker marker1 = Utils.createTappableMarker(this,
                R.drawable.marker_red, latLong1);

latLong1で指定された座標にmarker_red(赤いバルーン)を表示するという記述になります。これを応用して、POIファイルから読み込んだ座標に矢印を表示させれば良いことになります。
私が引っかかったのが、サンプルではマーカーは一つだけですが、いくつも表示しなければいけないというところです。複数あるなら配列を使えばいいやと考えましたが、そもそもこの部分の記述の意味をちゃんと理解していないので、苦労しました。結局Marker型のArrayListを作成すれば良いということがわかるのに半日くらいかかってしまいました。
やり方はまず、

ArrayList<Marker> markers = new ArrayList<Marker>();

でMarker型のArrayList markersを作成します。それで、表示したい記号をmarkers.add(xxxxx)のように追加していけばよいということでした。
具体的なソースは

//POIファイルの解析
        try {
            String listXmlPath = Path + "/"+ POI_FILE;
            is = new FileInputStream(new File(listXmlPath));
            BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
            //String content = new Scanner(
            //        new File(listXmlPath)).useDelimiter("\\z").next();
            //XMLファイルをまとめて読み込み
            XmlPullParser xpp = Xml.newPullParser();

            xpp.setInput(reader);
            //解析するXMLファイルの中身を渡す
            int eventType = xpp.getEventType();
            while (eventType != XmlPullParser.END_DOCUMENT) {
                switch (eventType) {
                    case XmlPullParser.START_DOCUMENT:
                        Log.i("MainActivity", "ドキュメント開始");
                        break;
                    case XmlPullParser.START_TAG:
                        XPname = xpp.getName();
                        Log.i("MainActivity", XPname + "要素開始");
                        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")){
                                lati = parseDouble(AtValue);
                            }
                            if( AtName.equals("lon")) {
                                longi = parseDouble(AtValue);
                                latLongX = new LatLong(lati, longi);
                                //latLongs.add(latLong);
                            }


                        }
                        break;
                    case XmlPullParser.TEXT:
                        POIname = xpp.getText();
                        Log.i("MainActivity", "テキスト = " + POIname);

                        if (XPname.equals("icon")){
                            switch (POIname) {
                                case "1001009":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.left1, latLongX));
                                    break;
                                case "1001010":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.left2, latLongX));
                                    break;
                                case "1001011":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.left3, latLongX));
                                    break;
                                case "1001012":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.left4, latLongX));
                                    break;
                                case "1001013":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.left5, latLongX));
                                    break;
                                case "1001014":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.left6, latLongX));
                                    break;
                                case "1001015":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.left7, latLongX));
                                    break;
                                case "1001016":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.left8, latLongX));
                                    break;
                                case "1001001":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.right1, latLongX));
                                    break;
                                case "1001002":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.right2, latLongX));
                                    break;
                                case "1001003":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.right3, latLongX));
                                    break;
                                case "1001004":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.right4, latLongX));
                                    break;
                                case "1001005":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.right5, latLongX));
                                    break;
                                case "1001006":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.right6, latLongX));
                                    break;
                                case "1001007":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.right7, latLongX));
                                    break;
                                case "1001008":
                                    markers.add( Utils.createTappableMarker(this, R.drawable.right8, latLongX));
                                    break;

                            }
                        }
                        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ファイルの読み込みに失敗.");
        }

始めの部分はGPXファイルの読み込みとほぼ同じです。fillterをPOIにして拡張子がPOIのファイルのみ表示するように変えています。
緯度、経度の読み込みも同じ処理です。読み込んだ緯度、経度はlatlongXという変数に代入しています。アイコンを区別する部分は、要素名をXPnameという変数に代入し、それが”icon”の時がアイコンであると判断しています。その場合のテキストをPOInameに代入し、Switch~Case でそれぞれのアイコンに対する処理を行っています。連番になっているので、変数を使ってもっとスマートに処理できると思いますが、POInameがStrign型なのでそのまま型変換をせずにベタな処理で済ませてしまっています。

f:id:alasixOsaka:20200302163139j:plain
矢印を地図に4つ表示したところ
こんな感じで表示されます。気を付けないといけないのは、矢印のサイズは地図のスケールにかかわらず一定ということです。そのため、縮尺を縮小して引いた地図を表示してしまうと、矢印がいっぱい重なってとっても見にくくなる点です。この辺はもっとスマートな処理がありそうなもんですが、うまく見つけられなかったのと、実用上は200mスケールや500mスケールなどしか使わないだろうと思われるので実害はないと判断しました。
一応、ここまでで、高低図の表示以外は初めに考えた処理が全部できたということになります。高低図ははっきり言ってハードルが高いのでできる自信がないですが、少し頑張ってみようと思っています。いろいろ使っていると使い勝手の悪い部分が見えてきたので、そのあたりも含めて、次回は一旦総括をしてみようかと思っています。