等高線付きの地図を表示する(アンドロイド地図アプリの開発 その19)

前回までで、このアプリ開発を一旦総括しました。
alasixosaka.hatenablog.com

さて、今回は地図に等高線を表示させてみようという話です。総括のところで、今後の機能についても書いてみましたが、実はもう一つやってみたいことがあって、それが等高線付きの地図を表示するというものです。
mapsforgeの地図データは、Mapsforgeのサーバーに行けばダウンロードできるのですが、この地図には等高線がついていません。
Mapsforge Download Server
ただ、トレランとかに使うことを考えると等高線付きの地図も見たいということもあって調べてみました。mapsforgeの地図形式はベクターという形式らしいのですが、実はLocusMapというアプリがあって、同じ形式の地図を使えるようになっているらしい、また、OpenAndroMapsというのもあって、ここで等高線付きの地図がダウンロードできるらしい。
blogs.da-cha.jp
また、自分で等高線付きの地図を作っている人もおられて、作った地図も公開してくださっている。
yueno.net
ここのやり方を見れば自分でも地図を作ることができそうだが、とりあえずサクッと表示するためにありものの地図データをダウンロードしてやってみた。

地図ファイルを入れ替えるだけではうまくいかない

ところが、地図ファイルをただ入れ替えるだけでは等高線は表示されなかった。

f:id:alasixOsaka:20200315135117j:plain
そのまま地図ファイルだけ入れ替えてもダメだった
そうは簡単に問屋はおろしてくれないということか。
実は、OpenAndroMapsのサイトには気になることが書いてあって、”The maps from this site need a Rendertheme for nice and correct rendering on screen. (This have to be done only once for each device)"と書いてある。つまり、専用のRenderthemeを使わないとちゃんと表示されませんよということらしい。そして、Renderthemeもダウンロードできるようになっている。問題はそこからだった。

mapsforgeのRenderthemeを変更する。

検索すると、mapsforgeのサイトの中にRenderthemeに関する記述があった。
mapsforge/Rendertheme.md at master · mapsforge/mapsforge · GitHub
ここには、Renderthemeとして外部ファイルを使う場合のやり方として、"External render-theme files are also supported and can be activated via the tileRendererLayer.setXmlRenderTheme(new ExternalRenderTheme(File)) method at runtime."と書いてある。しかし、これだけでは全然やり方がわからない。
さらに調べると、こんなサイトがあった。
www.programcreek.com

ここには、外部ファイルのRenderthemeを使うときのサンプルコードがいくつか書いてあるのでこれを参考にした。
次の問題は、自分のアプリのどの部分をいじれば良いのかということだった。
結論から書くと、DefautltTheme.javaというクラスがあって、ここを変更すると良いということが分かった。
そこで、先ほどのサイトの記事を参考に、こんな感じに変更してみた。Elevate.xmlがOpenAndroMapsから落としてきたRendertheme。storage/emulated/0/Elevate.xmlはフルパスの記述で、アプリからgetRenderthemeが呼ばれた時に、オリジナルは、 InternalRenderTheme.DEFAULT(つまり内部に持っているデフォルトのRendertheme)を返すようになっていたのを、ダウンロードしてきた外部ファイルを返すようにしてみた。もし、該当の外部ファイルがなかったら、デフォルトを返すようにしている。

public class DefaultTheme extends SamplesBaseActivity  {

    /**
     * This MapViewer uses the built-in default theme.
     *
     * @return the render theme to use
     */
    @Override
    protected XmlRenderTheme getRenderTheme() {
        //return InternalRenderTheme.DEFAULT;
        try {
            return  new ExternalRenderTheme("storage/emulated/0/Elevate.xml");
        } catch (Exception e) {
            // just return the default theme
        }
        return InternalRenderTheme.DEFAULT;
    }

}

やってみると少し見た目は変わったが今度はうまく等高線が表示できた。

f:id:alasixOsaka:20200315140801j:plain
等高線が表示された地図

テーマを適当に入れ替えれるようにする。

シチュエーションによっては等高線があったほうが良いこともあれば、ない方が見やすいこともあるので、テーマを選んで入れ替えれるようにしてみたい。
そうすると、またメニュー画面が追加になるが、もう一つサブ画面を作って、テーマを選ぶ画面を追加してみた。
まず、メニュー画面にボタンを一つ追加し、"Select Theme"とした。また、"Theme Select.java"というjava class を一つ追加し、ボタンをクリックしたら、呼び出すようにした。Theme Select.javaではリストビューを表示し、リストからテーマを選択するようにした。現在は、"default"と"Locus"の2つが選択できる。将来的に増やしたい時は、リストビューに追加できるようにしている。ちなみにLocusにあまり意味はないです。最初に等高線付きの地図を見つけたのがLocusMapのサイトだったので敬意を表してLocusとしています。

MainActivityの変更点は下記の通りです。

   private void clickWait(){

        ~省略~
        Button btselth = findViewById(R.id.btSelectTheme);
        ~省略~
        btselth.setOnClickListener(listener);
  ~省略~
        }
    }
    private class BtListener implements View.OnClickListener {
        @Override
        public void onClick(View view){
            //Intent SMV = new Intent(getApplicationContext(),SimplestMapViewer.class);
            //startActivity(SMV);
            int id = view.getId();

            switch (id) {

                ~省略~
                case R.id.btSelectTheme:
                    Intent selth = new Intent(MainActivity.this,ThemeSelect.class);
                    startActivity(selth);
                    break;
            }
        }
    }

clickwait()のところに、ボタンのidを追加し、リスナーに渡しています。リスナーの方では、SelectThemeのボタンがタップされたらThemeSelect.classのインテントを発行して処理を引き継ぎます。
また、レイアウトファイルActivity_main.xmlにボタンを追加します。

<Button
        android:id="@+id/btSelectTheme"
        android:layout_width="202dp"
        android:layout_height="38dp"
        android:layout_alignParentStart="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_marginStart="84dp"
        android:layout_marginLeft="84dp"
        android:layout_marginTop="400dp"
        android:text="@string/select_theme" />

ThemeSelect.javaソースコードは下記の通り。

public class ThemeSelect extends Activity {

    private ListView lv;
    private static String MapTheme ="";

    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.select_theme);
        ArrayAdapter<String> adapter =
                new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
        adapter.add("default");
        adapter.add("Locus");
        lv = (ListView) findViewById(R.id.themeselect);
        lv.setAdapter(adapter);
        lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
                preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
                MapTheme = preferences.getString("theme","default");
                switch (position){
                    case 0:
                        MapTheme = "default";
                        break;
                    case 1:
                        MapTheme = "Locus";
                }
                SharedPreferences.Editor editor = preferences.edit();
                editor.putString("theme", MapTheme);
                //editor.putString("path", file.getPath());
                editor.apply();
                finish();
            }
        });
    }
}

リストビューでdefaultとLocusを表示して、どちらをタップしたかでSwitch文で分岐して、MapThemeを決定しています。決定したMapThemeはSharedPreferenceに書き込んで、DefaultTheme.javaで選択するようにします。
SelectThemeのリストビューselect_theme.xmlは下記の通りです。リストビューを一つ表示するシンプルなものです。

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

    </ListView>

</LinearLayout>

DefaultTheme.javaの方は次のように書き換えます。

public class DefaultTheme extends SamplesBaseActivity  {

    /**
     * This MapViewer uses the built-in default theme.
     *
     * @return the render theme to use
     */
    private static String MapTheme ="";
    private static String Path ="";
    String sdPath = Environment.getExternalStorageDirectory().getPath();
    @Override
    protected XmlRenderTheme getRenderTheme() {
        //return InternalRenderTheme.DEFAULT;
        SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
        preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
        MapTheme = preferences.getString("theme","default");
        Path = preferences.getString("path", sdPath);
        try {
            if (MapTheme.equals("Locus")){
                return  new ExternalRenderTheme(Path + "Elevate.xml");
            }

        } catch (Exception e) {
            // just return the default theme
        }
        return InternalRenderTheme.DEFAULT;
    }

}

元々、InternalRenderTheme.DEFAULTをシンプルに返すようになっていたのを、SharedPreferenceからthemeを読みだして返すようにしています。ただ、Locusを選んだ時に該当のファイルElevate.xmlがない場合はデフォルトを返すようにしてエラーの発生を防いでいます。また、RenderThemeのファイルは地図ファイルと同じディレクトリに置いておく必要があります。そうしないとエラーになってデフォルトのテーマが選ばれます。


そして、Intent で呼び出すjava class を追加したのでmanifest に追記が必要になります。始めはこれを忘れていてエラーになって、何故なのか結構悩んでしまった。ここで、mpf_rotation8は今回のプロジェクト名です。

<activity android:name="com.example.mpf_rotation8.ThemeSelect"></activity>

ちなみに、default とLocus の違いは等高線の有り無し以外に見た目も結構変わります。また、テーマにLocus を選んでも地図データに等高線が含まれてないと当然、等高線は表示されず見た目だけが変わります。
次回は残りの修正点、画面をオフにしない、軌跡を残す、GPSをバックグランドで動かすといったことに取り組んでみたいと思います。

参考にしたサイト
HikingでOpenStreetMapを活用
Downloads - openandromaps
mapsforge/Rendertheme.md at master · mapsforge/mapsforge · GitHub
Java Code Examples org.mapsforge.map.rendertheme.ExternalRenderTheme
上野家のホームページ - PC/地図/Locus map用ベクター地図Locus Map - 資料室
map フォーマットファイルを自分で作成する方法について - プログラマーのメモ書き

これまでの総括(一旦 その3)(アンドロイド地図アプリの開発 その18)

前回までで、Javaクラスのソースについては全部終わりました。

alasixosaka.hatenablog.com

今回は、それ以外のファイルについてです。プロジェクト名は今回の場合"mpf_rotation7"です。

Manifest

Manifestはこんな感じです。GPSと外部ファイルへのアクセス許可。アクティビティの登録などを追記しています。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.mpf_rotation7">

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:name="com.example.mpf_rotation7.SamplesApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name="com.example.mpf_rotation7.RotateMapViewer"></activity>
        <activity android:name="com.example.mpf_rotation7.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name="com.example.mpf_rotation7.SimplestMapViewer"
            android:configChanges="keyboardHidden|orientation|screenSize" />
        <activity
            android:name="com.example.mpf_rotation7.OverlayMapViewer"
            android:configChanges="keyboardHidden|orientation|screenSize" />
    </application>

</manifest>

gradle

gradleについては依存関係の部分だけを記します。ここにmapsforgeのライブラリを登録しておく必要があります。

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'org.mapsforge:mapsforge:0.6.1'
    implementation 'org.mapsforge:mapsforge-core:0.11.0'
    implementation 'org.mapsforge:mapsforge-map:0.11.0'
    implementation 'org.mapsforge:mapsforge-map-reader:0.11.0'
    implementation 'org.mapsforge:mapsforge-themes:0.11.0'
    implementation 'net.sf.kxml:kxml2:2.3.0'

    implementation 'org.mapsforge:mapsforge-map-android:0.11.0'
    implementation 'com.caverock:androidsvg:1.3'
}

リソースファイル

リソースファイルが山ほどあるので順番に。

drawable

