高低図の全体表示(アンドロイド地図アプリの開発 その23)

前回はViewを分割して高低図の領域を作成しました。
alasixosaka.hatenablog.com


今回は、ここにGPXファイルから読み込んだ位置と標高のデータから高低図をプロットしてみたいと思います。
とりあえず、今のところ考えている機能としては、GPSの現在地を地図上に表示する場合は、高低図にも現在地を表示しようと思っています。その時は、高低図の横軸スケールは10㎞にしようと思っています。
GPSの現在地表示をしない場合、全体像を把握するため、横軸はコース全体の距離をスケールにしようと思っています。
縦軸に関しては、両方とも共通で、そのコースの最大標高に合わせようと思っています。まあ、そのあたりは使いながら使い勝手を見て必要があれば変更していこうと思っています。

GPXファイルから距離と標高を取ってくる。

今までは、GPXファイルから取り出すデータは、緯度と経度だけでしたが、GPXファイルには形式にもよりますが、標高が記載されています。
例えば、カシミール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">
<trk>
 <name>test4</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>
 <kashmir3d:icon>901001</kashmir3d:icon>
 </extensions>
<trkseg>
<trkpt lat="34.849862" lon="135.591704">
 <ele>-0.001000</ele>
 <time>2020-01-13T04:23:41Z</time>
</trkpt>
<trkpt lat="34.850909" lon="135.591318">
 <ele>-0.001000</ele>
 <time>2020-01-13T04:23:51Z</time>
</trkpt>

始めの方にぐじゃぐじゃと色々書いてありますが、肝心なのは"<trkseg>"以下の部分で、初めに緯度と経度が”<trkpt” 以下のlat="xxx"とlon="xxx"に記載されています。
今までは、この部分だけを読んでいました。標高は次の”<ele>”の部分に書かれています。
まず、グローバル変数としてArrayListを作成し名前をGPXsとします。そこに緯度、経度、スタートからの距離、標高の順番に記録していきます。
グローバル変数は、SamplesApplication.javaで定義しますので、ここの変数リストに

ArrayList<Double> GPXs = new ArrayList<>();

を加えてArrayListのGPXsをグローバル変数として作成します。

次に、OverlayMapViewer.javaのGPXファイル読み込みのルーチンを書き換えます。
まず、変数を追加します。

    private double Olati = 0;
    private double Olongi = 0;
    private double Tdistance = 0;
    private double elv = 0;
    private double elvmax=0;

Olatiは一つ前の地点の緯度、Olongiは一つ前の地点の経度で、距離の計算に使用します。Tdistanceはスタート地点からの通算の距離で高低図の横軸に使います。elvは標高、elvmaxはコースの最高標高です。
次に、GPXファイルの解析の部分に手を加えて通算距離を計算します。

//GPXファイルの解析
        try {
            String listXmlPath = Path + "/"+ GPX_FILE;
            is = new FileInputStream(new File(listXmlPath));
            BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
            
            //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);
                                globals.GPXs.add(lati);
                            }
                            if( AtName.equals("lon")) {
                                longi = parseDouble(AtValue);
                                globals.GPXs.add(longi);
                                if(Olati+Olongi!=0) {
                                    float[] distance =
                                            getDistance(Olati, Olongi, lati, longi);

                                    Tdistance += distance[0]/1000;
                                }
                                Olati=lati;
                                Olongi=longi;
                                globals.GPXs.add(Tdistance);
                                LatLong latLong = new LatLong(lati, longi);
                                latLongs.add(latLong);
                            }
                        }
                        break;
                    ~省略~

緯度を読み込むところで、globals.GPXs.add(lati);とし、経度を読み込むところで globals.GPXs.add(longi);として緯度経度を順番にGPXsに加えていきます。
また、一つ前の地点の緯度経度をそれぞれ、Olati, Olongiとして保存しておき、getdistanceを呼び出して距離を計算します。getdistanceは次のようになっていて

