プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

OpenStreetMap のオフラインマップ作成時の不具合の修正 (1/3)

避難所検索@伊勢 ではオフラインマップを表示しているのですが、いつの頃からか、洪水状態になっています(バージョン 1.0beta4 )。

ちょっと余裕ができたので、原因究明と対策を取りましたので、メモっておきます。

なお、全部で3つに分かれています

この記事の目次は次の通りです。

問題の現象

こんな感じで、洪水のようになってます。

f:id:junichim:20200918152600p:plain

正しくはこんな感じになるはずです(https://openstreetmap.org で表示)。

f:id:junichim:20200918152827p:plain

ちなみに、同じ場所を openstreetmap.org の編集エディタの iD で開くとこんな感じになります。

f:id:junichim:20200918153011p:plain

画像だとちょっとわかりにくいかもしれませんが、どうも、洪水になっているところは、地物が何も描かれていないところに該当しているようです。

海岸線のチェック

まずはオフラインマップに使っている海岸線が正いかどうか調べてみました。

OpenStreetMap の各種データを検証するためのサイト

OSM Inspector | Geofabrik Tools

上記のサイトで海岸線の状態をチェックできるとのことです。実際に調べてみると、2020/9/24 時点ですが問題ないようでした。

f:id:junichim:20200924135256p:plain

ということは、地面を表す特定のタグに対する描画色指定の問題っぽい感じがしてきます。 とはいえ、結論を急ぐ前に、もう少しいろいろと調べてみます。

アプリと map ファイルの組み合わせを確認

次に、アプリと map ファイルの組み合わせで確認してみます。

アプリとしては、

の2つ。

map ファイルは

  • mapsforge のプリコンパイルファイル, ベルリンのもの, berlin.map
  • 避難所検索@伊勢, 1.0beta4 リリース時点でapkファイルに含めているファイル
  • オフラインマップの更新ファイルの最新版, 2020/9/18時点

の3種類を試します。

map ファイルの準備

berlin.map は mapsforge のダウンロードサーバーから落としておきます(2020/9/18 時点のものを使いました)。

避難所検索@伊勢, 1.0beta4 リリース時点でapkファイルに含めているファイルは、 github のリポジトリにも含まれているので、それを用意します。

あと、オフラインマップの更新ファイルの最新版(2020/8/24 作成)のファイルを S3 から落としておきます。

なお、各アプリでこれらの map ファイルを参照するときは、適宜ソースコード上のファイル名と初期表示位置の緯度経度を修正しています(また、避難所検索@伊勢は現在位置に初期位置を移動させる機能もあるので、それもコメントアウトしています)。

mapsforge のサンプルアプリでの動作確認

mapsforge のリポジトリを clone して、 AndroidStudio でプロジェクトを開きます。何もしない状態で一度インストールと実行しておきます。こんな感じの初期画面が出てきます。

f:id:junichim:20200918150808p:plain

この一番上の『GettingStarted』ボタンを押すと、シンプルに地図表示だけを行えるので、これを使います。

berlin.map

mapsforge のサンプルを見ると berlin.map を設定しなさいとあります。初回実行時にも下記のように注意が出てきます。

f:id:junichim:20200918150839p:plain

指示通りに、ダウンロードした berlin.map を実機の所定のフォルダにコピーします。

C:\Users\mor\home\work\tmp\mapsforge>
C:\Users\mor\home\work\tmp\mapsforge>adb push berlin.map /sdcard/Android/data/org.mapsforge.samples.android/files/
berlin.map: 1 file pushed, 0 skipped. 15.9 MB/s (39343572 bytes in 2.367s)

C:\Users\mor\home\work\tmp\mapsforge>

もし、 org.mapsforge.samples.android がなければ、(この時点では何も地図が表示されないけど)一度 GettingStarted ボタンを押して地図を表示させるとフォルダが作られると思います。

サンプルアプリで地図を表示させるとちゃんと出ているのですが、ベルリンだと地物が多いので、土地が表示されているのか、地物が表示されているのか区別つきません。

f:id:junichim:20200918151525p:plain

なので、 openstreetmap.org でベルリン付近を表示させて、ログイン後『iD』エディタで編集画面にして、ベルリン近辺の地物がない箇所を確認しておきます。

f:id:junichim:20200918151245p:plain

上記の赤い丸で囲んだあたりがよさそうです。

同じ場所を拡大してスマホの画面でも表示すると、

f:id:junichim:20200918150924p:plain

ちゃんと地面の色になってますね。

ise.map(apk同梱版)

次は、現在のアプリをリリースした際にapkファイルに含めていたファイルで試します。

ファイルを実機にコピーして、

C:\Users\mor\home\work\tmp\mapsforge>adb push ise_init_1.0beta4.map /sdcard/Android/data/org.mapsforge.samples.android/files/
ise_init_1.0beta4.map: 1 file pushed, 0 skipped. 58.3 MB/s (4728762 bytes in 0.077s)

C:\Users\mor\home\work\tmp\mapsforge>

伊勢市駅付近を表示させると

f:id:junichim:20200918151655p:plain

問題ないですね。

ise.map(定期更新版)

最後に、定期的に更新しているオフラインマップを試します。 上記と同様にmapファイルを実機にコピーして、伊勢市付近を表示させると

f:id:junichim:20200918151839p:plain

倍率は違いますが、明らかにこれがおかしいですね。

避難所検索@伊勢での動作確認

ソースコードは 1.0beta4 のものを使いました。 上記と同様に試していきます。

berlin.map

f:id:junichim:20200918151939p:plain

正しく表示されています。

ise.map(同梱版)

f:id:junichim:20200918152301p:plain

問題ないですね。

ise.map(定期更新版)

f:id:junichim:20200918152042p:plain

やはりこれが問題ですね。

動作状況

まとめると、こんな感じになりました。

mapsforge sample 避難所検索@伊勢
ライブラリ ver v0.14.0+ v0.11.0
berlin.map
ise.map (apk同梱版)
ise.map (定期更新版, 2020/8/24) × ×

ということは、オフラインマップの定期更新で問題が起きるようになったと考えるのが妥当ではないかと思います。

ただ、2点ほど腑に落ちないのが、

  • apk同梱版の ise.map も同じ手順で作成したものを使っている
  • 最新版のリリース前に、オフラインマップの更新に問題がないことを(少なくとも一度は)確認している

というところです。

となると、上記が記憶違いでない限り、なぜオフラインマップの作成環境自体の変更をしていないのに、ある時期から正しく表示されなくなったのか?という点が非常に不思議です。

いずれにしても、上記を踏まえて、さらなる原因調査を行いたいと思います。続きは、次の記事にまとめていきます。

openstreetmap-carto が用いるshapfileについて

OpenStreetMap のタイルサーバーのセットアップの際に、地図描画のスタイルとして openstreetmap-carto を利用します。

こちらの記事に書いたように、タイルサーバーを再セットアップした際に、 openstreetmap-carto が利用するシェープファイルデータのダウンロード先が変わってました。

どうして、いろいろと変更があったのかちょっと興味があったので、調べてみたことをメモっておきます。

利用するファイルの比較

セットアップ時点

最初にセットアップした際は、v4.6.0 に当たるコミット ( 2017/12/18 ac7d4fb )でした。

この時は、データソースとして

を用いていました。

2020/9/9 再セットアップ時点

再セットアップでは 2020/9/7 b10aef3 のコミットを用いており、ダウンロードするファイルは、 以下になっていました。

ダウンロードしているファイルの内容

それぞれのファイルの内容をサイトの説明とファイル名から推測すると

ファイル名 内容
simplified-land-polygons-complete-3857 大陸や土地のポリゴン, 低ズームレベル用の単純化版, 分割なし
land-polygons-split-3857 大陸や土地のポリゴン, 分割あり
simplified-water-polygons-split-3857 海のポリゴン, 低ズームレベル用の単純化版, 分割あり
water-polygons-split-3857 海のポリゴン, 分割あり
antarctica-icesheet-polygons-3857 南極のポリゴン
antarctica-icesheet-outlines-3857 南極のアウトライン
ne_110m_admin_0_boundary_lines_land 国境に関するデータ ?
world_boundaries-spherical ?

となっているようです。

背景描画方法の変更

その後、 2019/3/25 b5938d6 のコミットにて、描画方式が、

  • 海の背景上に大陸(土地部分)のポリゴンを描画

から

  • 土地の背景上に海のポリゴンを描画

に変更になっています。

その理由としては、 PR #3694

This will allow solving several issues related to mud flats, mangroves, wetlands and areas of parkland at the coast. It also makes it possible to use different colors for rivers and lakes, if desired. It also makes it easier to render maritime borders less prominently

とあります。意訳すると

干潟、マングローブ林、湿地、海岸の緑地などに関連する問題をいくつか解決しいます。また、川や湖に異なる色を使うことを可能にします。また、海岸線を目立たないように描画するのが簡単になります。

ということのようです。

ですが、残念ながらレンダリングの中身まで追いかけてないので、この説明がピンとこないですね(あと、議論全部が追えてないので、誤解があるかもしれませんがご容赦ください)。

ダウンロード先サーバーを切り替え

2019/3/31 080b8ea のコミットで、シェープファイルをダウンロードするサーバーを切り替えています。これは単に既存のサーバーの運用停止に伴うもののようです。

world-boundaries を不使用

さらに、2019/4/22 2bff971 のコミットで、world_boundaries-spherical.tgz を利用しなくなります(特に理由が見当たらないのでなぜ急に不要になったかは不明です)。

シェープファイルの内容をdbで管理

そのうえで、再セットアップの際にあったように、2019/4/27 6782400 のコミットで、シェープファイルの扱いが変わり、DB内のテーブルに格納するようになったみたいです。

シェープファイルセットアップ前のgisデータベースのテーブルとして、タイルサーバー再セットアップ時に調べた以前のテーブル構成を見てみると、

osm@map:~$ psql -d gis
psql (9.5.23)
Type "help" for help.

gis=> \d
               List of relations
 Schema |        Name        | Type  |  Owner   
--------+--------------------+-------+----------
 public | geography_columns  | view  | postgres
 public | geometry_columns   | view  | osm
 public | planet_osm_line    | table | osm
 public | planet_osm_nodes   | table | osm
 public | planet_osm_point   | table | osm
 public | planet_osm_polygon | table | osm
 public | planet_osm_rels    | table | osm
 public | planet_osm_roads   | table | osm
 public | planet_osm_ways    | table | osm
 public | raster_columns     | view  | postgres
 public | raster_overviews   | view  | postgres
 public | spatial_ref_sys    | table | osm
(12 rows)

gis=> 

ですが、シェープファイルセットアップ後は、

mor@map:/home/osm/src$ sudo -u postgres -i
[sudo] mor のパスワード: 
postgres@map:~$ 
postgres@map:~$ psql gis
psql (9.5.23)
Type "help" for help.

gis=# \d
                        List of relations
 Schema |                Name                 | Type  |  Owner   
--------+-------------------------------------+-------+----------
 public | external_data                       | table | osm
 public | geography_columns                   | view  | postgres
 public | geometry_columns                    | view  | osm
 public | icesheet_outlines                   | table | osm
 public | icesheet_polygons                   | table | osm
 public | ne_110m_admin_0_boundary_lines_land | table | osm
 public | planet_osm_line                     | table | osm
 public | planet_osm_nodes                    | table | osm
 public | planet_osm_point                    | table | osm
 public | planet_osm_polygon                  | table | osm
 public | planet_osm_rels                     | table | osm
 public | planet_osm_roads                    | table | osm
 public | planet_osm_ways                     | table | osm
 public | raster_columns                      | view  | postgres
 public | raster_overviews                    | view  | postgres
 public | simplified_water_polygons           | table | osm
 public | spatial_ref_sys                     | table | osm
 public | water_polygons                      | table | osm
(18 rows)

gis=# 

となりました。確かに、シェープファイル名に対応するテーブルが追加されているのがわかります。

このように変更した理由は特に明記されていませんが、コミット時のコメントを見ると、

  • 運用中でもデータの更新ができる
  • ダウンロードするデータの日付けを見てダウンロードする

といったことが書いてあるので、データ更新を容易にするための変更ではないかと推測しています。

雑感

地図データの描画なんて、安定していて、データの更新以外はそうそう変更があるように思ってなかったんですが、意外といろいろと変わっていくものなんですね。

海岸線は、オフラインマップの生成でもかかわるので、機会を見つけていろいろと調べようと思います。

【Android】 NFC を試しました

以前から興味はあったのですが、ようやく仕事で使う機会に恵まれそうなので、 NFC を試してみました。

既にネットにいろいろと情報がありますが、自分なりに気づいた点をメモしておきます。

なお、検証は Nexus 6P, Android 8.1.0 で行いました。

NFC 全般について

最初はネットの記事などをいろいろと呼んでいたのですが、今一つ腑に落ちません。ということで、ちょっと昔の本になりますが、一冊 Amazon で買いました。

NFC Hacks ―プロが教えるテクニック & ツール

NFC Hacks ―プロが教えるテクニック & ツール

私の場合、やっぱり、何か新しいものを理解しようとするときは、本で概要をつかむほうが手っ取り早いです。 NFC Forum Tag の仕様なども載っていて、NFC 触るなら手元に一冊あってもいい感じだと思います。

タグの購入

NFCのテスト用に Amazon で買いました。

NXP Ntag 213 チップ, Type-2 タグ。シールタイプ。

にしても、手軽に買えるようになりましたね。

読み込みサンプル

大きく分けて、NFC の読み込みを行う際は、

  • スマホで NFC タグにタッチした際に、アプリを起動させる
  • アプリを起動している際に、 NFC にタッチして読み込む

の2パターンがあると思います。

前者を実現するには、 AndroidManifest にインテントフィルタ(暗黙的インテント)を定義してやります。後者に対応するには、アプリ内で NfcAdapter#enableForegroundDispatch を呼び出す必要があります(NfcAdapter#enableReaderMode でもできると思いますが、今回は試していません)。

個人的には後者でやるほうが、動きがわかりやすいように思います。

なお、今回作成した NFC の読み取りと(この記事では別段書き込みに触れてるところはないですが)書き込みサンプルをそれぞれ Gist に上げておきます。何かの参考にしてください。

NFC 読み取りサンプル · GitHub

NFC 書き込みサンプル · GitHub

インテントの指定方法

Android のドキュメントには、NFC を読み込んだ際、3種類のインテントが発行されるとあります。この際、ACTION_TAG_DISCOVERED 以外のインテントにはインテント名だけでなく、インテントフィルタの定義に追加情報が必要です。

インテント 追加情報
ACTION_NDEF_DISCOVERED mimetype
ACTION_TECH_DISCOVERED tech list
ACTION_TAG_DISCOVERED

追加情報がない場合、インテントフィルタを定義していてもその、正しくインテントの呼び出しがおこなわれませんでした(追加情報なしだからといって、ワイルドカード的にはならないようです)。以下に、具体例を示しておきます。

(参考) ACTION_TECH_DISCOVERED に tech list が必要という話

Android NFC Intent-filter for all type - Stack Overflow

ACTION_NDEF_DISCOVERED の例

NDEF に text/plain の情報が書き込まれているNFCタグを読み取る場合を想定します。

AndroidManifest での定義

例えば、AndroidManifest にインテントフィルタを定義して、スマホでNFCタグにタッチした際に、アプリを起動させてみると、

指定した mimeType アプリ起動
text/plain
image/jpeg ×
なし ×

となります。

mimeType 指定が text/plain を指定

        <activity
            android:name=".MainActivity"
            android:launchMode="singleTask">

            <intent-filter>
                <action android:name="android.nfc.action.NDEF_DISCOVERED" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
            </intent-filter>

mimeType 指定なし

        <activity
            android:name=".MainActivity"
            android:launchMode="singleTask">

            <intent-filter>
                <action android:name="android.nfc.action.NDEF_DISCOVERED" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
NfcAdapter#enableForegroundDispatch による指定

今度は、NfcAdapter#enableForegroundDispatch を呼び出し、アプリを起動中に、NFCタグにタッチして正しく読み込めるか試すと、

mimeType 指定 読み込み(画面表示)
text/plain
image/jpeg ×
なし ×

となりました。

mimeType 指定が text/plain の場合

    @Override
    protected void onResume() {
        super.onResume();
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()), 0);
        IntentFilter ndefFilter = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);

        try {
            ndefFilter.addDataType("text/plain");
        } catch (IntentFilter.MalformedMimeTypeException e) {
            e.printStackTrace();
        }

        IntentFilter[] intentFilters = new IntentFilter[] {ndefFilter};
        mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null);
    }

