トップ地図アプリMapX > 地図アプリMapX最初の一歩

地図アプリMapX最初の一歩

MapXプロジェクト作成

minSdkを Android 9 として空アプリを作成した。

  1. 地図アプリアイコン ic_map_blue24.png を res/drawable にコピーした。
  2. manifestは Mapアプリのものをコピーした。android:labelの Map は MapX に変更した。 serviceはとりあえずコメントアウトした。

この段階では、gradle の dependenciesのバージョンが合わない。 多分、Android Studio または SDKのバージョンを一時、上げていた影響であろう。 アプリMapのものをそのままコピーした。

これで最初の画面 Hello World! が LDPLayerの画面に表示された。

これで、LDPlayerで地図アプリのデバッグができる見込みがついたので、 このあとは Slow and steady wins the race. で行こう。

ファイルの置き場所

スマホでは地図アプリのデータファイルは SDカードに置くが、 エミュレータではSDカードのディレクトリの表記方法がはっきりしない。 NoxPlayerでは /sdcard らしいが、ファイルマネージャーアプリで簡単に操作できない。 したがって、内蔵ストレージに当たる /storage/emulated/0/Map に置くことにする。

地図データを共有フォルダを経由して、ここにコピーできた。

現在は Windows上の Java で地図データを作成しているが、エミュレータで作成することもできる。 スマホではパフォーマンス上不可能に近いので、現行方式の方が無難であろう。 Android Java のプログラムを大きくするのは避けたい。

メニューバー

メニューは R.menu.my_toolbar.xml で設定する。コードは下記の通りである。

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.my_toolbar, menu);
        MenuCompat.setGroupDividerEnabled(menu, true);
        return true;
    }

class Map extends View

地図は class Map extends View に描画する。

レイアウトはボタンやテキスト・ビューと同様で、次のようにする。これで、画面一杯に地図が描画できる。

  <com.example.mapx.Map
      android:id="@+id/map_view"
      android:layout_width="match_parent"
      android:layout_height="match_parent" />

Codeは MainActivity#onCreate() で以下のようにする。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Utils.writeLog("MainActivity.onCreate");
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        ...
    }

Mapアプリの activity_main.xml をコピーして、class Map の骨組みだけをコピーした。 ボタン+、ーおよびコピーライトのTextViewだけ表示された。

国土地理院地図の表示

国土地理院地図の表示ではレンダリングはなく、ネットからダウンロードしたタイル画像ファイルを 表示するだけである。

しかし、よく考えて分かりやすいプログラムとしたいので、じっくり取り組みたい。

    Tile.Source src;  // gsi, ort, osm, japan, kanto, hot
    int zoom;
    int CX, CY;       // 地図中心タイル座標(単位:画素)

スマホ実機とエミュレータの区別

実機は Android 12(API level 31)、エミュレータ LDPlayer 9 は Android 9(API level 28) である。 この API level の違いによって、一部、プログラムコードを変える必要が生まれる。 API level は Android Java では Build.VERSION.SDK_INT によって得られる。

また、たとえ Android OSのバージョンが同じであっても、 スマホとパソコンではディスプレイの解像度が大きく異なる。 タイル画像は 256x256画素であり、パソコンはこれでいいが、 高解像であるスマホでは 768x768画素に拡大して表示する必要がある。 このため、何らかの方法でスマホかPCかを判断する必要がある。 機種を知るときは Build.MODEL を使用する。あるいは、画面サイズから解像度を判定してもいいであろう。

LDPlayer 9 の場合、Build.MODEL は SM-G975N となるが、Samsung SM-G975N と解像度が同じというわけでない。

わかったこと

このパソコンの仕様では "14.0-インチ FHD (1920 x 1080) IPS Truelife LED-バックライト ディスプレイ" となっている。 エミュレータでの画面サイズは 1600x768 である。これが、実際に地図が表示される画面のサイズである。 スマホは、縦長であるが、回転した場合、横長になるかも知れない。

dpiは1インチ(2.54センチ)にどれだけのドット(ピクセル)があるか、 dpは『密度非依存ピクセル(density independent pixels )』の略で、 px = dp * ( dpi / 160 ) の関係となっている。

1 dp は、中密度画面(160 dpi、「ベースライン」密度)の 1 ピクセルとほぼ等しい仮想ピクセル単位である。

float density = getResources().getDisplayMetrics().density; の値は エミュレータでは 1.5 となった。しかし、これはパソコン画面ではなく、 スマホ Samsung SM-G975N の値かも知れない。その場合は、無意味な数値と言える。 一方、現スマホでは 2.625 となった。この値から察すると、パソコン画面は 1.0 前後のはずである。

暫定的には、この値が 2.5 以上のとき、スマホと判断して、1タイルを 768x768画素とする。 2.5未満のときパソコンによるエミュレータとして1タイルを 256x256画素とする。


density = 1.5 の場合、1タイル=256x256画素では、地図が小さ過ぎるようだ。 LDPlayerの設定変更で、density = 1.0 に変更できないか。