drawableはアプリ内で使用する画像ファイルです。
f:id:alasixOsaka:20200326084623j:plain
このうちleft1からleft8とright1からright8まではPOIで地図上に右折、左折を表示させるための矢印の画像でパワポで自作しました。
それ以外は、mapsforgeのサイトからコピーしたものです。marker_greenとmarker_redはそれぞれ緑と赤のバルーンで、本質的には必要ないものです。一応”Overlaymapviewer"のところで使ってはいるのでないとエラーになってしまいますが、サンプルプログラムをそのままコピペしているために必要なだけで、その部分を削除してしまえばエラーは解消します。

layout

画面はメニューを表示するMainActivityと地図を表示するRotateMapViewerがあるのでそれぞれにレイアウトファイルが必要です。実際のプロジェクトには4つファイルがあって2つほどゴミが残っていますが(笑)。
Activity_main.xmlはメニュー画面のボタンとGPSのオンオフ(といってもGPSそのものをオンオフするわけではなく、単にGPSの位置情報を地図に反映させるかどうか)のラジオボタンをRelativeLayoutで配置しています。

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

    <Button
        android:id="@+id/btRotationalView"
        android:layout_width="217dp"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_marginStart="105dp"
        android:layout_marginLeft="105dp"
        android:layout_marginTop="215dp"
        android:text="@string/RotationalView" />

    <Button
        android:id="@+id/btMapFileSelect"
        android:layout_width="214dp"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_marginStart="105dp"
        android:layout_marginLeft="105dp"
        android:layout_marginTop="101dp"
        android:text="Map File Select" />

    <Button
        android:id="@+id/btGPXSelect"
        android:layout_width="214dp"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_marginStart="107dp"
        android:layout_marginLeft="107dp"
        android:layout_marginTop="426dp"
        android:text="GPX File Select" />

    <RadioGroup
        android:id="@+id/radiogroup"
        android:layout_width="262dp"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_marginStart="83dp"
        android:layout_marginLeft="83dp"
        android:layout_marginTop="317dp"
        android:layout_marginBottom="10dp"
        android:background="#df7401"
        android:orientation="horizontal"
        android:paddingTop="10dp"
        android:paddingBottom="10dp">

        <RadioButton
            android:id="@+id/rbOn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="25dp"
            android:layout_marginRight="25dp"
            android:background="#ffffff"
            android:text="@string/onGPS" />

        <RadioButton
            android:id="@+id/rbOff"
            android:layout_width="101dp"
            android:layout_height="match_parent"
            android:background="#ffffff"
            android:text="@string/offGPS" />

    </RadioGroup>

    <Button
        android:id="@+id/btPOISelect"
        android:layout_width="214dp"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_marginStart="107dp"
        android:layout_marginLeft="107dp"
        android:layout_marginTop="547dp"
        android:text="POI FILE SELECT" />

</RelativeLayout>

rotatemapviewer.xmlはmapsforgeのサンプルをほぼコピペしています。やっていることはよくわかりません(笑)。地図画面とスケールを切り替えるプラスとマイナスのボタン。それにOpenStreetMapのクレジット表示があります。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mainView"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <org.mapsforge.map.android.rotation.RotateView
        android:id="@+id/rotateView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">

        <org.mapsforge.map.android.view.MapView
            android:id="@+id/mapView"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" />
    </org.mapsforge.map.android.rotation.RotateView>

    <com.example.mpf_rotation7.MapScaleBarView
        android:id="@+id/mapScaleBarView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_marginLeft="5dp" />

    <ImageButton
        android:id="@+id/zoomOutButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:background="@android:color/transparent"
        android:src="@drawable/zoom_control_out" />

    <ImageButton
        android:id="@+id/zoomInButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@id/zoomOutButton"
        android:layout_alignParentRight="true"
        android:background="@android:color/transparent"
        android:src="@drawable/zoom_control_in" />



    <TextView
        android:id="@+id/attribution"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="@dimen/attribution_margin"
        android:gravity="center"
        android:singleLine="true"
        android:text="@string/copyright_osm"
        android:textColor="#000"
        android:textSize="@dimen/attribution_size" />

</RelativeLayout>
mipmap

mipmapのファイルもすべてMapsforgeのサイトからのコピペです。
f:id:alasixOsaka:20200326091235j:plain

values

valuesにもいっぱいファイルがありますが、使っていないのがありそうです。
f:id:alasixOsaka:20200326091526j:plain
絶対に使っているのはstrings.xmlとdimens.xmlの2つです。しかもstrings.xmlのなかにもゴミがいっぱい。コピペしたのに必要なものだけ追記しているので、こんなの絶対使ってないだろというのがいっぱいあります。
一応全文を載せると。

<resources>
    <string name="app_name">mpf_Rotation4</string>
    <string name="RotationalView">RotationalView</string>
    <string name="copyright_osm">Map data © OpenStreetMap contributors</string>
    <string name="onGPS">GPS On</string>
    <string name="offGPS">GPS Off</string>

    <string name="menu_preferences">Settings</string>
    <string name="menu_position_enter_coordinates">Map Position</string>
    <string name="menu_svgclear">Clear SVG icon cache</string>

    <string name="cancelbutton">Cancel</string>
    <string name="okbutton">OK</string>
    <string name="downloadnowbutton">OK</string>
    <string name="rotate_button">Rotate</string>
    <string name="zoomin_button">+</string>
    <string name="zoomout_button">-</string>

    <string name="latitude">Latitude</string>
    <string name="longitude">Longitude</string>
    <string name="zoomlevel">Zoom Level</string>

    <string name="preferences_scale">Display Scale</string>
    <string name="preferences_scale_desc">Adjust scale of the map display</string>

    <string name="preferences_language">Multilingual Map Language Selection</string>
    <string name="preferences_language_desc">Select language of multilingual map (multilingual map must be loaded)</string>

    <string name="preferences_language_showlocal">Language Display Style</string>
    <string name="preferences_language_showlocal_desc">Show both local and preferred language</string>

    <string name="preferences_scalebar">Scale bar</string>
    <string name="preferences_scalebar_desc">Set units system on the scale bar</string>

    <string name="preferences_textwidth">Maximum Textwidth</string>
    <string name="preferences_textwidth_desc">Debugging aid for setting maximum textwidth</string>

    <string name="preferences_category_wayfiltering">Way filtering</string>
    <string name="preferences_wayfiltering">Way filtering</string>
    <string name="preferences_wayfiltering_desc">Way filtering reduces the number of ways returned at the risk of clipping artifacts</string>
    <string name="preferences_wayfiltering_distance">Way filter size</string>
    <string name="preferences_wayfiltering_distance_desc">The higher the number the less artifacts, but slower</string>

    <string name="preferences_category_tilecache">Tile cache</string>
    <string name="preferences_tilecache_persistence">Persistence</string>
    <string name="preferences_tilecache_persistence_desc">Keep tiles over activity restart</string>

    <string name="preferences_debug_timing">Debug timing</string>
    <string name="preferences_debug_timing_desc">Measure rendering time</string>
    <string name="preferences_rendering_threads">Number of threads to render concurrently</string>
    <string name="preferences_rendering_threads_desc">Minimum 1</string>

    <string name="preferences_r4_emergency">Emergency</string>
    <string name="preferences_r4_emergency_desc">Show hospitals, doctors, police etc</string>
    <string name="preferences_r4_accommodation">Accommodation</string>
    <string name="preferences_r4_accommodation_desc">Show hostels, guesthouses, campsites etc</string>
    <string name="preferences_r4_tourism_poi">Tourism</string>
    <string name="preferences_r4_tourism_poi_desc">Show points of interest</string>
    <string name="preferences_r4_shopping">Shopping</string>
    <string name="preferences_r4_shopping_desc">Show shops</string>
    <string name="preferences_r4_amenities">Amenities</string>
    <string name="preferences_r4_amenities_desc">Show amenities???</string>
    <string name="preferences_r4_public_transport">Transport</string>
    <string name="preferences_r4_public_transport_desc">Show transport</string>
    <string name="preferences_r4_food">Food</string>
    <string name="preferences_r4_food_desc">Show restaurants</string>
    <string name="preferences_r4_traffic">Traffic</string>
    <string name="preferences_r4_traffic_desc">Show traffic/transport??</string>
    <string name="preferences_r4_barriers">Barriers</string>
    <string name="preferences_r4_barriers_desc">Show barriers</string>
    <string name="preferences_r4_sports">Sports</string>
    <string name="preferences_r4_sports_desc">Show sports facilities</string>
    <string name="preferences_r4_places">Places</string>
    <string name="preferences_r4_places_desc">Show place names</string>
    <string name="preferences_r4_buildings">Buildings</string>
    <string name="preferences_r4_buildings_desc">Show buildings</string>
    <string name="preferences_r4_nature">Nature</string>
    <string name="preferences_r4_nature_desc">Show natural items</string>
    <string name="preferences_r4_areas">Areas</string>
    <string name="preferences_r4_areas_desc">Show areas</string>
    <string name="preferences_r4_roads">Roads</string>
    <string name="preferences_r4_roads_desc">Show roads</string>
    <string name="preferences_r4_waterbodies">Water</string>
    <string name="preferences_r4_waterbodies_desc">Show lakes, rivers etc</string>

    <string name="dialog_location_title">Coordinates</string>
    <string name="dialog_reverse_geocoding_title">Reverse Geocoding</string>

    <string name="startup_dontshowagain">Don\'t show again</string>
    <string name="startup_message">To run this samples app, you need any map with filename berlin.map installed on storage.\n\nadb push file.map /sdcard/Android/data/org.mapsforge.samples.android/files/berlin.map</string>
    <string name="startup_message_tilestore">To run this sample activity, you need a TMS tile store at /sdcard/Android/data/org.mapsforge.samples.android/files/tilestore. Sample data in the app data directory.</string>
    <string name="startup_message_multimap">To run this sample activity, you need to download a low-resolution world map first.</string>
    <string name="startup_message_twomaps">To run this sample activity, you need also a map with filename second.map installed on storage.</string>
    <string name="startup_message_poi">To run this sample activity, you need any poi with filename berlin.poi installed on storage.\n\nadb push file.poi /sdcard/Android/data/org.mapsforge.samples.android/files/berlin.poi</string>

</resources>

dimens.xmlはシンプルで

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="attribution_margin">1dp</dimen>
    <dimen name="attribution_size">4pt</dimen>
    <dimen name="controls_margin">8dp</dimen>
</resources>

これ以外に、menuの下にoptions_menu.xmlxmlの下にpreferences.xmlがありますが、これらも使ってなさそう。
f:id:alasixOsaka:20200326092247j:plain
なんでこんなにゴミばっかりになったのか不明ですが、とりあえすこれで動いていうのでまあいいかなと。

一応ここまでで総括は終わりです。使っているうちに不便なところも出てきたので現在そのあたりを修正しています。
近いうちにまた修正版をアップする予定です。

東海自然歩道を走る その3

東海自然歩道を走るシリーズの第三弾です。
前回は、萩谷から嵐山までを行きました。
alasixosaka.hatenablog.com

今回は、嵐山から鞍馬の手前、二ノ瀬駅まで行ってきました。

嵐山からスタート

朝9時前に嵐山に到着。