mimeType 指定がなし

    @Override
    protected void onResume() {
        super.onResume();
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()), 0);
        IntentFilter ndefFilter = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);

        IntentFilter[] intentFilters = new IntentFilter[] {ndefFilter};
        mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null);
    }

ACTION_TECH_DISCOVERED の例

次に、購入直後のNFCタグ(一度も書き込みを行っていないタグ, Type-2タグ)を読み取る場合を想定します。

AndroidManifest での定義

上記と同じく、AndroidManifest にインテントフィルタを定義して、スマホでNFCタグにタッチした際に、アプリを起動させてみると、

tech list 指定 アプリ起動
tech list あり
なし ×

となりました(下記に示すように tech list として xml/nfc_tect_list.xml を作成して、その中で Ndef を指定しています)。

AndroidManifest.xml

            <intent-filter>
                <action android:name="android.nfc.action.TECH_DISCOVERED" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <meta-data
                android:name="android.nfc.action.TECH_DISCOVERED"
                android:resource="@xml/nfc_tech_filter" />
 

xml/nfc_tect_list.xml の中身

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.Ndef</tech>
    </tech-list>
</resources>
NfcAdapter#enableForegroundDispatch による指定

例えば、NfcAdapter#enableForegroundDispatch を呼び出し、アプリを起動中に、NFCタグにタッチして正しく読み込めるか試すと、

