タイル抜け問題の対応(アンドロイド地図アプリの開発 その7)

メインマシンのアップグレードもひと段落ついて、久しぶりに地図アプリ開発の記事を書きます。
喘息の発作が出たり、マシンのアップグレードではまりまくったり、その間にトレランのレースにも出て、結構いろんなことがあって、Android Studioを動かすのも久しぶりで、何をやったか忘れていて、思い出すところから始めないダメだったりして、スムーズにいきませんでした。また、作ったアプリも実は問題があったことが判明したので、今回はそれの対応ということになります。その問題点は、

地図を回転させるとタイル抜けが発生してしまう。

ということがわかりました。

f:id:alasixOsaka:20190812160158j:plain
こんな感じで、地図が所々抜けてしまう。
この現象は、ズームイン、ズームアウトを使って、回転をかけたときに発生するようで、始めは、タイルキャッシュの使い方を疑ってみた。つまり、その5で書いたように、地図データをタイルキャッシュというキャッシュに読み込んで表示をしているみたいなのですが、
alasixosaka.hatenablog.com
サンプルプログラムを読んでいると、キャッシュの使い方が2通りあって、普通の変数を使う方法と配列を使う方法がある。
最もシンプルなGettingStarted.javaの方法だと、

TileCache tileCache = AndroidUtil.createTileCache(this, "mapcache",
                    mapView.getModel().displayModel.getTileSize(), 1f,
                    mapView.getModel().frameBufferModel.getOverdrawFactor());

TileRendererLayer tileRendererLayer = new TileRendererLayer(tileCache, mapDataStore,
                    mapView.getModel().mapViewPosition, AndroidGraphicFactory.INSTANCE);

のように通常の変数を使っている。自分のアプリも、伊勢在住のプログラマーさんアプリもこのやり方を使っている。
ところが、もう少し複雑なことをしているプログラムでは、

protected void createTileCaches() {
        this.tileCaches.add(AndroidUtil.createTileCache(this, getPersistableId(),
                this.mapView.getModel().displayModel.getTileSize(), this.getScreenRatio(),
                this.mapView.getModel().frameBufferModel.getOverdrawFactor()));
    }

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);
    }

のように配列を使っている。ただ、見たところtileCashes.get(0)となっていて、配列の1番目の変数を使っているようにしか見えないので、違いがよくわからなかった。
ただ、気になっていた点ではあったので、まずはここを変えてみた。が、結果は同じだった。
そうなると、プログラムをちゃんと理解していないので、何が悪いのかさっぱりわからない。
サンプルプログラムではタイル抜けは発生していないので、何か原因があるはずなのだが、力量不足で突き止められない。仕方ないので、サンプルプログラムと同じように動かしてみることにした。
と言っても、GitHubから単にクローンしてAndroid Studioに読み込んだだけでは、複雑怪奇なプログラムの塊が出来上がるだけで、何が何だかさっぱりわからない。

f:id:alasixOsaka:20190812162949j:plain
GitHubからmapsforgeのサンプルプログラムをクローンするとこうなる
わかる人にはわかるのかもしれないが、自分には魑魅魍魎のの世界にしか思えない。これでどこからプログラムが始まるのかすらわからない。でも、動かすちゃんと走るので何とも不思議。
仕方ないので、地道にプログラムをくみ上げていくことにする。GitHubのmapsforgeのサイトから、mapsforge-samples-android/src/と開いていくと、javaファイルがいっぱい現れる。ところが、MainActivity.javaというファイルはないので、どれがメインプログラムなのか、適当に読んで当たりをつける。
どうも、Samples.javaがそうらしい。そこで、Empty Activityを作って、MainActivityをSamples.javaにリネームして、中身をコピペする。中身を見ると、

        LinearLayout linearLayout = findViewById(R.id.samples);
        linearLayout.addView(createButton(GettingStarted.class));
        linearLayout.addView(createLabel(null));
        linearLayout.addView(createButton(SimplestMapViewer.class));
        linearLayout.addView(createButton(DiagnosticsMapViewer.class));

こんな感じに、メニューと思しき記述がずらずらと並んでいる。メニュー画面の実際はこんな感じ。

f:id:alasixOsaka:20190812164046j:plain
サンプルプログラムを動かしたときのメニュー画面
とにかく、地図が回転できれば良いので、不要なメニューはコメントアウトして動かないようにする。
地図を回転するところは、RotateMapViewer.javaを呼び出している。ボタンをクリックすると、それぞれに対応するプログラムを呼び出している。その部分は上の方のここの部分らしい。

if (customListener == null) {
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    startActivity(new Intent(Samples.this, clazz));
                }
            });
        } else {
            button.setOnClickListener(customListener);
        }

clazzに呼び出すプログラムの名前が入っていて、startActivity(new Intent(Samples.this, clazz));で呼び出しているようだ。
したがって、次にやることは、新しいJavaクラスを作成して、RotateMapViewer.javaという名前にして、GitHubのファイルを開いて中身をコピペすること。これで終われば簡単なのだが、RotateMapViewer.javaは頭の部分が