f:id:alasixOsaka:20200322082605j:plain
今回のスタートは阪急嵐山駅
3連休の中日だったが、相変わらず人は多くない。たぶん普段だったら人がうじゃうじゃいるところだが。
渡月橋も余裕で歩けるくらいしか人がいない。
f:id:alasixOsaka:20200322082752j:plain
渡月橋もひとはまばら
渡月橋を渡って川沿いを左手に進む。ここで、最初のミス。地図を見て川沿いを行けるところまで行って右に入ると覚えていて、道なりに進むと天竜寺に出てしまって、ここで地図を確認したら、川沿いをもっと先に進まないと行けなかったことに気づく。戻ってみると、河原の方に下りる道があって、これを見落としていた。河原をしばらく進んで嵐山公園に入る。
f:id:alasixOsaka:20200322083237j:plain
嵐山公園の中にある標識。京都一周トレイルと同じコースになっているようだ
f:id:alasixOsaka:20200322083338j:plain
嵐山公園の中の看板
嵐山を抜けて保津峡の方に向かう。ここは自転車でも時々来る道だ。六丁峠を越えて落合というところに出る。夏とかに来ると川下りの船が通って、歓声が聞こえたりするが、今日は川下りもやっていないのかな。
f:id:alasixOsaka:20200322083531j:plain
保津峡にある看板

高雄へ

保津川から清滝川に入って遡っていく。ここにトレイルがあることは知らなかった。

f:id:alasixOsaka:20200322084750j:plain
清滝川沿いを進む
岩がゴロゴロして進みにくい。しばらく進むと清滝の集落に到着。
f:id:alasixOsaka:20200322085042j:plain
清滝の集落
更に清滝川沿いを進む。川を遡るので当然だが、ずーっと緩やかな登りが続く。個人的にはダラダラ登るのが苦手で頑張って走りすぎると後で足に来るし、歩くほどの傾斜でもないしという感じ。
しばらくすると右手の上に水路のようなものが見えた。
f:id:alasixOsaka:20200322085417j:plain
上の方に水路が見える。振りかえって写真を撮っているので左手に見えるが、進行方向からは右手にある。
更に進むとようやく高雄に到着。ここまでだいたい1時間半。いいペースだ。ここで、おにぎりを一つ食べる。
f:id:alasixOsaka:20200322085556j:plain
高雄に到着

中川まで

高雄から先も清滝川沿いを進むが、国道162号線に入るので車が多い。国道でしかも歩道もないのでとても走りにくい。しばらくは我慢するしかない。中川の集落の手前まで2㎞あまり車と並走。トンネルが見えたので旧道の方に入る。ここは廃道になっているようだが歩行者は通れそうなので行くことにする。車が頻繁に走るトンネルは嫌だ。旧道をしばらく行くと東海自然歩道の道標があった。旧道の入り口には何もなかったのに、唐突に標識がある。入口に標識を立てると廃道を積極的に薦めるような形になるので大人の事情で立てていないのだろうか? いずれにしても車が来ないので快適に走れる。

f:id:alasixOsaka:20200322090236j:plain
中川集落の手前の旧道にある標識
旧道(廃道)を抜けると中川トンネルの手前に出る。ここは、旧道側が集落に向かう道になるので、道が活きている。自然歩道は中川から山に入るので、ここも旧道側を行く。
f:id:alasixOsaka:20200322090431j:plain
中川トンネル手前の分岐

鷹峯へ

中川の集落に入るところでトレイルに入る。今日のルートは舗装路が多くて自然歩道という感じがあまりしない。ここもトレイルかと思いきやしっかり舗装はされている。

f:id:alasixOsaka:20200322090730j:plain
鷹峯へ向かう道の分岐
f:id:alasixOsaka:20200322090802j:plain
看板もある。
途中に滝があった。女滝と男滝があるようだが、男滝の方は道が崩落していて近づけないので見ることができなかった。
f:id:alasixOsaka:20200322090914j:plain
女滝。男滝の方は見ることができない。
ここもダラダラ登る。だんだん登りが走れなくなってきた。最後は峠を越えるような感じでようやくトレイルが現れた。
f:id:alasixOsaka:20200322091225j:plain
ようやくトレイルが。
しかし、ここでトラブル。

左ひざに痛みが発生。

この辺りで左ひざの外側が痛くなってきた。両膝には今日もテーピングをしていて、更に古傷の右足の付け根も少し不安があったのでテーピングをしておいたのだが、まさかの左ひざの外側が痛むとは。
この先は再び京都の市街地に入るのでいつでもリタイア可能だが、ゴールまで行きたいという気持ちもあるのでどうしようか悩む。
とりあえず、峠を降りたところで昼食にして休憩を取り、ロキソニンを摂取して様子を見ることにする。
今日も昼食はカップラーメン。このところちょっとはまっている。前回はポンポン山の山頂で雪の中ぬるいカップラーメンを食べて懲りたので、山専サーモスを買ってみた。

山専サーモスは優秀だった。

さすがに山専というだけあって、お湯はほとんど冷めてなくて、アツアツのカップラーメンが食べられた。外気温が全く違うので一概に比較するべきではないのかもしれないが、やっぱり暖かいものは良い。
少し元気が出たが、足の痛みは治まった気がしない。特に下りがつらい。市街地に出ると傾斜も緩くなるので、やめるのはいつでもできるのでとりあえず進めるところまで進もうと決める。

f:id:alasixOsaka:20200322091923j:plain
トレイルを終えて再び市街地に戻る

市街地を柊野に向かう

市街地に入ってもっとゴミゴミしているのかと思ったが、そのあたりはもう北のはずれの方ということで閑静な住宅街という感じのところを進む。ただ、市街地は道がわかりにくいのが難点。自作のGPSアプリも併用しながら道を確かめながら進む。余談になるが、自作のGPSアプリは自転車で使うことを想定していて、常時ONという想定だったが、トレランで使うとなると時々ONという使い方になって、GPSが切れてしまう。一度GPSが切れるとなかなか現在地を拾ってくれないので、かなり待たされてイライラする。トレラン用途にも使うならバックグラウンドでGPSを動かすようにしないとだめだということに気づいた。
市街地を地図を見ながらゆっくり進んでいると足の痛みもほぼ感じなくなっていた。ロキソニンが効いたのだろうか? ここら辺も北に向かって緩い登りが続くのであまり走れない。スタートから25㎞を過ぎているので前半のダラダラ登りがボディーブローのように効いてきた。本当はこの辺はゆっくりでいいから走り切りたかったのだが、とにかくゴールすることを目標に切り替えて時々走り、時々歩くという感じで進む。
このあたり疲れているせいで写真を全く撮っていない。

夜泣峠へ

柊野で大きな車道に合流。ただ、ここは歩道があるのでまだましだ。市原に抜ける途中で産廃処理場のある道との分岐を過ぎると歩道がなくなったが、その先は車も少ないので割と走り(歩き)良い。
少しの傾斜が足にこたえるのでほとんど走れなくなってきた。この道は雲ケ畑の方へ続いている。途中で右に折れて最後の登りに差し掛かる。

f:id:alasixOsaka:20200322093913j:plain
車道を離れて最後の登りに向かう。
最後の登りを登り切ったところが夜泣峠になるが、ここの登りが一番きつかった。最後の最後でこの登りはさすがにこたえた。ひいひい言いながらようやく登った。
f:id:alasixOsaka:20200322094054j:plain
夜泣峠までの登りはきつかった
ここで最後のおにぎりを食べて一休み。

ゴールの二ノ瀬駅

峠からの下りはまた劇坂で、再び左ひざに痛みが出てきた。ロキソニンの効果もなくなってきたか? とりあえずゴールまで1㎞ほどなのでゆっくりと下ってゴールに向かう。

f:id:alasixOsaka:20200322094329j:plain
夜泣峠から降りたところが本日のゴール二ノ瀬駅
f:id:alasixOsaka:20200322094418j:plain
叡電二ノ瀬駅
二ノ瀬からは叡電に乗って帰った。叡電といえば学生時代にもよく使っていたが、いつもガラガラという印象だったが、結構お客さんが乗っていてびっくりした。鞍馬寺に観光に行った帰りだろうか? 叡電の終点は出町柳。ここで京阪と接続してい便利だ。途中一乗寺の駅を通った。学生時代は一乗寺に住んでいたので一乗寺界隈は懐かしい。ちなみに、宮本武蔵の決闘で有名な一乗寺はもっと山側にあってかなり離れている。今はどうなったか知らないが、学生時代はそこに石碑があって、何代目かはわからないが下がり松もあった。叡電一乗寺駅は有名なラーメン屋の天天有が近くにある。いまでもあるのかな? 学生時代はよく食べに行った。
今回は、予想外のところに足の痛みが出たがとりあえずリタイアせずにゴールすることができたのでまあ、良しとするか。次回はいよいよ比叡山に登ることになる。ポンポン山の比じゃないので覚悟が必要だ。

これまでの総括(一旦 その2)(アンドロイド地図アプリの開発その17)

前回は、JavaクラスでGithubに上がっているmapsforgeのサンプルプログラムを一部変更したものについてまで書きました。

alasixosaka.hatenablog.com

今回は、Javaクラスで、自分で作成したものについて書きます。
その前に、前回の記事の訂正。コピペだけのJavaクラスは実はもう一つあって、Setting.javaもコピペオンリーでした。前回の記事も修正してあります。

自分で作ったJavaクラスは次のものたちです。

  • MainActivity.java
  • FileListDaialog.java

MainActivity.java

MainActivityというといかにも、アプリの本体のようですが、実は単なるメニュー画面です。だいたい、地図アプリというと、起動すると地図が画面に出てくるところを想像してしまいますが、メニューをどうするか考えて、結局起動時にメニュー画面を表示し、地図はサブ画面で表示する形にしました。どうせ、初めにGPXファイルとかを選んだりするので、メニュー画面からスタートしても不便はないと考えたためです。こうすると、地図画面からメニュー画面に戻るときも戻るボタンを押せばよいのでシンプルに操作できます。
MainActivty.javaソースコードです。

public class MainActivity extends Activity implements RadioGroup.OnCheckedChangeListener,FileListDialog.onFileListDialogListener{

    private final static int PERMISSION_REQUEST_CODE = 1;
    private final static int PERMISSION_GPS_CODE = 1001;
    private final static int CHOSE_FILE_CODE = 1002;
    public static Uri launchUrl;

    private SamplesApplication setGPS;