tech list 指定 アプリ起動
tech list あり
なし ×

となります。

tech list がある場合の NfcAdapter#enableForegroundDispatch 呼び出し

    @Override
    protected void onResume() {
        super.onResume();
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()), 0);
        IntentFilter techFilter = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);

        IntentFilter[] intentFilters = new IntentFilter[] {techFilter};

        String tech = Ndef.class.getName();
        String techlist[] = new String[]{tech};           // and 条件
        String techLists[][] = new String[][] {techlist}; // or 条件

        // TECH_DISCOVERED, tech list 必須
        mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, techLists);
    }

tech list がない場合の NfcAdapter#enableForegroundDispatch 呼び出し

    @Override
    protected void onResume() {
        super.onResume();
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()), 0);
        IntentFilter techFilter = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);

        IntentFilter[] intentFilters = new IntentFilter[] {techFilter};

        // TECH_DISCOVERED, tech list 必須
        mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null);
    }

ACTION_NDEF_DISCOVERED の例外

ドキュメントによると NfcAdapter#enableForegroundDispatch のインテントフィルタと tech list 引数の両方に null を渡した場合、 ACTION_TAG_DISCOVERED を取得できるとあります。

        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()), 0);

        // すべてを受け付ける
        mNfcAdapter.enableForegroundDispatch(this, pendingIntent, null, null);