設定でサイズを自由に変更できる。しかし、パソコンも 1920 x 1080 で動かしていないようであり、 取りあえず、1300 x 700、240DPI でパソコン画面に近くなった。 必ずしも、パソコンに合わせる必要はない。今後、適宜、調整する。

終了時の状態保存と再開

特にデバッグ時はアプリの起動と終了が何度も起きる。終了時の状態(地図のソース、zoom、CX、CYなど)を 保存しておき、次の起動ではこれらの値を使う方が望ましい。

地図アプリ Map ではこれらの値を SharedPreference に保存している。一般にはこの方法がスマートであろう。 しかし、地図アプリ Map でも、歩数計に関する情報はこの方法は使わず、データファイルに保存している。 特に、デバッグ段階では、この値を手作業で修正したり、消したりしたいことがある。 SharedPrefrenceではこれができない。

開発段階で不正な値が書きこまれると、最悪ではアプリがダウンする。 エラーチェックに気を配れば、対応はできるが、その分プログラムが増える。 手作業で、このファイルを修正してから、アプリを起動する方が楽なため、このようにした。

おわりに

国土地理院の標準地図および航空地図をダウンロード表示するまでを実装した。 ここまでは簡単である。

アプリのメモリ使用量はこの段階では 10MB 未満である。

zoom 8 での画面例を下に示す。ここではタイル境界線も表示した。 横長でもあり、ツールバーの高さはもっと小さくてもいいが、小さくする方法は見つからなかった。 スマホ実機の縦長ではさほど大きく感じないが、可能ならば、少し小さくしたい。

アプリMapではタイトルの前の地図アイコンは 32x32 画素であるが、Map では 24x24 画素とした。 背景が透明で白色のため、元々のアイコンファイルのサイズ 32x32 を 24x24 に縮小するのは簡単ではない。

次のようにすれば、アイコンのサイズを 24x24 にできることが分かった。

[map24dp_white.xml]
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- pngファイルであればbitmapタグでも表示可能 -->
    <item
        android:width="24dp"
        android:height="24dp">
        <bitmap android:src="@drawable/map32_white" />
    </item>
</layer-list>

ダウンロード済みのファイルは 30日間はそのまま表示した。 それを超える場合は、サーバーにそのタイルの更新時間を問い合わせ、 更新がなければそのまま使い続け、更新されている場合は新たにダウンロードするようにした。

現在のところ、国土地理院地図の使用は従で、主に、OpenStreetMap を使っているので、 これでもいいかも知れないが、 更新されていないタイルは何度も更新がないか問い合わせが起きる。 一度問い合わせて、更新がなかった場合、ダウンロードはしないが、 現在のファイルの時刻を現在時刻に変更しておけば、問い合わせは精々、30日に一度になる。

プログラムをこのように修正して、スマホ実機で時刻更新を確認した。

来歴

2023.6.30 強制終了

finish() での終了では、うまく再開しない。 今はまだ サービス(位置情報取得)を実装していないが、もし、System.exit(0)で強制終了できる場合、 おそらく、サービスを終了させる必要があるだろう。

finish() では別のアプリがアクティブになるだけなのだろう。

Android Studio の Logcat に頼るのではなく、ファイルに独自のログを書き込んでみよう。

Android Studio もクローズして、何度か finish、再開を繰り返すと、2,3回はうまくいくが、 やがて、何も表示されなくなる。一旦、アプリを消すと、同じことの繰り返しとなる。

表示状態のセーブと回復は問題ないようだ。タイル描画のところでログを取ってみよう。 非アクティブになったとき、一部のデータが廃棄されたのかも知れない。 finish() や onPause などで、タイルキャッシュをセーブして、 onResume で回復すればいいかも知れないが、面倒であり、時間もかかる。 地図アプリMapXを常時動かしておれば、Android OSが MapX を重要アプリと認知して、 非アクティブでデータを廃棄することがなくなるのかも知れない。

一般には、バッテリ消費を少しでも抑えるために、データ廃棄が起きるのかも知れない。


とりあえず、finish処理のあと System.exit(0) を実行することにした。

2023.6.28 実機デバッグ

機種依存の初期化は機種が分かってからでないとできない。

次のエラーはアドレス間違いか?

 D/dl error: java.io.FileNotFoundException: https://cyberjapandata.gsi.go.jp/xyz/std/14/4850/2148.png
    https://cyberjapandata.gsi.go.jp/xyz/std/14/4850/2148.png

SharedPreferencesを使うのを止めると正常にタイル地図が表示された。 少なくとも、デバッグ段階では SharedPreferences の使用はやめた方が無難である。

2023.6.27 Permission denied

最初のログ書き込みテストでPermission deniedとなった。予め、ファイルを作った場合も同じ結果となった。 スマホ実機ではMapアプリに権限を与えていたので、これに相当する操作がいるのであろう。

プログラムでダイアログを使う方法もあるが、設定でアプリMapXにファイル操作権限を与えた。 これで、ログ書き込みが行われるようになった。

エラーは適宜トーストで表示すべきであろう。

リファレンス

[1] [Android] スクリーンサイズを取得する