    private static String MAP_FILE = "";
    private static String Path ="";
    String sdPath = Environment.getExternalStorageDirectory().getPath();
    String fillter = "map";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setGPS=(SamplesApplication)getApplication();

        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED)  {
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.ACCESS_FINE_LOCATION,},
                    1001);
            //return;
        }

        final int permission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
        if (permission != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
        } else {
            clickWait();
        }
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        switch(requestCode) {
            case PERMISSION_REQUEST_CODE:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    clickWait();
                    return;
                }
            case PERMISSION_GPS_CODE:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){

                    if(ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION)
                            != PackageManager.PERMISSION_GRANTED){
                        return;
                    }

                }

            default:

        }

        // アプリを終了
        this.finish();
    }
    private void clickWait(){

        Button btClick = findViewById(R.id.btRotationalView);
        Button btMFS = findViewById(R.id.btMapFileSelect);
        Button btGPX = findViewById(R.id.btGPXSelect);
        Button btPOI = findViewById(R.id.btPOISelect);
        BtListener listener = new BtListener();
        btClick.setOnClickListener(listener);
        btMFS.setOnClickListener(listener);
        btGPX.setOnClickListener(listener);
        btPOI.setOnClickListener(listener);
        RadioGroup group = (RadioGroup)findViewById(R.id.radiogroup);
        group.setOnCheckedChangeListener((RadioGroup.OnCheckedChangeListener) this);
        if(setGPS.getGPScondition()==false){
            group.check(R.id.rbOff);
        }else {
            group.check(R.id.rbOn);
        }
    }

    private class BtListener implements View.OnClickListener {
        @Override
        public void onClick(View view){
            //Intent SMV = new Intent(getApplicationContext(),SimplestMapViewer.class);
            //startActivity(SMV);
            int id = view.getId();

            switch (id) {

                case R.id.btRotationalView:
                    Intent RMV = new Intent(getApplicationContext(),RotateMapViewer.class);
                    startActivity(RMV);
                    break;
                case R.id.btMapFileSelect:
                    SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
                    MAP_FILE = preferences.getString(fillter, "berlin.map");
                    Path = preferences.getString("path", sdPath);

                    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
                    intent.setType("file/*");
                    startActivityForResult(intent, CHOSE_FILE_CODE);


                    break;
                case R.id.btGPXSelect:
                    preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
                    Path = preferences.getString("path", sdPath);
                    fillter = "gpx";
                    FileListDialog dlg = new FileListDialog(MainActivity.this);
                    dlg.setOnFileListDialogListener(MainActivity.this);
                    dlg.show(Path,Path,fillter);
                    break;

                case  R.id.btPOISelect:
                    preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
                    Path = preferences.getString("path", sdPath);
                    fillter = "poi";
                    FileListDialog dlg2 = new FileListDialog(MainActivity.this);
                    dlg2.setOnFileListDialogListener(MainActivity.this);
                    dlg2.show(Path,Path,fillter);


            }


        }
    }
    @Override
    public void onClickFileList(File file) {
        if (file == null) {
            Toast.makeText(this, "ファイルが取得できませんでした", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, file.getName(), Toast.LENGTH_SHORT).show();
            SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
            SharedPreferences.Editor editor = preferences.edit();
            editor.putString(fillter, file.getName());
            //editor.putString("path", file.getPath());
            editor.apply();

        }
    }
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data){
        try {
            if (requestCode == CHOSE_FILE_CODE && resultCode == RESULT_OK){
                String filePath = data.getDataString();
                filePath = filePath.substring(filePath.indexOf("storage"));
                String decodefilePath = URLDecoder.decode(filePath, "utf-8");

                File file = new File(decodefilePath);
                String mapfile = (String) file.getName();
                String fpath = decodefilePath.substring(0,decodefilePath.indexOf(mapfile));

                String ext = mapfile.substring(mapfile.indexOf(".")+1);

                if (ext.equals("map")){
                    SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
                    SharedPreferences.Editor editor = preferences.edit();
                    editor.putString("map", mapfile);
                    editor.putString("path", fpath);
                    editor.apply();
                } else {
                    Toast.makeText(this, "not map file", Toast.LENGTH_SHORT).show();
                }

                Toast.makeText(MainActivity.this, mapfile, Toast.LENGTH_SHORT).show();
            }
        } catch (UnsupportedEncodingException e){
            e.printStackTrace();
        }
    }
    public void onCheckedChanged(RadioGroup group, int checkedId) {
        if (checkedId == R.id.rbOn){
            setGPS.setGPScondition(true);

        }else{
            setGPS.setGPScondition(false);
        }

    }
}

起動するとこういう感じの画面が出ます。

f:id:alasixOsaka:20200315125949j:plain
起動時のメニュー画面
できることは、地図ファイルの選択、GPSによる位置情報を反映させるかどうか(GPSのON/OFFではない)、GPXファイルの選択、POIファイルの選択(中身の形式はGPXと同じ)、地図画面への移行となります。

FileListDialog.java

メニュー画面でファイルを選ぶボタンが3つありますが、そのうち地図ファイルの選択については、ファイルマネージャーアプリを呼び出して、そこから選択するようにしています。こいつはファイルの場所を明示してやる役割も兼ねているためです。したがって、当たり前ですが、まず初めに地図ファイルを選択する必要があります。その他のファイルGPXやPOIは同じディレクトリに置いておく必要があります。これらのファイルのディレクトリ、ファイル名はSharedPreferenceに書き込まれているので、いったん選択した後、変更がなければ毎回選びなおす必要はありません。
そして、GPXやPOIを選ぶときは、ディレクトリが決まっているので、そのディレクトリの中から、対応する拡張子のファイルを選択して表示するようにしています。そして、画面のクリックで選択が完了します。
それを行っているのが、FiliListDialogです。ソースコードは次になります。

public class FileListDialog extends Activity implements View.OnClickListener, DialogInterface.OnClickListener {

    private Context _parent = null;
    private File[] _dialog_file_list;
    private onFileListDialogListener _listener = null;
    private int _select_count = -1;
    private boolean _is_directory_select = false;
    private String fill = "";
    private ArrayList<String> arrayList ;
    private File [] finalFileList;


    public int item=0;
    final String[] items = {"Data1", "Data2", "Data3", "Data4","Data5","Data6","Data7","Data8","Data9","Data10","Data11","Data12","Data13","Data14","Data15"};
    private onFileListDialogListener listener;

    public void setDirectorySelect(boolean is){
        _is_directory_select = is;
    }

    public boolean isDirectorySelect(){
        return _is_directory_select;
    }

    public FileListDialog(Context context){
        _parent = context;
    }

    public void show (String path, String title,String fillter) {
        fill = fillter;
        arrayList = new ArrayList<>();
        try {
            _dialog_file_list = new File(path).listFiles();
            if(_dialog_file_list==null){
                if(_listener!=null){
                    //_listener.onClickFileList(null);
                }
            }else {
                //String[] list = new String[_dialog_file_list.length];
                finalFileList = new File[_dialog_file_list.length];
                int count = 0;
                String name = "";

                for (File file : _dialog_file_list){
                    if(file.isDirectory()||file.getName().endsWith(fill)) {
                        name = file.getName();
                        if (file.isDirectory()) {
                            name = name + "/";
                        }
                        arrayList.add(name);
                        finalFileList[count] = file;
                        count++;
                    }
                }
                String [] array = arrayList.toArray(new String[arrayList.size()]);
                new AlertDialog.Builder(_parent).setTitle(title).setItems(array,this).show();
            }
        }catch (SecurityException se){

        }catch (Exception e){

        }




    }

    @Override
    public void onClick(DialogInterface dialog, int which) {
        _select_count = which;
        if((_dialog_file_list==null)||(_listener==null)){

        }else {
            //File file = _dialog_file_list[which];
            File file = finalFileList[which];
            if(file.isDirectory() && !isDirectorySelect()){
                show(file.getAbsolutePath(), file.getPath(),fill);
            }else {
                _listener.onClickFileList(file);
            }
        }

    }

    @Override
    public void onClick(View v) {

    }


    public void setOnFileListDialogListener(onFileListDialogListener listener) {
        _listener = listener;
    }



    public interface onFileListDialogListener{
        public void onClickFileList(File file);
    }
}

このクラスは自分で作ったといいよりもネットの記事を参考にほとんどまねしたようなものなので、あまり胸を張って作ったとは言えないです。

次回は、Javaクラス以外のファイルについて書いてみます。

これまでの総括(一旦 その1)(アンドロイド地図アプリの開発 その16)

前回までで、やりたいと思っていたことのうち高低図の表示以外はできるようになったので、一旦ここまでを総括してみたい。

必要な機能

まず、なぜこのアプリを開発しようと思い立ったかと言うと、以前はガーミンのサイコンを使っていたんですが、老眼になって小さな画面が見辛くなったので、もう少し大きい画面のものはないかと探してスマホのアプリでサイコンとして使えるものがあることを知り、使っていましたが、どうも自分のニーズにぴったりのアプリがない。それなら、自分で作ってしまえと思ったのがことの始まり。しかし、アンドロイドのアプリは作ったことがなく、一から勉強することになって悪戦苦闘の連続でした。まあ、それでもそれなりに使えるところまでやってきました。

自分的に欲しい機能は次の通り。

  • オフラインで地図が使える
  • GPX 形式のルートが表示できる
  • 地図上に右折、左折などの記号が表示できる
  • 高低図の表示

オフラインマップはオープンストリートマップが有名ですが既存のアプリは色々制約があって使いづらい。今回の開発にあたってはオープンストリートマップ系ではあるが保存形式の異なるmapsforge を使いました。特別な理由と言うほどのものもなかったのですが、日本語で記事を書いておられる伊勢在住のプログラマーさんのブログが参考になったのと、それまで使っていたアプリのipbike でmapsforge の地図を使っていて馴染みがあったことが理由でした。

ただ、mapsforge 本家のサイトは不親切そのものでとてもアプリ開発初心者がとっつけるものではありませんでした。とにかく伊勢在住のプログラマーさんのブログを読んで地図を表示させるところから始めました。ソースコードを読んでいるうちになんとなくやっていることが理解できるようになってきたので、mapsforge 本家サイトのサンプルプログラムを読んでやりたい機能を付け足していきました。そんなこんなで高低図以外は一応できるようになりました。まあ、汎用性はまったくない自分専用のアプリになっていますが、自分が使えればそれで良いので。でも、未完成ながら使っているうちに少し使い勝手の悪い部分が見えてきました。主な改善点は次の通りです。

  • スクリーンが最大30分で消える
  • 地図の回転がいま一つ
  • 既に通過したところは色が変わった方が見やすい
  • 高低図はやっぱり欲しい

スクリーンの問題は端末側の設定では30分以上には伸ばせないのでアプリで解決するしかなさそうです。やり方は調べてわかったので簡単にできそうです。

地図の回転に関しては地磁気センサーを使って方位を測定しているのですが、進行方向と地図の向きがずれてるように感じることがままあります。センサーを使わずGPS だけで方位を決めてやった方が違和感が無くなるかもしれません。車に付けている中華製のアンドロイドナビはGPS オンリーですが方位の違和感を感じた事がありません。ただ、動き出さないと正しい方位を示しませんが。そうすると止まった状態で地図の向きが合わないと言う事が起こります。このあたりは使いながらどっちがいいか判断していくしかないでしょう。

通ったところを色を変えると言うのは、地図の方位がずれると言うのと少し関係していて、パッと見てこれから行く方向がわかりづらいときがあって、そんなとき色分けがしてあればわかりやすくなると思ってます。

高低図に関しては、やっぱり登りであとどのくらいで峠なのかを知りたいので、欲しい機能です。ただ、実装するとなるといろいろとハードルがあります。特に、ルートを外れたときの処理が厄介そうです。

ソースコード

アプリは沢山のクラス、xmlなどから構成されています。
長くなるので、サンプルプログラムをそのまま使ったものについては、ソースコードを省略します。また、全文はとっても長いので3回に分けることにしました。
なお、開発環境は、AndroidStudioのVer3.4でパソコンのOSはwindows10です。実端末はXperiaZ4でAndroidのバージョンは6.0です。また、AQUOS SENSE PLUS (Android Version 8.0)でも動くことは確認しています。