でも、実はこの時、 ACTION_TAG_DISCOVERED だけでなく、 ACTION_NDEF_DISCOVERED にも反応していました。この時は、上記と異なり、mimeType の指定がなくてもです。

AndroidManifest でのインテントフィルタと NfcAdapter#enableForegroundDispatch のインテントフィルタは同じように指定できると思っていたのですが、この部分については両者で異なるようです。

なお、 NfcAdapter#enableForegroundDispatch を呼び出す際に、 ACTION_TAG_DISCOVERED をインテントフィルタで明示的に指定しても、取得することができました。

ACTION_TAG_DISCOVERED に反応するアプリについて

なお、今回実機でいろいろと試していたら、下記の画面が表示されることがありました。

f:id:junichim:20200914184257p:plain

これは、購入してまだ書き込みを行っていない NFC タグを読み取ったときに出てきました。

どうもこれは、 ACTION_TECH_DISCOVERED および ACTION_TAG_DISCOVERED に対応するアプリがインストールされていないときに、おそらくシステムが用意しているアプリが反応したのだと思われます。似たような話が下記にありました。

「収集された新しいタグ」プロンプトが頻繁に表示されます | HUAWEI サポート 公式サイト

また、自分で作成したNFC読み取りサンプルが ACTION_TECH_DISCOVERED または ACTION_TAG_DISCOVERED に対応させたときは、このアプリの表示もなくなったので、ほぼあたっているかと思います。特に問題があるわけではありませんが、こういうものがありました、というメモです。

NDEFメッセージについて

Gist の処理を見るとわかりますが、 ACTION_NDEF_DISCOVERED の場合に、 NdefMessage を取得しようとすると、配列の形式で取得します。 でも、 NFC (NDEF) の仕様を眺めていても、Ndefメッセージは一つだけのようです。これはなぜなんでしょうか?

調べてみると、やはり、基本的には1つのNFCタグには1つのNDEFメッセージのようです(参考までに、1つのNDEFメッセージには複数のNDEFレコードを入れることができるようです)。

1つのNFCタグに2つのNDEFメッセージ/レコード - Android

Android のドキュメントにも通常一つのNDEFメッセージとあります。でも、将来の拡張性のため配列にして扱ってる、と書いてます。このため、 NdefMessage を取得する時、配列で戻ってきているようです。

NfcAdapter  |  Android デベロッパー  |  Android Developers

これで、だいたい感触がつかめたので、引き続きいろいろと試そうと思います。