プログラマーのメモ書き

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

OpenStreetMap で地図を編集してみよう!

はじめに

先日のOSC名古屋2017 にて、 OpenStreetMap のセッションを聞いてきました。 で、このセッション中に、地図が自由に編集できますよ、という話がありました。 自由にというとちょっと語弊がありますね。OpenStreetMap は

OpenStreetMap(OSM)は、道路地図などの地理情報データを誰でも利用できるよう、フリーの地理情報データを作成することを目的としたプロジェクトです。誰でも自由に参加して、誰でも自由に編集でき、誰でも自由に利用する事が出来ます。

OpenStreetMap 日本語サイト より引用

というもので、自分が知っている地域の情報が地図に反映されていない場合は、それを反映することができますよ、といったほうがいいかと思います。

ということで、 OpenStreetMap の地図を編集した話をメモしておきたいと思います。

ちなみに、OpenStreetMap なんて知らねーぜ、という方も多いかと思いますが、実は、Facebookの場所の地図表示はこの OpenStreetMap を使っています(地図のどこか下のほうかな?にクレジットがあります)。なので、知らず知らずのうちに使っていることも多いかもですよ。

アカウントの取得

地図を編集するなら、まずアカウントを取得する必要があります。 OpenStreetMapのページを開いて、『地図を編集』を押します。 アカウントを持っていなければ、『今すぐ登録』で登録します。 登録には、有効なメールアドレスとパスワード、表示名を入力します。

登録すると確認メールが来るので、問題なければメール中のリンクをクリックすればOKです。

チュートリアル

ログインすると、下記の様な画面が表示されます。

f:id:junichim:20170612165642p:plain

初めてなので、チュートリアルをやってみました。 チュートリアルでは、場所(ポイント)の指定方法、ライン(道路など)の指定方法、エリア(建物など)の指定方法、を教えてくれます。 いずれも、直感的な操作なので、すぐにわかる事かと思います。

ま、チュートリアルの詳細は紹介するよりも、実際にやってみてもらうほうがよくわかると思いますので、割愛します。

ただ、残念ながら、ポイントの説明までは日本語なのですが、エリアとラインの説明が英語でした。そんなに難しくないので、ポイントをやってみれば、エリアとラインのチュートリアルもなんなくこなせるかと思います。

地図を編集

さて、実際に地図を編集してみましょう。 OpenStreetMapにログインした状態だと、画面上部の『編集』ボタンをクリックすることでそのまま編集ができます。

f:id:junichim:20170612171150p:plain

編集画面では、上空からの画像(衛星写真?航空写真?)の上に、登録済みの地物(先ほどのポイント、ライン、エリア)が重ねて表示されています。

伊勢近辺の方ならわかりますが、上記の画像でも ダイムスタジアム伊勢(以前は、倉田山公園野球場)や最古の厄除観音で知られる松尾観音寺といった、超メジャーな場所ですらまだ登録されていません。 最初に登録すれば、登録者としての名前が一生(どころかOpeNStreetMapがある限りずっと)残るので、これは登録しない手はありません。

ということで、私が登録してしまいます。 登録すると、こんな感じです。

f:id:junichim:20170612173100p:plain

上空からの写真だと、伊勢市の消防本部・防災センターが建設中のようで、敷地がよくわからかったので、ポイントとして登録しています。 この状態だと、まだ正式に登録されていません。画面上部の『保存』ボタンを押すと、

f:id:junichim:20170612173620p:plain

のように、登録時の情報入力画面が表示されますので、適切なコメントを記入してから、『アップロード』を押します。 問題がなければ、データがアップロードされます。アップロード完了後、わりとすぐに反映されます(審査とかないので)。

f:id:junichim:20170612173959p:plain

これ、編集しだすと、『あ、ここの神社がない。よし、登録してしまえ。』とか『ここの川岸の形が違ってる。よし、なおしてやれ。』とか、簡単に編集できるので、結構くせになりそうです。