クラスは大きく次の3種類からなります。

  • サンプルプログラムと同じもの
  • サンプルプログラムを少し変えたもの
  • 自分で作ったもの

自分で作ったものは次回に書くことにし、それ以外について書きます。
それでもめちゃ長くなりますが。なお、すべてのソースでimportの部分は省略しています。AndroidStudioでは勝手に書き込んでくれますので。
サンプルプログラムと同じもの

  • DefaultTheme
  • MapScaleBarImpl
  • MapScarlBarView
  • Utils
  • Stting(2020/3/15追記、もう一つあるのを忘れていました)

これらは、サンプルプログラムのソースコードをそのまま使っています。

ここからコピーして使いました。
mapsforge/mapsforge-samples-android/src/main/java/org/mapsforge/samples/android at master · mapsforge/mapsforge · GitHub

サンプルプログラムを少し変えたもの

  • OvarlayMapViewer
  • RotateMapViewer
  • SamplesApplication
  • SamplesBaseActivity
  • SimplestMapViewew

OvarlayMapViewerは大幅改変です。サンプルプログラムを土台にしているので、ゴミがいっぱい残っています。そのうちに整理するつもりですが、機能に影響ないのでそのままにしています。このままだとベルリン市街にいろいろなマークが表示されます。マップにいろいろなものをオーバーレイで表示するためのクラスです。GPXファイルを読み込んでルートを表示したり、POIファイルを読み込んでマークを表示する機能を追加しています。

public class OverlayMapViewer extends DefaultTheme implements LocationListener {

    private final static int PERMISSION_GPS_CODE = 1001;

    private LocationManager locationManager;
    private SamplesApplication setGPS;

    private MyLocationOverlay myLocationOverlay;

    private double Latitude;
    private double Longitude;

    protected LatLong latLong1 = new LatLong(52.5, 13.4);
    protected LatLong latLong2 = new LatLong(52.499, 13.402);
    protected LatLong latLong3 = new LatLong(52.503, 13.399);
    protected LatLong latLong4 = new LatLong(52.51, 13.401);
    protected LatLong latLong5 = new LatLong(52.508, 13.408);
    protected LatLong latLong6 = new LatLong(52.515, 13.420);
    protected LatLong latLong7 = new LatLong(52.51, 13.41);
    protected LatLong latLong8 = new LatLong(52.51, 13.42);
    protected LatLong latLong9 = new LatLong(52.52, 13.43);

    protected LatLong latLong10 = new LatLong(52.514, 13.413);
    protected LatLong latLong11 = new LatLong(52.514, 13.423);
    protected LatLong latLong12 = new LatLong(52.524, 13.433);
    protected LatLong latLong13 = new LatLong(52.516, 13.4145);
    protected LatLong latLong14 = new LatLong(52.516, 13.4245);
    protected LatLong latLong15 = new LatLong(52.526, 13.4345);

    protected LatLong latLongX = new LatLong( 34.859881, 135.577572);
    protected LatLong latLongY = new LatLong( 34.859158, 135.581306);

    private static String GPX_FILE = "";
    private static String POI_FILE = "";
    private static String Path ="";
    String sdPath = Environment.getExternalStorageDirectory().getPath();
    private String AtName;
    private String AtValue;
    private String XPname;
    private String POIname;
    private double lati = 0;
    private double longi = 0;

    @SuppressWarnings("deprecation")
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    protected void addOverlayLayers(Layers layers) {

        SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
        GPX_FILE = preferences.getString("gpx", "null.gpx");
        POI_FILE = preferences.getString("poi", "null.poi");
        Path = preferences.getString("path", sdPath);
        //File file = new File(Path, GPX_FILE);

        Polyline polyline = new Polyline(Utils.createPaint(
                AndroidGraphicFactory.INSTANCE.createColor(Color.BLUE),
                (int) (4 * mapView.getModel().displayModel.getScaleFactor()),
                Style.STROKE), AndroidGraphicFactory.INSTANCE);
        List<LatLong> latLongs = new ArrayList<>();
        FileInputStream is = null;
        ArrayList<Marker> markers = new ArrayList<Marker>();

        //GPXファイルの解析
        try {
            String listXmlPath = Path + "/"+ GPX_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);
                                LatLong latLong = new LatLong(lati, longi);
                                latLongs.add(latLong);
                            }


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

        //latLongs.add(latLong1);
        //latLongs.add(latLong2);
        //latLongs.add(latLong3);
        polyline.setPoints(latLongs);

        // this illustrates that bitmap shaders can be used on a path, but then any dash effect
        // will not be applied.
        Paint shaderPaint = Utils.createPaint(AndroidGraphicFactory.INSTANCE.createColor(Color.GREEN), 90, Style.STROKE);
        shaderPaint.setBitmapShader(AndroidGraphicFactory.convertToBitmap(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? getDrawable(R.drawable.marker_green) : getResources().getDrawable(R.drawable.marker_green)));

        Polyline polylineWithShader = new Polyline(shaderPaint, AndroidGraphicFactory.INSTANCE, true) {
            @Override
            public boolean onTap(LatLong tapLatLong, Point layerXY, Point tapXY) {
                if (contains(tapXY, mapView.getMapViewProjection())) {
                    Toast.makeText(OverlayMapViewer.this, "Polyline tap\n" + tapLatLong, Toast.LENGTH_SHORT).show();
                    return true;
                }
                return false;
            }
        };
        List<LatLong> latLongs2 = new ArrayList<>();
        latLongs2.add(latLong7);
        latLongs2.add(latLong8);
        latLongs2.add(latLong9);
        polylineWithShader.setPoints(latLongs2);

        Paint paintFill = Utils.createPaint(
                AndroidGraphicFactory.INSTANCE.createColor(Color.GREEN), 2,
                Style.FILL);
        Paint paintStroke = Utils.createPaint(
                AndroidGraphicFactory.INSTANCE.createColor(Color.BLACK), 2,
                Style.STROKE);
        Polygon polygon = new Polygon(paintFill, paintStroke, AndroidGraphicFactory.INSTANCE) {
            @Override
            public boolean onTap(LatLong tapLatLong, Point layerXY, Point tapXY) {
                if (contains(tapLatLong)) {
                    Toast.makeText(OverlayMapViewer.this, "Polygon tap\n" + tapLatLong, Toast.LENGTH_SHORT).show();
                    return true;
                }
                return false;
            }
        };
        List<LatLong> latLongs3 = new ArrayList<>();
        latLongs3.add(latLong2);
        latLongs3.add(latLong3);
        latLongs3.add(latLong4);
        latLongs3.add(latLong5);
        polygon.setPoints(latLongs3);

        // A polygon filled with a shader, where the shader is not aligned
        Paint paintFill2 = Utils.createPaint(
                AndroidGraphicFactory.INSTANCE.createColor(Color.GREEN), 2,
                Style.FILL);
        paintFill2.setBitmapShader(AndroidGraphicFactory.convertToBitmap(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? getDrawable(R.drawable.marker_green) : getResources().getDrawable(R.drawable.marker_green)));

        Paint paintStroke2 = Utils.createPaint(
                AndroidGraphicFactory.INSTANCE.createColor(Color.BLACK), 2,
                Style.STROKE);
        Polygon polygonWithShaderNonAligned = new Polygon(paintFill2, paintStroke2,
                AndroidGraphicFactory.INSTANCE);
        List<LatLong> latLongs4 = new ArrayList<>();
        latLongs4.add(latLong10);
        latLongs4.add(latLong11);
        latLongs4.add(latLong12);
        latLongs4.add(latLong10);
        polygonWithShaderNonAligned.setPoints(latLongs4);

        Paint paintFill3 = Utils.createPaint(
                AndroidGraphicFactory.INSTANCE.createColor(Color.RED), 2,
                Style.FILL);
        paintFill3.setBitmapShader(AndroidGraphicFactory.convertToBitmap(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? getDrawable(R.drawable.marker_red) : getResources().getDrawable(R.drawable.marker_red)));

        Paint paintStroke3 = Utils.createPaint(
                AndroidGraphicFactory.INSTANCE.createColor(Color.BLACK), 2,
                Style.STROKE);
        Polygon polygonWithShaderAligned = new Polygon(paintFill3, paintStroke3,
                AndroidGraphicFactory.INSTANCE, true);
        List<LatLong> latLongs5 = new ArrayList<>();
        latLongs5.add(latLong13);
        latLongs5.add(latLong14);
        latLongs5.add(latLong15);
        latLongs5.add(latLong13);
        polygonWithShaderAligned.setPoints(latLongs5);

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

        //ArrayList<Marker> markers = new ArrayList<Marker>();
        //markers.add( Utils.createTappableMarker(this,
                //R.drawable.left1, latLongX));
        //markers.add( Utils.createTappableMarker(this,
                //R.drawable.right1, latLongY));
        Marker marker1 = Utils.createMarker(this, R.drawable.right1, latLongY);

        Circle circle = new Circle(latLong3, 100, Utils.createPaint(
                AndroidGraphicFactory.INSTANCE.createColor(Color.WHITE), 0,
                Style.FILL), null) {
            @Override
            public boolean onTap(LatLong geoPoint, Point viewPosition,
                                 Point tapPoint) {
                if (this.contains(viewPosition, tapPoint, geoPoint.latitude,
                        mapView.getModel().mapViewPosition.getZoomLevel())) {
                    Toast.makeText(OverlayMapViewer.this,
                            "The Circle was tapped " + geoPoint.toString(),
                            Toast.LENGTH_SHORT).show();
                    return true;
                }
                return false;
            }
        };

        FixedPixelCircle tappableCircle = new FixedPixelCircle(
                latLong6,
                20,
                Utils.createPaint(
                        AndroidGraphicFactory.INSTANCE.createColor(Color.GREEN),
                        0, Style.FILL), null) {
            @Override
            public boolean onTap(LatLong geoPoint, Point viewPosition,
                                 Point tapPoint) {
                if (this.contains(viewPosition, tapPoint)) {
                    Toast.makeText(OverlayMapViewer.this,
                            "The Circle was tapped " + geoPoint.toString(),
                            Toast.LENGTH_SHORT).show();
                    return true;
                }
                return false;
            }
        };

        layers.add(polyline);
        layers.add(polylineWithShader);
        layers.add(polygon);
        layers.add(polygonWithShaderAligned);
        layers.add(polygonWithShaderNonAligned);
        layers.add(circle);
        for (int i=0; i < markers.size(); i++ ){
            layers.add(markers.get(i));
        }