public class RotateMapViewer extends OverlayMapViewer {

となっていて、OvalayMapViewerを継承している。そこで、また新しいJavaクラスを作成し、OvalayMapViewerという名前にして、GitHubのプログラムをコピペする。この、OvalayMapViewerというのは、バルーンだったり、線や面を地図に上書きするサンプルで、いらないと思うのだが、継承関係を無視すると、Android Studioであちこちに赤字が出てエラーになるので、無視できない。ところが、OvalayMapViewerはまた、DefaultTheme.javaを継承していて、DefaultThemeはまた、SamplesBaseActivityを継承している。仕方いないので、次々とJavaクラスを作成する羽目になった。その他にも、呼び出されているJavaクラスがあって、結局、10個のJavaクラスを作成した。

f:id:alasixOsaka:20190812165822j:plain
全部で10個のjavaクラスを作った。
まだまだ、やることはたっぷりあった。次にbuild.gradleに依存関係を記述する。依存関係の記述の末尾に下記を追記。

    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'

xmlファイルもたくさん必要だ。
f:id:alasixOsaka:20190812170347j:plain
図の中で、activity_main.xlmは初めにできたファイルで実は必要ない。その他のファイルをGitHubのmapsforge-samples-android/res/から探してきて、ファイルを作成し、中身をコピペする。
そして、drawableも必要だ。

f:id:alasixOsaka:20190812170753j:plain
必要なdrawable
drawableは、画面に表示するための絵で、hdpiとmdpiの2種類があるが、hdpiだけでも動くようだ。今回は、mapsforge-samples-android/res/drawable-hdpiの中身を使った。
最後はAndroidmanifest.xmlに呼び出すjavaクラスを追記、とりあえず、必要そうなOvalayMapViewer.java、RotateMapViewer.javaとSetting.javaを書いておいた。また、SamplesApplicarionは、Applicationを継承しているので、<applicationの下に android:name=".SamplesApplication"を追記する。

<application
        android:name=".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=".Samples">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

        <activity
            android:name=".OverlayMapViewer"
            android:configChanges="keyboardHidden|orientation|screenSize" />

        <activity
            android:name=".RotateMapViewer"
            android:configChanges="keyboardHidden|orientation|screenSize" />

        <activity
            android:name=".Setting"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:label="Settings">
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value=".Samples" />
        </activity>
    </application>

これでエラーが解消したので、いざ動かしてみようととしたのが、次の画面。

f:id:alasixOsaka:20190812171433j:plain
メニュー画面。(不要なメニューを省いた)
不要なメニューを省いたのですっきりしている。GraphHopperが残っているのはご愛敬というところで。
ところが、ここで、RotateMapViewerをクリックするとエラーで止まってしまう。デバッグモードで動かして、原因を見てみると、Berlin.mapのファイルが開けないということのようだ。しばらく悩んだが、ふと伊勢在住のプログラマーさんの記事を思い出した。最近のAndroidはファイルを開くのに、パーミッションが必要で処理を記述する必要があった。それなら、パーミッションの記述を書いてやればと思ったが、ここで、アクティビティーの構造が理解できてないので、どこに書いたら良いのかわからない。OnCreateでメニュー画面を作って、OnResumeでボタンのクリックを待っているみたいなのだが、初歩的なプログラムの書き方をしてくれてないので、今一わからない。
仕方ないので、簡単なメニュー画面なら自分で作ってしまったほうが早いということで、作ることにした。ボタンを一つ配置し、クリックするとRotateMapViewer.javaが動くようにしてやればよいと考えた。
それにパーミッションの記述を加えてやればなんとかなるだろう。作ったMainActivityがこれ。

public class MainActivity extends Activity {

    private final static int PERMISSION_REQUEST_CODE = 1;
    public static Uri launchUrl;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //パーミッションが許可されていたらクリック待ち、されていない場合はダイアログを出す。
        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;
                }
            default:
        }

        // アプリを終了
        this.finish();
    }
    private void clickWait(){
  //ボタンのクリック待ち。
        Button btClick = findViewById(R.id.btRotationalView);
        BtListener listener = new BtListener();
        btClick.setOnClickListener(listener);
    }

    private class BtListener implements View.OnClickListener {
  //クリックされたらRotateMapViewer.javaを起動
        @Override
        public void onClick(View view){
           
            Intent RMV = new Intent(getApplicationContext(),RotateMapViewer.class);
            startActivity(RMV);
        }
    }
}

f:id:alasixOsaka:20190812174023j:plain
自分で作ったメニュー画面
ところが、バルーンやマークは表示されるが、肝心の地図が表示されない。ここでもいろいろ悩んだが、RotateMapViewer.javaには地図を表示するという記述がないので、まずは何らかの方法で地図を描いてやってから回転すればよいのかと思い、SimplestMapView.javaを先に起動することにした。すると、今度はちゃんと地図が表示され、タイル抜けもなく表示することができた。しかし、サンプルプログラムではパーミッションの記述があるように思えないし、パーミッションの許可を求めてこない、しかし、自分でプログラムを書くと、ほとんどコピペなのにもかかわらず、パーミッションの許可が必要だった。なんか納得がいかないがとりあえず動いた。でも、シンプルだったプログラムがかなりごちゃごちゃになってしまった。この先どうなるかちょっと不安になる。
f:id:alasixOsaka:20190812180709j:plain
サンプルプログラムをベースにするとタイル抜けは解消した。
次は、実機を使って、端末のセンサーで地図を回転できるようにしてみたい。