最後に

今回地図を編集してみようかな、と思い立ったきっかけは、OSM名古屋2017で聞いたのもあるのですが、たまたま自分の実家近辺を見たときに、近所の目印となるようなものが全くなかったためです。まあ、田舎だから仕方ないのですが、せっかくその場所に関する情報をもっていて、OpenStreetMapというものも知っているので、登録しない手はない、と思い立った次第です。

あと、普段は、OpenStreetMap を使ってばかりですが、一度昔々に地図作成ってどうなんだろうか?と思って、OpenStreetMap系の話を聞いたときは、(その時の話の主旨をよくわかってなかったのもあるかもしれませんが)GPSロガーが必要だったような印象を受けて、これは敷居高いな、と思った印象がありました。

でも、今回試して上空からの写真を元に、気軽に地図を編集できるので、これはやらない手はない、という印象でした。 登録したものが間違っていたらどうしよう、とかは気にせず、間違っていたら誰かが直してくれるはずなので、気にせずいきましょう。

少しでも、興味があれば、ぜひ試してみることをお勧めします。

ちなみに、上記の地図編集は、iDというエディタをブラウザ上で使った場合の地図編集となるようです。OpenStreetMapの地図編集は、これも上記の方法一通りというのではなく、JOSMという上級者向け(?)のツールなどもあるようです(こっちはまだ試せてません)。

地図編集するほどではない方に

あと、今回初めて知ったのですが、 OpenStreetMap の画面から、修正要望のようなメモを残す機能があります。

f:id:junichim:20170612174843p:plain

画面右端の列の吹き出しようなアイコンをクリックすると、

f:id:junichim:20170612232851p:plain

のようなメモ編集蘭が表示されます。

これは、自分で修正するほどではないけど、『こうしてほしい』、とか、『ここ間違ってるよ』いったような要望を伝えることができる機能のようです。 便利なのが、アカウントがなくてもメモを残すことができます。もちろん、アカウントでのログインがあれば、その人に紐づいたメモになります。

気になることがあるけど、地図を編集するほどではない、とか、アカウント作るのなんかいやだ、という場合、こういう機能を利用するのもありかと思います。

AWS CLI 複数の認証情報の設定について

以前、EC2のインスタンスを t1.micro から t2.nano へ移行する作業について書きました。

blog.mori-soft.com

この中で、S3 へバックアップを行うという設定を行っていたのですが、月一動作だったので、改めて動作確認をしてみると、どうもうまく動いていません。 こんな感じのエラーです。

2017-06-01 03:05:01 JST: aws s3 cp バックアップファイル名 s3://バケット名/
upload failed: バックアップファイル名 to s3://バケット名/ファイル名 An error occurred (AccessDenied) when calling the CreateMultipartUpload operation: Access Denied

アクセス拒否のようです。

aws cli はいろんな方法で認証情報を指定できるので、これが間違っているのだろうと予想されます。 いろいろと調べてみると、 aws configure list で現在の認証情報の設定状況を確認できることがわかりました。

dev.classmethod.jp

早速、試してみると

bitnami@ip-172-30-0-73:~/.aws$ aws configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ****************AAAA shared-credentials-file    
secret_key     ****************aaaa shared-credentials-file    
    region                <not set>             None    None
bitnami@ip-172-30-0-73:~/.aws$ 

あれ?なんかおかしいぞ。

実は、aws cli を使う際、EC2 用のユーザーと S3 用のユーザーを分けていました。 で、上記のアクセスキーなどは、EC2ユーザーのものです。 おまけに、 region も設定されていない。

ということで、認証情報の設定方法を見直しました。

間違っていた点