        layers.add(tappableCircle);
    }

    @Override
    protected void createLayers() {
        super.createLayers();
        setGPS=(SamplesApplication)getApplication();

        // we just add a few more overlays
        addOverlayLayers(mapView.getLayerManager().getLayers());

        Drawable drawable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? getDrawable(R.drawable.ic_maps_indicator_current_position) : getResources().getDrawable(R.drawable.ic_maps_indicator_current_position);
        Marker marker = new Marker(null, AndroidGraphicFactory.convertToBitmap(drawable), 0, 0);
        Circle circle = new Circle(null, 0,
                getPaint(AndroidGraphicFactory.INSTANCE.createColor(48, 0, 0, 255), 0, Style.FILL),
                getPaint(AndroidGraphicFactory.INSTANCE.createColor(160, 0, 0, 255), 2, Style.STROKE));

        myLocationOverlay = new MyLocationOverlay(marker, circle);
        mapView.getLayerManager().getLayers().add(myLocationOverlay);
    }

    private static Paint getPaint(int color, int strokeWidth, Style style) {
        Paint paint = AndroidGraphicFactory.INSTANCE.createPaint();
        paint.setColor(color);
        paint.setStrokeWidth(strokeWidth);
        paint.setStyle(style);
        return paint;
    }
    private Marker createMarker(LatLong latlong, int resource) {
        Drawable drawable = getResources().getDrawable(resource);
        Bitmap bitmap = convertToBitmap(drawable);

        return new Marker(latlong, bitmap, 0, -bitmap.getHeight() / 2);
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.d("DEBUG", "GPSStart");
        if (setGPS.getGPScondition()==true) {
            locationStart();
        }

    }

    @Override
    public void onStop() {
        super.onStop();
        Log.d("DEBUG", "GPSStop");
        if (setGPS.getGPScondition()) {
            SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
            SharedPreferences.Editor editor = preferences.edit();
            editor.putFloat("Latitude", (float) Latitude);
            editor.putFloat("Longitude", (float) Longitude);
            editor.apply();
        }
        locationStop();
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initLocationManager();

    }

    private void initLocationManager() {
        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
    }



    private void checkPermission() {
        if (locationManager != null && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
            Log.d("debug", "location manager Enabled");
        } else {
            // GPSを設定するように促す
            Intent settingsIntent =
                    new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
            startActivity(settingsIntent);
            Log.d("debug", "not gpsEnable, startActivity");
        }


        if(ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION},1001);
        }

    }

    private void locationStart() {
        checkPermission();
        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,1000,0,this);
    }

    private void locationStop() {
        locationManager.removeUpdates(this);
    }

    @Override
    public void onLocationChanged(Location location) {
        Latitude = location.getLatitude();
        Longitude = location.getLongitude();
        this.myLocationOverlay.setPosition(Latitude, Longitude, location.getAccuracy());

        // Follow location
        this.mapView.setCenter(new LatLong(Latitude, Longitude));
    }

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
        Log.d("DEBUG", "called onStatusChanged");
        switch (status) {
            case LocationProvider.AVAILABLE:
                Log.d("DEBUG", "AVAILABLE");
                break;
            case LocationProvider.OUT_OF_SERVICE:
                Log.d("DEBUG", "OUT_OF_SERVICE");
                break;
            case LocationProvider.TEMPORARILY_UNAVAILABLE:
                Log.d("DEBUG", "TEMPORARILY_UNAVAILABLE");
                break;
            default:
                Log.d("DEBUG", "DEFAULT");
                break;
        }
    }

    @Override
    public void onProviderDisabled(String provider) {
        Log.d("DEBUG", "called onProviderDisabled");
    }

    @Override
    public void onProviderEnabled(String provider) {
        Log.d("DEBUG", "called onProviderEnabled");
    }
}
RotateMapViewer

RotateMapViewerは地図を回転表示させるクラスです。自分のアプリではGPSのON/OFFを切り替えて、ONの時は現在地を表示するとともに、地図を端末の向いている方位に合わせて回転させています。回転角は地磁気センサーを使って取得しています。GPSがOFFの時は地図は北を向いて表示するようにしています。ルート全体を見たい時、この先のルートを見たい時などに使うようにしています。GPSがONの状態だと、地図で別の場所を見ようとしても強制的に現在地に引き戻されてしまうので、OFFの機能を付けています。

public class RotateMapViewer extends OverlayMapViewer {

    private SensorManager sensorManager = null;
    private SensorEventListener sensorEventListener = null;

    private float[] fAccell = null;
    private float[] fMagnetic = null;
    float[] saveAcceleVal  = new float[3];
    float[] saveMagneticVal = new float[3];

    private float Latitude;
    private float Longitude;

    private SamplesApplication setGPS;

    @Override
    protected void createControls() {
        //Button rotateButton = (Button) findViewById(R.id.rotateButton);
        //rotateButton.setOnClickListener(new View.OnClickListener() {
        //    @Override
        //    public void onClick(View v) {
        //        RotateView rotateView = (RotateView) findViewById(R.id.rotateView);
        //        rotateView.setHeading(rotateView.getHeading() - 45f);
        //        rotateView.postInvalidate();
        //    }
        // });
        sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);

        sensorEventListener = new SensorEventListener() {
            @Override
            public void onSensorChanged(SensorEvent event) {
                switch (event.sensor.getType()) {
                    case Sensor.TYPE_ACCELEROMETER:
                        fAccell = event.values.clone();
                        LowPassFilter(fAccell);
                        break;
                    case Sensor.TYPE_MAGNETIC_FIELD:
                        fMagnetic = event.values.clone();
                        LowPassFilter2(fMagnetic);
                        if(fAccell!=null) {
                            float[] inR = new float[9];
                            SensorManager.getRotationMatrix(inR, null, fAccell, fMagnetic);
                            float[] outR = new float[9];
                            SensorManager.remapCoordinateSystem(inR, SensorManager.AXIS_X, SensorManager.AXIS_Y, outR);
                            float[] fAttitude = new float[3];
                            SensorManager.getOrientation(outR, fAttitude);
                            //String buf =
                            //        "---------- Orientation --------\n" +
                            //                String.format( "方位角\n\t%d\n", (int)rad2deg( fAttitude[0] )) +
                            //                String.format( "前後の傾斜\n\t%d\n", (int)rad2deg( fAttitude[1] )) +
                            //                String.format( "左右の傾斜\n\t%d\n", (int)rad2deg( fAttitude[2] ));
                            //TextView t = (TextView) findViewById( R.id.textView1 );
                            //t.setText( buf );
                            RotateView rotateView = (RotateView) findViewById(R.id.rotateView);
                            rotateView.setHeading((int) rad2deg(fAttitude[0]));
                            rotateView.postInvalidate();
                        }
                        break;
                }
                //if (fAccell != null && fMagnetic != null){

                //}
            }

            @Override
            public void onAccuracyChanged(Sensor sensor, int accuracy) {

            }
        };




        ImageButton zoomInButton = (ImageButton) findViewById(R.id.zoomInButton);
        zoomInButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mapView.getModel().mapViewPosition.zoomIn();
            }
        });

        ImageButton zoomOutButton = (ImageButton) findViewById(R.id.zoomOutButton);
        zoomOutButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mapView.getModel().mapViewPosition.zoomOut();
            }
        });
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    @Override
    protected void createMapViews() {
        mapView = getMapView();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            findViewById(R.id.rotateView).setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        }
        mapView.getModel().frameBufferModel.setOverdrawFactor(1.0d);
        mapView.getModel().init(this.preferencesFacade);
        mapView.setClickable(true);

        // Use external scale bar
        mapView.getMapScaleBar().setVisible(false);
        MapScaleBarImpl mapScaleBar = new MapScaleBarImpl(
                mapView.getModel().mapViewPosition,
                mapView.getModel().mapViewDimension,
                AndroidGraphicFactory.INSTANCE, mapView.getModel().displayModel);
        mapScaleBar.setVisible(true);
        mapScaleBar.setScaleBarMode(DefaultMapScaleBar.ScaleBarMode.BOTH);
        mapScaleBar.setDistanceUnitAdapter(MetricUnitAdapter.INSTANCE);
        mapScaleBar.setSecondaryDistanceUnitAdapter(ImperialUnitAdapter.INSTANCE);
        MapScaleBarView mapScaleBarView = (MapScaleBarView) findViewById(R.id.mapScaleBarView);
        mapScaleBarView.setMapScaleBar(mapScaleBar);
        mapView.getModel().mapViewPosition.addObserver(mapScaleBarView);

        mapView.setBuiltInZoomControls(hasZoomControls());
        mapView.getMapZoomControls().setZoomLevelMin(getZoomLevelMin());
        mapView.getMapZoomControls().setZoomLevelMax(getZoomLevelMax());
        //initializePosition(mapView.getModel().mapViewPosition);
        SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
        Latitude = preferences.getFloat("Latitude", (float) 34.491297);
        Longitude = preferences.getFloat("Longitude", (float) 136.709685);
        //Latitude = (float) 34.491297;
        //Longitude = (float) 136.709685;
        mapView.setCenter(new LatLong(Latitude, Longitude)); // 伊勢市駅
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
    @SuppressWarnings("deprecation")
    @Override
    protected void createTileCaches() {
        boolean persistent = sharedPreferences.getBoolean(
                SamplesApplication.SETTING_TILECACHE_PERSISTENCE, true);

        Display display = ((WindowManager) getSystemService(WINDOW_SERVICE))
                .getDefaultDisplay();
        final int hypot;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
            android.graphics.Point point = new android.graphics.Point();
            display.getSize(point);
            hypot = (int) Math.hypot(point.x, point.y);
        } else {
            hypot = (int) Math.hypot(display.getWidth(), display.getHeight());
        }

        this.tileCaches.add(AndroidUtil.createTileCache(this,
                getPersistableId(),
                this.mapView.getModel().displayModel.getTileSize(), hypot,
                hypot,
                this.mapView.getModel().frameBufferModel.getOverdrawFactor(), persistent));
    }

    @Override
    protected int getLayoutId() {
        return R.layout.rotatemapviewer;
    }

    @Override
    protected boolean hasZoomControls() {
        return false;
    }

    private float rad2deg( float rad ) {
        return rad * (float) 180.0 / (float) Math.PI;
    }

    final float filterVal = 0.8f;

    public void LowPassFilter(float[] target ){
        float outVal[] = new float[3];
        outVal[0] = (float)(saveAcceleVal[0] * filterVal
                + target[0] * (1-filterVal));
        outVal[1] = (float)(saveAcceleVal[1] * filterVal
                + target[1] * (1-filterVal));
        outVal[2] = (float)(saveAcceleVal[2] * filterVal
                + target[2] * (1-filterVal));

        //現在の測定値を次の計算に使うため保存する
        saveAcceleVal = target.clone();

        //加速度センサーから得た値を書き換える
        fAccell = outVal.clone();
        return ;
    }
    public void LowPassFilter2(float[] target ){
        float outVal[] = new float[3];
        outVal[0] = (float)(saveMagneticVal[0] * filterVal
                + target[0] * (1-filterVal));
        outVal[1] = (float)(saveMagneticVal[1] * filterVal
                + target[1] * (1-filterVal));
        outVal[2] = (float)(saveMagneticVal[2] * filterVal
                + target[2] * (1-filterVal));

        //現在の測定値を次の計算に使うため保存する
        saveMagneticVal = target.clone();

        //加速度センサーから得た値を書き換える
        fMagnetic = outVal.clone();
        return ;
    }

    public void onStart() { // ⇔ onStop
        setGPS=(SamplesApplication)getApplication();
        super.onStart();
        if(setGPS.getGPScondition()==true) {

            sensorManager.registerListener(
                    sensorEventListener,
                    sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
                    (int) 1e6);
            sensorManager.registerListener(
                    sensorEventListener,
                    sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),
                    (int) 1e6);
        }
    }

    public void onStop() { // ⇔ onStart
        super.onStop();

        sensorManager.unregisterListener( sensorEventListener );
    }
}
SamplesApplication