public float[] getDistance(double x, double y, double x2, double y2) {
        // 結果を格納するための配列を生成
        float[] results = new float[3];

        // 距離計算
        Location.distanceBetween(x, y, x2, y2, results);

        return results;
    }

アンドロイドのライブラリを呼び出して2地点間の距離を計算し、値を返しています。距離はfloat[0]に格納されています。
距離が計算出来たらTdistanceに加算して通算距離をGPXsに追記します。
次に標高ですが、標高については、この読み込みルーチンでは要素の値としてではなく、テキストとして読みだされますので、case XmlPullParser.TEXT:以下の部分を変更しています。

                case XmlPullParser.TEXT:
                        String t = xpp.getText();
                        Log.i("MainActivity", "テキスト = " + t);
                        if (XPname.equals("ele")){
                            elv = parseDouble(t);
                            if (elv>elvmax){
                                elvmax = elv;
                            }               
                            globals.GPXs.add(elv);                           
                            XPname="";
                        }
                        break;
                    case XmlPullParser.END_TAG:
                        Log.i("MainActivity", xpp.getName() + "要素終了");
                        break;
                }
                eventType = xpp.next();
                //次のトークンに進む
            }
            Log.i("MainActivity", "ドキュメント終了");

if (XPname.equals("ele")){で標高のデータかどうかを判断し、標高データなら、String tに入っている標高値をストリングデータから倍精度変数に変換してelvに格納します。また、elvがそれまでの最高標高より大きければelvmaxを書き換えて更新するとともに、GPXsに標高データを格納します。
これでデータの準備はできました。

全体高低図の表示

 高低図に現在地を表示するためには、また計算が必要になるので、今回はとりあえず全体図を表示するところまでやります。
高低図の表示はprotected void addOverlayLayers(Layers layers) { }の最後に

        if (globals.setHeight){
            HeightMap();
        }

を書き加えて実行しています。globals.setHeightはグローバル変数で型式はboolean、メイン画面でHeight MapをOnにしたときにTrueになります。
また、HeigtMap()を次のように加えます。

    private void HeightMap() {
        ((PaintView)findViewById(R.id.height)).map(globals.GPXs,elvmax);
    }

これで、PaintView.javaのmap()が呼び出されます。map()に渡す引数は、各地点の経度、緯度、標高と通算距離が格納されているArrayListのglobals.GPXsと最高標高のelvmaxです。
PaintView.javaは次のようになっています。

public class PaintView extends View {
    static ArrayList<Double> arrays = new ArrayList<>();
    private int xpos;
    private int xposo;
    private int ypos;
    private int yposo;
    private int ofset=0;
    private double elvmax;

    public PaintView(Context context, AttributeSet attribute){
        super(context,attribute);
    }
    @Override
    public void onDraw(Canvas canvas){
        Paint paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(3);
        int cheight = canvas.getHeight();
        int cwidth = canvas.getWidth();
        float mdis = 1000000;
        int Npos=0;

        double ymax=0;
        double xmax=0;

        if (elvmax>500){
            ymax = elvmax;
        }else {
            ymax = 500;
        }
        int idx = arrays.size();
        xmax = arrays.get(idx-2);
        if (xmax<10){
            xmax=10;
        }
        int xdiv = (int) (xmax/5);
        int ydiv = (int)(ymax/500);

        try {
            for (int i=0;i<xdiv;i++){
                canvas.drawLine(i*cwidth/xdiv,0,i*cwidth/xdiv,cheight,paint);
            }
            canvas.drawRect(new Rect(0, 0, cwidth, cheight), paint);
            for (int i=0;i<ydiv;i++){
                canvas.drawLine(0,cheight-i*cheight/ydiv,cwidth,cheight-i*cheight/ydiv,paint);
            }
            //canvas.drawLine(0,0,cwidth,cheight,paint);
        }catch (Exception e){
            Log.e("PaintView","error");
        }
        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(6);
        try {
            //canvas.drawRect(new Rect(10,10,100,100),paint);

            for (int i = 0; i < arrays.size() / 4; i++) {

                Double x = arrays.get(i * 4 + 2);
                Double y = arrays.get(i * 4 + 3);
                xpos = (int) (x / xmax * cwidth) + ofset;
                ypos = (int) (cheight - (y / ymax * cheight));

                if (xpos>=cwidth){
                    Log.e("PaintView","Over X");
                    break;
                }

                if (i != 0) {
                    canvas.drawLine(xposo, yposo, xpos, ypos, paint);
                }
                xposo = xpos;
                yposo = ypos;
            }
        }catch (Exception e){
            Log.e("PaintView","error");
        }
        //canvas.drawPath(path,paint);
    }
    public void map(ArrayList arraylist,double emax){
        arrays = arraylist;
        elvmax = emax;
        invalidate();
    }
}

OverLayMapviewer.javaで呼び出しているmap()は一番最後の部分で次のように、GPXsに格納された配列をarraysで受けています。GPXsはグローバル変数なのでわざわざ引数で受け取る必要が無いのですが、一応お作法として引数で受け取っています。あまり大きな意味はありません。また最高標高をelvmaxに受けています。そして、invalidate()を実行して、Ondraw()を呼び出して、グラフを表示しています。

    public void map(ArrayList arraylist,double emax){
        arrays = arraylist;
        elvmax = emax;
        invalidate();
    }

Ondrawが高低図表示の本体部分で、変数のofsetは今回は使っていませんが、後々横にスクロールするときに使う予定です。Paint paint = new Paint();でpaintをnewして作成します。ここにいろいろな線を書いていくことになります。
paint.setColor(Color.BLACK);で色を黒に、paint.setStyle(Paint.Style.STROKE);で線を実線にしています。paint.setStrokeWidth(3);は線の太さを設定しています。これらは、グラフの軸線を書くための設定です。
int cheight = canvas.getHeight();でCanvasの高さ方向のドット数、int cwidth = canvas.getWidth();でCanvasの横方向のドット数を取得し計算に使用しています。

縦軸、横軸のスケールを決める。

縦軸の最大値はymaxで、elvmaxつまりコースの最高標高が500m以下なら500m、500m以上なら最高標高をymaxにして、スケールを決めています。最高標高が500m以下の場合はymaxを500mにして、過度に拡大表示をしないようにしています。

       if (elvmax>500){
            ymax = elvmax;
        }else {
            ymax = 500;
        }

横の軸線は500mごとに1本引くようにし、全体スケールに合わせて本数を計算します。 int ydiv = (int)(ymax/500);で軸線の本数を計算しydivに代入しています。
また、縦の軸線については、前回は、Canvasをただ4分割して縦線を引いていましたが、次のように、コースの通算距離を横軸のスケール(xmax)として計算でもとめています。ただし、距離が10㎞以下の場合は、xmaxを10㎞にします。コースの通算距離はarraysで受け取ったグローバル変数GPXsの最後から2番目の値になりますので、int idx = arrays.size();で配列の数を受け取り、xmax = arrays.get(idx-2);で通算距離をxmaxに代入しています。そして、xmaxが10より小さい場合はxmaxを10(㎞)に設定しています。

        int idx = arrays.size(); 
        xmax = arrays.get(idx-2);
        if (xmax<10){
            xmax=10;
        }

また、横軸の軸線は5㎞ごとに引くこととし、int xdiv = (int) (xmax/5);のように計算してxdivに代入しています。

軸線を描く

軸線を描く部分は次のようになっています。

       try {
            for (int i=0;i<xdiv;i++){
                canvas.drawLine(i*cwidth/xdiv,0,i*cwidth/xdiv,cheight,paint);
            }
            canvas.drawRect(new Rect(0, 0, cwidth, cheight), paint);
            for (int i=0;i<ydiv;i++){
                canvas.drawLine(0,cheight-i*cheight/ydiv,cwidth,cheight-i*cheight/ydiv,paint);
            }
            //canvas.drawLine(0,0,cwidth,cheight,paint);
        }catch (Exception e){
            Log.e("PaintView","error");
        }

先ほど計算した、xdivが横軸の軸線数で、for文でループし、
canvas.drawLine(i*cwidth/xdiv,0,i*cwidth/xdiv,cheight,paint);で線を描いています。
canvas.drawLine( )が線を引く命令で、第一引数が始点のX座標、第二引数が始点のY座標、第三引数が終点のX座標、第四引数が終点のY座標、第五引数がCanvasです。
始点のX座標は、i*cwidth/xdivで全体をいくつで割るかがxdivで例えば4つに割る場合で、横軸のドット数cwidthが800なら、800/4で200ドットごとに線を引きます。
y軸も同様です。

高低図の表示

高低図を描いているのは

        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(6);

以下の部分で、色を青にして、線幅を6にし、軸線よりもやや太くしています。
高低図は for (int i = 0; i < arrays.size() / 4; i++) { }で配列数を4で割った数だけループします。配列には、一つの地点に着き、緯度、経度、通算距離、標高の4つのデータが格納されているので4で割った数が地点の数になります。ここは2次元配列にしてもよかったのですが、ArrayListを2次元にするのがややこしいので今回はシンプルに1次元配列にしました。
X地点はDouble x = arrays.get(i * 4 + 2); Y地点はDouble y = arrays.get(i * 4 + 3);として配列arraysがら読みだしています。
そして、xpos = (int) (x / xmax * cwidth) + ofset;でX座標を計算しています。フルスケールがxmaxなのでxをxmaxで割り、cwidthを掛けるとX座標になります。ofsetはスクロールするときの変数で今回は0のままです。
Y座標の計算は少しややこしくて、Canvasの座標は左上がゼロになるので、そのままプロットすると上下逆さの図になってしまいます。そのため、左下が高さゼロになるように計算をしています。ypos = (int) (cheight - (y / ymax * cheight));として左下からのドット数y/ymax*cheightを高さ方向のドット数cheightから引いて計算しています。
これで、各ポイントのX座標、Y座標が求まりました。
始めのポイントは線が引けませんので、i=0の時だけスキップするため、if (i != 0) { canvas.drawLine(xposo, yposo, xpos, ypos, paint); }とし、2番目のポイント以降で線を引いています。
xposo、yposoはそれぞれ前の地点のX座標、Y座標です。プロットした後、xposo = xpos; yposo = ypos; として代入しています。
また、何故かわかりませんが、一度メイン画面に戻って、再度地図画面に戻ると高低図の最後の地点から左下に直線が引かれてしまうという不具合があったので、

                 if (xpos>=cwidth){
                    Log.e("PaintView","Over X");
                    break;
                }

として、右端にプロットが来た時点でループを抜け出すようにしています。この不具合はデバッグで確認をしてみましたが、理由がよくわかりませんでした。ですが、一応この処理を入れたことで解消されています。
また、この処理は、現在地表示をするときに横軸のスケールを最大で10㎞にしようと思っているので、処理を途中で抜け出すのにも使えるので、まあいいかという感じで入れています。表示させるとこんな感じになります。
嵐山~二ノ瀬まで行った時のルート図です。最高標高が500m未満なので縦軸の軸線がありません。最後に登っているのがよくわかります。前回はここでへばってしまいました。

f:id:alasixOsaka:20200412173322j:plain
嵐山~二ノ瀬までのルートと高低図
次回は、現在地を表示させることをやってみようと思います。また、今回は高低図を表示するために、Viewを変更したままになっているので、高低図はいらないよというときはViewを戻して、全面地図のViewを表示させないと無意味なCanvasが下に表示されたままになります。そのあたりもやっていかないといけません。