恥ずかしならが、調べた結果、いろいろと間違っていました。まず現在設定していた方法は、

  • EC2 用のユーザーは aws configure コマンドで設定する
    • .aws/config と .aws/credentials に内容が書かれている
  • S3 用のユーザーは .aws/config.s3 ファイルにすべての内容を記述する
  • S3 用のユーザーを使う場合は、 環境変数で AWS_DEFAULT_CONFIG=.aws/config.s3 を指定する

というものでした。

で、まず大きく間違っていたのが、下記の説明にある、

docs.aws.amazon.com

この部分、

Storing Credentials in Config
The AWS CLI will also read credentials from the config file. If you want to keep all of your profile settings in a single file, you can. If there are ever credentials in both locations for a profile (say you used aws configure to update the profile's keys), the keys in the credentials file will take precedence.

(頑張って意訳)

Config ファイルへの認証情報の保存について
AWS CLI は config ファイルから認証情報を読むこともできます。プロファイル設定のすべてを1ファイルに保存することも可能です。もし、プロファイルの情報が複数個所にある場合は、credentialsファイルのキーが優先されます。

です。

この前半部分を読んで、configファイルに認証情報全部を書けると思い込んでしまいました。でも、上記の意訳からもわかるように、書けることはかけるんですが、(同じプロファイル)に対する認証情報が複数ある場合、credentialsファイルのものが優先的に使われるそうです。 やられました。

更なる間違い

上記の問題が大きいので、些細なことですが、さらに、ついでに間違っていたのが、config.s3 ファイルの書き方です。 config.s3 ファイルを、

bitnami@ip-172-30-0-73:~/.aws$ cat config.s3
[s3]
aws_access_key_id = アクセスキー
aws_secret_access_key = シークレットアクセスキー
region = ap-northeast-1
bitnami@ip-172-30-0-73:~/.aws$ 

と書いており、いつつけたか覚えのない s3 というプロファイル名がついていました。 これは、プロファイル名になるはずなので、切り替えたければ、 --profile s3 のようにコマンド実行時にプロファイル名も指定する必要がありました。

仮にここを [default] に修正すると、

bitnami@ip-172-30-0-73:~/.aws$ aws configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ****************AAAA shared-credentials-file    
secret_key     ****************aaaa shared-credentials-file    
    region           ap-northeast-1      config-file    /home/bitnami/.aws/config.s3
bitnami@ip-172-30-0-73:~/.aws$ 

のように、regionは正しく認識されました(もっとも、上記の二重に認証情報がある場合の問題が残っているので、これでも使えませんがね)。

最終的な設定内容

で、結局、認証情報を使い分けるために、プロファイルを追加することとしました。ここで、プロファイル名は s3user としています。 設定後のファイルはこんな感じになります。

~/.aws/credentials

[default]
aws_access_key_id = EC2アクセス用のアクセスキー
aws_secret_access_key = EC2アクセス用のシークレットアクセスキー

[s3user]
aws_access_key_id = S3アクセス用のアクセスキー
aws_secret_access_key = S3アクセス用のシークレットアクセスキー

~/.aws/config

[default]
region = ap-northeast-1

[profile s3user]
region = ap-northeast-1

--profile s3user をつけてコマンドを実行すると、S3 用ユーザーの認証情報が使われています。

bitnami@ip-172-30-0-73:~/.aws$ aws configure list --profile s3user
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                   s3user           manual    --profile
access_key     ****************BBBB shared-credentials-file    
secret_key     ****************bbbb shared-credentials-file    
    region           ap-northeast-1      config-file    ~/.aws/config
bitnami@ip-172-30-0-73:~/.aws$ 

念のために、 --profile なしを試すと、ちゃんと EC2 用ユーザーの認証情報が使われます。

bitnami@ip-172-30-0-73:~/.aws$ aws configure list                                                                                                                     
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ****************AAAA shared-credentials-file    
secret_key     ****************aaaa shared-credentials-file    
    region           ap-northeast-1      config-file    ~/.aws/config
bitnami@ip-172-30-0-73:~/.aws$ 

この認証情報を使うようにバックアップスクリプトを修正したら、問題なく動作しました。 めでたし、めでたし。

mapsforge でポップアップするマーカーを試す

下記の記事で、 Android でのオフライン地図表示を試しましたが、

blog.mori-soft.com

本格的に進めるために、まずは地図表示ライブラリの mapsforge をもう少し詳しく使ってみたいと思います。

簡単なサンプル

まず最初に、簡単なサンプルとして、下記のリンク先の説明に従って、地図を表示してみます。

https://github.com/mapsforge/mapsforge/blob/master/docs/Getting-Started-Android-App.md

英語ですが、書いてある通りにすればよいので、そんなに難しくはありません。

地図データのダウンロード

地図データについては、ベルリンの地図を表示してもピンとこないので、日本の地図をダウンロードしておきます。 ちなみに、今回は multilingual 以下にある地図を使いました。

今回のアプリは実機(Nexus5, Android 6.0)で試そうと思います。なので、実機にファイルを転送しておきます。

C:\> adb push japan-multi.map /sdcard/Download/

ファイルの保存先(今回は /sdcard/Download/ )は、実装する予定のコードに合わせます。

依存関係の記述

開発環境として Android Studio を使っているので、ライブラリファイルの依存関係を下記 Integration Guide を参考に

https://github.com/mapsforge/mapsforge/blob/master/docs/Integration.md

build.gradle に書いておきます。 基本的に、 Core と Android のセクションのものを書いておけばいいはずです。下記のようになりました。

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.2.0'

    compile 'org.mapsforge:mapsforge-core:0.8.0'
    compile 'org.mapsforge:mapsforge-map:0.8.0'
    compile 'org.mapsforge:mapsforge-map-reader:0.8.0'
    compile 'org.mapsforge:mapsforge-themes:0.8.0'
    compile 'net.sf.kxml:kxml2:2.3.0'

    compile 'org.mapsforge:mapsforge-map-android:0.8.0'
    compile 'com.caverock:androidsvg:1.2.2-beta-1'
}