SamplesApplicationでは、GPSのON/OFFを返すルーチンだけを追加しています。

public class SamplesApplication extends Application {

    public static final String TAG = "Mapsforge Samples";

    public static final String SETTING_DEBUG_TIMING = "debug_timing";
    public static final String SETTING_LANGUAGE_SHOWLOCAL = "language_showlocal";
    public static final String SETTING_PREFERRED_LANGUAGE = "language_selection";
    public static final String SETTING_RENDERING_THREADS = "rendering_threads";
    public static final String SETTING_SCALE = "scale";
    public static final String SETTING_TEXTWIDTH = "textwidth";
    public static final String SETTING_TILECACHE_PERSISTENCE = "tilecache_persistence";
    public static final String SETTING_WAYFILTERING = "wayfiltering";
    public static final String SETTING_WAYFILTERING_DISTANCE = "wayfiltering_distance";

    private boolean setGPS;

    @Override
    public void onCreate() {
        super.onCreate();
        AndroidGraphicFactory.createInstance(this);
        Log.e(TAG,
                "Device scale factor "
                        + Float.toString(DisplayModel.getDeviceScaleFactor()));
        SharedPreferences preferences = PreferenceManager
                .getDefaultSharedPreferences(this);
        float fs = Float.valueOf(preferences.getString(SETTING_SCALE,
                Float.toString(DisplayModel.getDefaultUserScaleFactor())));
        Log.e(TAG, "User ScaleFactor " + Float.toString(fs));
        if (fs != DisplayModel.getDefaultUserScaleFactor()) {
            DisplayModel.setDefaultUserScaleFactor(fs);
        }

        MapFile.wayFilterEnabled = preferences.getBoolean(SETTING_WAYFILTERING, true);
        if (MapFile.wayFilterEnabled) {
            MapFile.wayFilterDistance = Integer.parseInt(preferences.getString(SETTING_WAYFILTERING_DISTANCE, "20"));
        }
        MapWorkerPool.DEBUG_TIMING = preferences.getBoolean(SETTING_DEBUG_TIMING, false);

        setGPS = false;
    }
    public boolean getGPScondition() {
        return setGPS;
    }
    public void setGPScondition(boolean setGPS) {
        this.setGPS = setGPS;
    }

}
SmplesBaseActivity

あまりよくわかっていませんが、基本的なセッティングなどを扱っているようです。ここも、基本は大きく変えていませんが、地図ファイルのファイル名やディレクトリなどをSharedPreferenceから読み込むようにしています。

public abstract class SamplesBaseActivity extends MapViewerTemplate implements SharedPreferences.OnSharedPreferenceChangeListener {

    public static final String SETTING_SCALEBAR = "scalebar";
    public static final String SETTING_SCALEBAR_METRIC = "metric";
    public static final String SETTING_SCALEBAR_IMPERIAL = "imperial";
    public static final String SETTING_SCALEBAR_NAUTICAL = "nautical";
    public static final String SETTING_SCALEBAR_BOTH = "both";
    public static final String SETTING_SCALEBAR_NONE = "none";

    protected static final int DIALOG_ENTER_COORDINATES = 2923878;
    protected SharedPreferences sharedPreferences;

    @Override
    protected int getLayoutId() {
        return R.layout.mapviewer;
    }

    @Override
    protected int getMapViewId() {
        return R.id.mapView;
    }

    @Override
    protected MapPosition getInitialPosition() {
        int tileSize = this.mapView.getModel().displayModel.getTileSize();
        byte zoomLevel = LatLongUtils.zoomForBounds(new Dimension(tileSize * 4, tileSize * 4), getMapFile().boundingBox(), tileSize);
        return new MapPosition(getMapFile().boundingBox().getCenterPoint(), zoomLevel);
    }

    @Override
    protected void createLayers() {
        TileRendererLayer tileRendererLayer = AndroidUtil.createTileRendererLayer(this.tileCaches.get(0),
                mapView.getModel().mapViewPosition, getMapFile(), getRenderTheme(), false, true, false,
                getHillsRenderConfig());
        this.mapView.getLayerManager().getLayers().add(tileRendererLayer);

        // needed only for samples to hook into Settings.
        setMaxTextWidthFactor();
    }

    @Override
    protected void createControls() {
        super.createControls();
        setMapScaleBar();
    }

    @Override
    protected void createMapViews() {
        super.createMapViews();

        mapView.getMapZoomControls().setZoomControlsOrientation(MapZoomControls.Orientation.VERTICAL_IN_OUT);
        mapView.getMapZoomControls().setZoomInResource(R.drawable.zoom_control_in);
        mapView.getMapZoomControls().setZoomOutResource(R.drawable.zoom_control_out);
        mapView.getMapZoomControls().setMarginHorizontal(getResources().getDimensionPixelOffset(R.dimen.controls_margin));
        mapView.getMapZoomControls().setMarginVertical(getResources().getDimensionPixelOffset(R.dimen.controls_margin));
    }

    @Override
    protected void createTileCaches() {
        boolean persistent = sharedPreferences.getBoolean(SamplesApplication.SETTING_TILECACHE_PERSISTENCE, true);

        this.tileCaches.add(AndroidUtil.createTileCache(this, getPersistableId(),
                this.mapView.getModel().displayModel.getTileSize(), this.getScreenRatio(),
                this.mapView.getModel().frameBufferModel.getOverdrawFactor(), persistent));
    }

    @Override
    protected String getMapFileName() {
        SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);

        String mapfile = (MainActivity.launchUrl == null) ? null : MainActivity.launchUrl.getQueryParameter("mapfile");
        if (mapfile != null) {
            return mapfile;
        }
        return preferences.getString("map", "berlin.map");
        //return "berlin.map";
    }

    @Override
    protected File getMapFileDirectory() {
        SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
        String mapdir = (MainActivity.launchUrl == null) ? null : MainActivity.launchUrl.getQueryParameter("mapdir");
        if (mapdir != null) {
            File file = new File(mapdir);
            if (file.exists() && file.isDirectory()) {
                return file;
            }
            throw new RuntimeException(file + " does not exist or is not a directory (configured in launch URI " + MainActivity.launchUrl + " )");
        }
        return new File(preferences.getString("path", Environment.getExternalStorageDirectory().getPath()));
        //return super.getMapFileDirectory();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setTitle(getClass().getSimpleName());
    }

    @Override
    protected void onDestroy() {
        this.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
        super.onDestroy();
    }

    /*
     * Settings related methods.
     */

    @Override
    protected void createSharedPreferences() {
        super.createSharedPreferences();

        this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);

        // problem that the first call to getAll() returns nothing, apparently the
        // following two calls have to be made to read all the values correctly
        // http://stackoverflow.com/questions/9310479/how-to-iterate-through-all-keys-of-shared-preferences
        this.sharedPreferences.edit().clear();
        PreferenceManager.setDefaultValues(this, R.xml.preferences, true);

        this.sharedPreferences.registerOnSharedPreferenceChangeListener(this);
    }

    @SuppressWarnings("deprecation")
    @SuppressLint("InflateParams")
    @Override
    protected Dialog onCreateDialog(int id) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        LayoutInflater factory = LayoutInflater.from(this);
        switch (id) {
            case DIALOG_ENTER_COORDINATES:
                builder.setIcon(android.R.drawable.ic_menu_mylocation);
                builder.setTitle(R.string.dialog_location_title);
                final View view = factory.inflate(R.layout.dialog_enter_coordinates, null);
                builder.setView(view);
                builder.setPositiveButton(R.string.okbutton, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        double lat = Double.parseDouble(((EditText) view.findViewById(R.id.latitude)).getText()
                                .toString());
                        double lon = Double.parseDouble(((EditText) view.findViewById(R.id.longitude)).getText()
                                .toString());
                        byte zoomLevel = (byte) ((((SeekBar) view.findViewById(R.id.zoomlevel)).getProgress()) +
                                SamplesBaseActivity.this.mapView.getModel().mapViewPosition.getZoomLevelMin());

                        SamplesBaseActivity.this.mapView.getModel().mapViewPosition.setMapPosition(
                                new MapPosition(new LatLong(lat, lon), zoomLevel));
                    }
                });
                builder.setNegativeButton(R.string.cancelbutton, null);
                return builder.create();
        }
        return null;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.options_menu, menu);
        return true;
    }

    @SuppressWarnings("deprecation")
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        Intent intent;
        switch (item.getItemId()) {
            case R.id.menu_preferences:
                intent = new Intent(this, Setting.class);
                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
                if (renderThemeStyleMenu != null) {
                    intent.putExtra(Setting.RENDERTHEME_MENU, renderThemeStyleMenu);
                }
                startActivity(intent);
                return true;
            case R.id.menu_position_enter_coordinates:
                showDialog(DIALOG_ENTER_COORDINATES);
                break;
            case R.id.menu_svgclear:
                AndroidGraphicFactory.clearResourceFileCache();
                break;
        }
        return false;
    }

    @SuppressWarnings("deprecation")
    @Override
    protected void onPrepareDialog(int id, final Dialog dialog) {
        if (id == DIALOG_ENTER_COORDINATES) {
            IMapViewPosition currentPosition = SamplesBaseActivity.this.mapView.getModel().mapViewPosition;
            LatLong currentCenter = currentPosition.getCenter();
            EditText editText = (EditText) dialog.findViewById(R.id.latitude);
            editText.setText(Double.toString(currentCenter.latitude));
            editText = (EditText) dialog.findViewById(R.id.longitude);
            editText.setText(Double.toString(currentCenter.longitude));
            SeekBar zoomlevel = (SeekBar) dialog.findViewById(R.id.zoomlevel);
            zoomlevel.setMax(currentPosition.getZoomLevelMax() - currentPosition.getZoomLevelMin());
            zoomlevel.setProgress(SamplesBaseActivity.this.mapView.getModel().mapViewPosition.getZoomLevel()
                    - currentPosition.getZoomLevelMin());
            final TextView textView = (TextView) dialog.findViewById(R.id.zoomlevelValue);
            textView.setText(String.valueOf(zoomlevel.getProgress()));
            zoomlevel.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    textView.setText(String.valueOf(progress));
                }

                @Override
                public void onStartTrackingTouch(SeekBar arg0) {
                    // nothing
                }

                @Override
                public void onStopTrackingTouch(SeekBar arg0) {
                    // nothing
                }
            });
        } else {
            super.onPrepareDialog(id, dialog);
        }
    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
        if (SamplesApplication.SETTING_SCALE.equals(key)) {
            this.mapView.getModel().displayModel.setUserScaleFactor(DisplayModel.getDefaultUserScaleFactor());
            Log.d(SamplesApplication.TAG, "Tilesize now " + this.mapView.getModel().displayModel.getTileSize());
            AndroidUtil.restartActivity(this);
        }
        if (SamplesApplication.SETTING_PREFERRED_LANGUAGE.equals(key)) {
            String language = preferences.getString(SamplesApplication.SETTING_PREFERRED_LANGUAGE, null);
            Log.d(SamplesApplication.TAG, "Preferred language now " + language);
            AndroidUtil.restartActivity(this);
        }
        if (SamplesApplication.SETTING_TILECACHE_PERSISTENCE.equals(key)) {
            if (!preferences.getBoolean(SamplesApplication.SETTING_TILECACHE_PERSISTENCE, false)) {
                Log.d(SamplesApplication.TAG, "Purging tile caches");
                for (TileCache tileCache : this.tileCaches) {
                    tileCache.purge();
                }
            }
            AndroidUtil.restartActivity(this);
        }
        if (SamplesApplication.SETTING_TEXTWIDTH.equals(key)) {
            AndroidUtil.restartActivity(this);
        }
        if (SETTING_SCALEBAR.equals(key)) {
            setMapScaleBar();
        }
        if (SamplesApplication.SETTING_DEBUG_TIMING.equals(key)) {
            MapWorkerPool.DEBUG_TIMING = preferences.getBoolean(SamplesApplication.SETTING_DEBUG_TIMING, false);
        }
        if (SamplesApplication.SETTING_RENDERING_THREADS.equals(key)) {
            Parameters.NUMBER_OF_THREADS = preferences.getInt(SamplesApplication.SETTING_RENDERING_THREADS, 1);
            AndroidUtil.restartActivity(this);
        }
        if (SamplesApplication.SETTING_WAYFILTERING_DISTANCE.equals(key) ||
                SamplesApplication.SETTING_WAYFILTERING.equals(key)) {
            MapFile.wayFilterEnabled = preferences.getBoolean(SamplesApplication.SETTING_WAYFILTERING, true);
            if (MapFile.wayFilterEnabled) {
                MapFile.wayFilterDistance = preferences.getInt(SamplesApplication.SETTING_WAYFILTERING_DISTANCE, 20);
            }
        }
    }

    /**
     * Sets the scale bar from preferences.
     */
    protected void setMapScaleBar() {
        String value = this.sharedPreferences.getString(SETTING_SCALEBAR, SETTING_SCALEBAR_BOTH);

        if (SETTING_SCALEBAR_NONE.equals(value)) {
            AndroidUtil.setMapScaleBar(this.mapView, null, null);
        } else {
            if (SETTING_SCALEBAR_BOTH.equals(value)) {
                AndroidUtil.setMapScaleBar(this.mapView, MetricUnitAdapter.INSTANCE, ImperialUnitAdapter.INSTANCE);
            } else if (SETTING_SCALEBAR_METRIC.equals(value)) {
                AndroidUtil.setMapScaleBar(this.mapView, MetricUnitAdapter.INSTANCE, null);
            } else if (SETTING_SCALEBAR_IMPERIAL.equals(value)) {
                AndroidUtil.setMapScaleBar(this.mapView, ImperialUnitAdapter.INSTANCE, null);
            } else if (SETTING_SCALEBAR_NAUTICAL.equals(value)) {
                AndroidUtil.setMapScaleBar(this.mapView, NauticalUnitAdapter.INSTANCE, null);
            }
        }
    }

    /**
     * sets the value for breaking line text in labels.
     */
    protected void setMaxTextWidthFactor() {
        mapView.getModel().displayModel.setMaxTextWidthFactor(Float.valueOf(sharedPreferences.getString(SamplesApplication.SETTING_TEXTWIDTH, "0.7")));
    }

}
SimplestMapViewer