コードの記述

で、リンク先の指示に従ってサンプルコードを書くのですが、今回試した実機(Nexus5)がたまたま Android 6.0 だったため、外部ストレージ領域へのアクセスに手間取りました。 というのも、API レベル 23 以上がターゲットの場合、パーミッションをインストール時に許可するのではなく、実行時に許可するようになってました。

developer.android.com

ここだけ気をつければ、あとは大きな問題なく動作するはずです。 動作時のイメージはこんな感じです。

f:id:junichim:20170530172659p:plain

なお、参考までにサンプルコードGitHubにあげておきます。

https://github.com/samples-of-junichim/MapsforgeSample.git

このリポジトリのタグ simple になります。

サンプルプログラムの動作

さて、次はマーカーを表示してみたいと思います。

ただ、参考になるものが、GitHub の mapsforge の説明にはみあたらないので、一度 mapsforge のリポジトリを clone して、この中に含まれている mapsforge-samples-android を動かしてみます。

androidのサンプルプログラムは、リポジトリを clone 後、 Android Studio にインポートすればOKです。 なお、インポートの際は、 mapsforge-samples-android ではなく、リポジトリのトップディレクトリを指すようにしてください(mapsforge-samples-android を指してもうまく認識されません)。

問題がなくプロジェクトが立ち上がったら、実機を接続して実行してみてください。なお、このサンプルの実行には、germany.map の地図ファイルが必要になりますので、別途ダウンロードして、実機の /sdcard にコピーしておきます。

このサンプルプログラムを動作させるといろいろなデモを見ることができます。

マーカーの表示には、このうち、OverlayMapViewer.java を参考にしました。

f:id:junichim:20170530173634p:plain

実行するとこんな感じの表示になります。

f:id:junichim:20170530173800p:plain

なお、このサンプルでは、いろんなものを地図上に表示する方法を示しています。円や折れ線、画像(いわゆるマーカー)、画像で領域を埋める、などです。参考になりますね。

マーカーの表示

さて、上記のソースコードをざっと読んで、実際に試してみましょう。上記のソースコードから、マーカーを表示するためには、 Marker というクラスをインスタンス化して、MapView に追加すれば良さそうです。

mapView.getLayerManager().getLayers().add(marker);

Marker をインスタンス化する際には、表示したい画像を Drawable から、 Bitmap(org.mapsforge.core.graphics.Bitmap)に変換してやる必要があります。 このあたりの処理も、先ほどのサンプルコードに載っていて、

        Drawable drawable = getResources().getDrawable(resource);
        Bitmap bitmap = AndroidGraphicFactory.convertToBitmap(drawable);

とすればOKです。 なお、AndroidGraphicFactory.convertToBitmap メソッドは、android.graphics.Bitmap を内部に保持する AndroidBitmap クラス(org.mapsforge.core.graphics.Bitmap インターフェースを実装)を返しています。

今回作成したサンプルの動作としては、画面上をロングタップすると、その位置にマーカーを設置する、というものです。 最初のサンプルでも出てきた、TileRendererlayer の onLongPress をオーバーライドして、そこからマーカーを設置するメソッドを呼び出しています。

        TileRendererLayer trl = new TileRendererLayer(tileCache, mds, mapView.getModel().mapViewPosition, AndroidGraphicFactory.INSTANCE) {
            @Override
            public boolean onLongPress(LatLong tapLatLong, Point layerXY, Point tapXY) {
                return setMarker(tapLatLong);
            }
        };

動作イメージは下記のようになります。

f:id:junichim:20170530175048p:plain

この部分のソースコードは、リポジトリ

https://github.com/samples-of-junichim/MapsforgeSample.git

の marker タグを見てください。

ポップアップを追加

次は、マーカーをタップすると、ポップアップを表示するという処理を試したいと思います。

これって、地図系のアプリでは当たり前のように使われてるとおもうんだけど、ライブラリとかだとたまにサポートされていなかったりします。 で、mapsforge はどうかというと、ああ、残念、組み込み済みの便利な実装はありませんでした。 というわけで、自分で実装します。

ポップアップの挙動もいろんな方法があると思うのですが、今回は、次のように動作するポップアップを作成します。

  • マーカーをタップするとポップアップが表示される
  • ポップアップが表示中の場合は、再度同じマーカーがタップされるとポップアップを閉じる
  • マーカーのない場所をタップするとポップアップが閉じる
  • ポップアップ表示中に、別のマーカーをタップすると後者のマーカーのポップアップのみを表示する(つまり、ポップアップの表示をマップ全体で最大一つのマーカーだけに限定する)

さて、実装に先立ち、ヒントになるものを、 mapsforge-samples-android から探すと、BubbleOverlay.java と ViewOverlayViewer.java が参考になりそうです。

BubbleOverlay.java を見ると、マップ上に吹き出しが表示されています。この吹き出しは、いったんTextViewを作成して、それをビットアップに変換して、Makerにして表示するというものです。

ViewOverlayViewer.java でやってるのは、View(Button)を作って、これをMapView(ViewGroupの派生)に追加して表示させるというものです。

今回は、BubbleOverlay の方法を参考にしました。 やり方はいたってシンプルで、最初に地図上をロングタップして Marker を表示するところは同じです。で、その Marker のタップイベントのリスナーにおいて、ポップアップを表示する処理を行います。