ここが地図を描画する本体部分です。ここもマップファイル名を読み込む部分だけSharedPreferenceから読み込むようにしているだけです。

public class SimplestMapViewer extends MapViewerTemplate {
    /**
     * This MapViewer uses the built-in default theme.
     *
     * @return the render theme to use
     */
    @Override
    protected XmlRenderTheme getRenderTheme() {
        return InternalRenderTheme.DEFAULT;
    }

    /**
     * This MapViewer uses the standard xml layout in the Samples app.
     */
    @Override
    protected int getLayoutId() {
        return R.layout.mapviewer;
    }

    /**
     * The id of the mapview inside the layout.
     *
     * @return the id of the MapView inside the layout.
     */
    @Override
    protected int getMapViewId() {
        return R.id.mapView;
    }

    /**
     * The name of the map file.
     *
     * @return map file name
     */
    @Override
    protected String getMapFileName() {
        SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
        //return "berlin.map";
        return preferences.getString("map", "berlin.map");
    }

    /**
     * Creates a simple tile renderer layer with the AndroidUtil helper.
     */
    @Override
    protected void createLayers() {
        TileRendererLayer tileRendererLayer = AndroidUtil.createTileRendererLayer(this.tileCaches.get(0),
                this.mapView.getModel().mapViewPosition, getMapFile(), getRenderTheme(), false, true, false);
        this.mapView.getLayerManager().getLayers().add(tileRendererLayer);
    }

    @Override
    protected void createMapViews() {
        super.createMapViews();
    }

    /**
     * Creates the tile cache with the AndroidUtil helper
     */
    @Override
    protected void createTileCaches() {
        this.tileCaches.add(AndroidUtil.createTileCache(this, getPersistableId(),
                this.mapView.getModel().displayModel.getTileSize(), this.getScreenRatio(),
                this.mapView.getModel().frameBufferModel.getOverdrawFactor()));
    }

    @Override
    protected MapPosition getInitialPosition() {
        int tileSize = this.mapView.getModel().displayModel.getTileSize();
        byte zoomLevel = LatLongUtils.zoomForBounds(new Dimension(tileSize * 4, tileSize * 4), getMapFile().boundingBox(), tileSize);
        return new MapPosition(getMapFile().boundingBox().getCenterPoint(), zoomLevel);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setTitle(getClass().getSimpleName());
    }
}

ふう!! 長くなったので続きは次回に。

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スケールなどしか使わないだろうと思われるので実害はないと判断しました。
一応、ここまでで、高低図の表示以外は初めに考えた処理が全部できたということになります。高低図ははっきり言ってハードルが高いのでできる自信がないですが、少し頑張ってみようと思っています。いろいろ使っていると使い勝手の悪い部分が見えてきたので、そのあたりも含めて、次回は一旦総括をしてみようかと思っています。

パン作りその2

悪戦苦闘しながらパンを焼いています。今回はその続きです。

 

alasixosaka.hatenablog.com

 

パネトーネマザーを使ってみる

ネットでいろいろ調べていたら、パネトーネマザーという酵母があって、結構膨らみが良いとのことなので、使ってみることにした。

伊勢丹に富澤商店のお店が入っているのでそこで買ってきた。

パネトーネマザーを使ったプチパン(その1)今一つ

まずは、手成型でプチパンを焼いてみた。イメージは丸いバターロールのような感じ。

材料

  • 小麦(春よ恋)200g
  • 砂糖15g
  • 塩2g
  • 卵1/2
  • 牛乳 100ml
  • バター10g
  • 水 170ml

HB を使って1次発酵まで。発酵温度の設定は中、1時間。8分割してベンチタイム20分。成形してオーブンの発酵機能で2次発酵、35℃で80分。膨らみが足りないので追加10分。オーブン180℃で30分焼成
思ったより固く仕上がった。温度はもう少し低くても良いと思う。

パネトーネマザーを使ったプチパン(その2)成功

今度は全粒粉を使ったプチパンを焼いてみました。イメージは丸いフランスパンという感じ。基本はフランスパンのレシピを基にしています。フランスパンは通常中力粉を使います。強力粉に薄力粉を混ぜるという方法もありますが、今回は春よ恋の強力粉を使いました。最近買った本では、春よ恋は結構さくっとした仕上がりになるということなのであえて薄力粉を混ぜるほどでもないかと思って、強力粉と全粒粉のブレンドでいってみました。

 

 この本はめちゃ勉強になりますね。一口に国産小麦といってもいろんな種類があって、それぞれに特色があることが書いてあります。なんも知らずに適当に焼いていたので、うまくいかないのも当然かなと思いました。

さて、プチパンですが材料はこんな感じ。本の記述を参考に水は結構減らして60%にしてみました。

 

材料

  • 小麦粉(春よ恋)170g
  • 全粒粉(春よ恋)30g
  • 砂糖 6g
  • 塩 3g
  • パネトーネマザー 6g
  • レモン 4ml
  • 水 120ml

 

フランスパン風にしあげるということで、バターやミルクなどは入れていません。

作り方

作り方は、MK精工のHPを参考にしました。

HBに材料をすべて入れて、中速で20分ねる。ねり終わったら、発酵モード低温で1次発酵。70分。

発酵後生地を取りだし、ガス抜き、6分割。丸めてラップをかぶせてベンチタイム20分。

ガス抜きをしてから、丸め直して、オーブンの発酵機能を使って2次発酵。35℃で35分。

生地の上に小麦粉をふる。オーブンを余熱し、生地にクープを1本入れ、オーブンのフランスパンモードで焼く。

今回は結構うまくいきました。クープはもっとざっくり入れても良いのかもしれません。ちょっと浅かった。

臭いはちょっと臭みがありましたが、味はとても美味しかった。ちょっと癖になりそうな感じ。

f:id:alasixOsaka:20200224103402j:plain

パネトーネマザーを使ったフランスパン風プチパン

サンドイッチ風にして、翌日のレース後に食べてみた。一晩冷蔵庫に入れてあったが、かなり美味しくこれは成功だった。

パネトーネマザーの食パン

今度は食パンにチャレンジしてみました。食パンはなかなか膨らまないのが難しいところですが、果たして。

レシピはパネトーネマザーのHPを参考にしました。ここに、春よ恋で焼いた例が載っていました。

www.panettone-mother.com

材料

  • 小麦粉(春よ恋) 400g
  • 砂糖 24g
  • 塩 7g
  • スキムミルク 9g
  • バター 22g(予定は24gだったが、正確に測るのが難しいので結果的に22gになってしまった)
  • パネトーネマザー 16g
  • 水 240g

水はかなり控えめにしたが、こねてる途中は結構べっちょりで多いのかと心配した。最終的にはつやのある生地に仕上がった。

作り方

HBにバター以外の材料を投入。ソフトパンモードで1.5斤を選んでスタート。バターは手動モードの癖で10分後に入れようと思ったら、8分でこねが終わって予備発酵になってしまった。仕方ないので、予備発酵の終了(約85分後)を待って投入。

今回はすごく膨らんで思った通りの感じで出来上がった。ただ、2次発酵の後のガス抜きでガスが抜けきっていないと思ったが案の定で、パンの中に大きな気泡ができていた。まあ、全自動でここまでできれば文句ないと思う。

f:id:alasixOsaka:20200224103922j:plain

釜からはみ出すぐらいまで膨らんだ。

f:id:alasixOsaka:20200224103959j:plain

切ったら中には大きな気泡が。それでもパネトーネマザーは偉大です。

予約を使ってもう一度食パンを焼いてみました。

今度は、前回よりもうまくいった。ガス抜きもばっちりで、中に大きな気泡もなくきれいに膨らんでいました。パネトーネと春よ恋の組み合わせはこのレシピでOKのようです。手持ちの春よ恋はほとんどなくなってしまいましたが(笑)。

 

次は、ピコリーノ式(1.5倍起こし)のホシノ天然酵母を使ってみたいと思っています。