具体的には、 Marker を extends したクラスを作り onTap にて

    @Override
    public boolean onTap(LatLong geoPoint, Point viewPosition, Point tapPoint) {

        Log.d(TAG, "LoaLong: " + geoPoint.getLatitude() + ", " + geoPoint.getLongitude());
        Log.d(TAG, "viewPos: " + viewPosition.x + ", " + viewPosition.y);
        Log.d(TAG, "tapPos : " + tapPoint.x + ", " + tapPoint.y);

        if (contains(viewPosition, tapPoint)) {
            Log.d(TAG, "contains: " + true);

            if (! TextUtils.isEmpty(mText)) {
                Log.d(TAG, "text is exist");

                if (null == mBalloonMarker) {
                    Log.d(TAG, "balloon is null");

                    Bitmap bmp = createBalloon(mMapView.getContext());
                    mBalloonMarker = new Marker(MarkerWithBubble.this.getLatLong(), bmp, 0, - bmp.getHeight() / 2 - BALLOON_VERTICAL_OFFSET );
                    mMapView.getLayerManager().getLayers().add(mBalloonMarker);
                } else {

                    if (null != mBalloonMarker) {
                        mBalloonMarker.setVisible(!mBalloonMarker.isVisible());
                    }
                }
            }
            Log.d(TAG, "text is null");
        } else {
            Log.d(TAG, "contains: " + false);

            if (null != mBalloonMarker) {
                mBalloonMarker.setVisible(false);
            }
        }
        return super.onTap(geoPoint, viewPosition, tapPoint);
    }

のようにします。

今回は、ポップアップの表示をマップ全体で最大一つのマーカーだけに限定したいというのがあったため、タップイベントが一つのMakerで終わらないように、ポップアップの処理を行っても、戻り値として true を返していません。 ここが個人的には、ちょっと気になるところです。もっといい実装方法はないかなー。

さて、上記関数で呼び出している createBalloon は

    private Bitmap createBalloon(Context c) {
        TextView tv = (TextView) LayoutInflater.from(mMapView.getContext()).inflate(R.layout.popup_marker, null);
        tv.setTextColor(mColor);
        tv.setTextSize(mTextSize);
        tv.setText(mText);

        return Utils.viewToBitmap(c, tv);
    }
public class Utils {

    public static Bitmap viewToBitmap(Context c, View view) {
        view.measure(View.MeasureSpec.getSize(view.getMeasuredWidth()),
                View.MeasureSpec.getSize(view.getMeasuredHeight()));
        view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
        view.setDrawingCacheEnabled(true);
        Drawable drawable = new BitmapDrawable(c.getResources(),
                android.graphics.Bitmap.createBitmap(view.getDrawingCache()));
        view.setDrawingCacheEnabled(false);
        return AndroidGraphicFactory.convertToBitmap(drawable);
    }

}

のように、さきほど挙げた BalloonOverlay.java で使っていた処理を参考にTextViewをBitmapに変換しています。

実際に動かしてみると、

f:id:junichim:20170530215347p:plain

のようになります。

参考

このポップアップするマーカーですが、最初は、 ViewOverlayViewer.java を参考に、バルーンを背景に持つTextViewをMapViewに追加して実装しようと考えました。 ポップアップの表示や複数マーカーに対する切り替えはうまくいったのですが、表示位置をずらすことがうまくできなくてこちらの方法ではあきらめました。

MapView に View を追加する際に、 MapView.LayoutParams でレイアウトパラメータを指定しています。このクラスは ViewGroup.LayoutParams の派生クラスで、オフセットを与えることができないようです。 さらに、 MapView#onLayout では子 View が MapView.LayoutParams を持っているのを前提に処理を行っているようなので、あきらめました。

リポジトリの 62f3131 あたりにこの方法で試した痕跡があるので、気になる方はぜひチャレンジしてみてください。

課題

一応、これで mapsforge でマーカーの表示とタップによるポップアップ表示ができるようになりました。 今回のサンプルでは、ロングタップでマーカーを設定する、という組み方をしたため、マーカーを設定した場所の情報がなく、そのため、画面回転で再描画が行われるとマーカーが消えてしまうという問題があります。 実際の利用時は、この辺りはリスト等で管理してあげないといけなさそうです。