Androidで道案内アプリケーションを作る

AndroidアプリでGPSを扱い始めると誰しも一度はGoogleMapに代表されるような地図アプリケーションを作りたいと思うのではないかと思います。
しかしGoogleが提供するGoogle Maps API（無償版）には利用目的やAPI呼び出し回数の制限があります。
今回は、Google Maps APIを使用せずにAndroid上で動作する地図アプリケーションを作成します。

# 動作検証環境
Aquos R3
API Level 26

#地図データの取得
地図データには[OpenStreetMap](https://openstreetmap.jp/)というオープンソースの地図プロジェクトのものを使用します。
OpenStreetMapの地図データをAndroidで使用するために、osmdroidというライブラリを使用します。

まずプロジェクトを作成したら、ルート直下のbuild.gradleに以下の一文を追加します。

```build.gradle
repositories {
    jcenter()
    mavenCentral() //追記
}
```

次に、appフォルダ配下のbuild.gradleに以下の一文を追加します。

```build.gradle
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'org.osmdroid.osmdroid-android:6.0.3' //追記
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}
```

以下のようにlayoutファイルに追記することでosmdroidのMapViewを追加できます。

```activity_main.xml
<org.osmdroid.views.MapView
        android:id="@+id/mapView"
        android:layout_width="wrap_content"
        android:layout_height="300dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
```

#GPSの利用
GPSの取得には**FusedLocationProviderClient**を使用します。
そのためにまず、build.gradleに以下の一文を追加してGoogle Play ServicesのLocation APIを使用できるようにします。

```build.gradle
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'org.osmdroid.osmdroid-android:6.0.3'
    implementation 'com.google.android.gms:play-services-location:17.0.0' //追記
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}
```

また、**AndroidManifest.xml**の\<application>要素以下に\<meta-data>を追記します。

```
    <application
        ......
        ......
        ...... >
        <meta-data                                                      //ここから
            android:name="com.google.android.gms.version"               //
            android:value="@integer/google_play_services_version" />    //ここまで追記
    </application>
```

これで**FusedLocationProviderClient**が使えるようになりました。
今回は、Locationは複数のアクティビティで取り扱えるように[汎用的なクラスを作成](https://qiita.com/maebaru/items/ba821938e66498d6ae76)して任意のクラスから呼び出します。
新たにクラスを作成し、**LocationCallback**を継承します。

```MyLocationManager.java
public class MyLocationManager extends LocationCallback {
}
```

他のクラスではonLocationResultListenerで取得したLocationを返します。

```MyLocationManager.java
public interface OnLocationResultListener {
    void onLocationResult(LocationResult locationResult);
}
```

コンストラクタで呼び出し元のクラスの情報を取得します。

```MyLocationManager.java
private Context context;
private FusedLocationProviderClient fusedLocationProviderClient;
private OnLocationResultListener mListener;

public MyLocationManager(Context context, OnLocationResultListener mListener) {
    this.context = context;
    this.mListener = mListener;
    this.fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context);
}
```

```MyLocationManager.java
/* 位置情報が更新されたら位置情報を返す */
    @Override
    public void onLocationResult(LocationResult locationResult) {
        super.onLocationResult(locationResult);
        mListener.onLocationResult(locationResult);

        if(locationResult == null) {
            Log.e("Error", "# No location data.");
            return;
        }
    }
```

位置情報の取得開始と停止を制御できるようにします。
また、位置情報サービスが無効化されている場合に有効化を促します。

```MyLocationManager.java
/* 位置情報の更新を開始する */
    public void startLocationUpdates() {
        //端末の位置情報サービスが無効になっている時、有効化を促す
        if(!isGPSEnabled()) {
            showLocationSettingDialog();
            return;
        }

        LocationRequest request = new LocationRequest();
        request.setInterval(5000);
        request.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

        fusedLocationProviderClient.requestLocationUpdates(request, this, null);
    }

    /* 位置情報の更新を停止する */
    public void stopLocationUpdates() {
        fusedLocationProviderClient.removeLocationUpdates(this);
    }

    /* 端末のGPS利用可否を返す */
    private Boolean isGPSEnabled() {
        android.location.LocationManager locationManager = (android.location.LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
        return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
    }

    /* 位置情報サービスの有効化を促す */
    private void showLocationSettingDialog() {
        new android.app.AlertDialog.Builder(context)
                .setMessage("設定画面で位置情報サービスを有効にしてください")
                .setPositiveButton("設定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
                        context.startActivity(intent);
                    }
                })
                .setNegativeButton("キャンセル", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        //何もしない
                    }
                })
                .create()
                .show();
    }
```

ここまでで作成した**MyLocationManager.java**のOnLocationListenerをベースになるActivityに実装します。

```BaseActivity.java
public abstract class BaseActivity extends FragmentActivity implements MyLocationManager.OnLocationResultListener {
    private MyLocationManager locationManager;

    @Override
    public void onStart() {
        super.onStart();

        locationManager = new MyLocationManager(getBaseContext(), this);
        locationManager.startLocationUpdates(); //位置情報の更新を開始する
        Log.d(getClass().getSimpleName(), "Start Location Update");
    }

    @Override
    public void onResume() {
        super.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();

        if(locationManager != null) {
            locationManager.stopLocationUpdates(); //位置情報の更新を停止する
            Log.d(getClass().getSimpleName(), "Stop Location Update");
        }
    }

    @Override
    public void onLocationResult(LocationResult locationResult) {

    }
}
```

これで、BaseActivityを継承したクラスから容易にLocationを利用できるようになりました。

#現在地から目的地までの経路を探索する

経路探索サービスMapQuestに登録して得たAPIキーを登録し経路探索APIを利用します。

```NavigationActivity.java
RoadManager roadManager = new MapQuestRoadManager("***YOUR API KEY***");
roadManager.addRequestOption("routeType=bicycle"); //自転車向けルートの設定
ArrayList<GeoPoint> waypoints = new ArrayList<GeoPoint>(); //経路の始点と終点
```

アプリを起動した段階では地図の示す位置が全く違う場所なので、初回に呼び出されたonLocationResultで取得した現在地を中心にします。

```MainActivity.java
double lat,lng; //緯度と経度
boolean isFirstGetLoc = false;

@Override
public void onLocationResult(LocationResult locationResult) {
    if(locationResult == null) { //位置情報が取得できなかった場合
        Log.e(getClass().getSimpleName(), "# No location data.");
        return;
    }

    /* 現在地の緯度経度の取得 */
    lat = locationResult.getLastLocation().getLatitude();
    lng = locationResult.getLastLocation().getLongitude();

    if(isFirstGetLoc == false) {
        initializeGeoPoint(mMapView);
        isFirstGetLoc = true;
    }

    updateNowDot();
}

```

```MainActivity.java
/* 起動時の初期座標で中心点を設定する */
protected void initializeGeoPoint(MapView mMapView) {
    IMapController mapController = mMapView.getController();
    //ズームレベルの設定
    mMapView.setMinZoomLevel(1.0);
    mapController.setZoom(20.0);

    //中心座標の設定
    setCenterPoint(lat, lng);
    //ズームボタンの無効化
    mMapView.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);
    mMapView.setMultiTouchControls(false);
    mMapView.setClickable(false);
}

/* 指定された座標を地図の中心に設定する */
protected void setCenterPoint(double latitude, double longitude) {
    GeoPoint centerPoint = new GeoPoint(lat, lng);
    IMapController mapController = mMapView.getController();
    mapController.setCenter(centerPoint);
}
```

現在地にマーカーを描画します。

```MainActivity.java
/* 現在地にマーカーを描画する */
protected void updateNowDot() {
    //現在地マーカーをリセットする
    if(nowDot.getPosition() != null) { //現在地のマーカーがあれば
        nowDot.remove(mMapView); //現在地のマーカーを削除する
    }

    //現在地にマーカーを生成する
    nowDot.setPosition(new GeoPoint(lat, lng));
    mMapView.getOverlays().add(nowDot);
    mMapView.invalidate(); //マーカーを最適化する

    //マーカーのアイコンの変更
    Drawable icon = ResourcesCompat.getDrawable(getResources(), R.drawable.marker_default, null);
    nowDot.setIcon(icon);
}

```

タップした位置を目的地とすると共に、MapQuestAPIを使って経路に線を引きます。

```NavigationActivity.java
private double targetLat, targetLng;
private Road road;
private RoadNode node;
private Polyline roadOverlay;
private Marker marker;

final MapEventsReceiver mReceive = new MapEventsReceiver() {

            @Override
            public boolean singleTapConfirmedHelper(GeoPoint p) {

                /* 地図上でタップした座標の取得 */
                targetLat = p.getLatitude();
                targetLng = p.getLongitude();

                GeoPoint startPoint = new GeoPoint(lat, lng); //現在地
                GeoPoint endPoint = new GeoPoint(targetLat, targetLng); //タップした地点を目的地に設定する
                waypoints.add(startPoint); //始点を経路に追加する
                waypoints.add(endPoint); //終点を経路に追加する


                road = roadManager.getRoad(waypoints); //経路を取得する
                roadOverlay = RoadManager.buildRoadOverlay(road); //経路のオーバーレイを生成する
                mMapView.getOverlays().add(roadOverlay); //経路のオーバーレイを地図上に追加する
                mMapView.invalidate(); //マーカーの最適化

                //経路探索
                Drawable nodeIcon = getResources().getDrawable(R.drawable.marker_default);
                for(int i = 0; i < road.mNodes.size(); i++) {
                    node = road.mNodes.get(i);
                    nodeMarker = new Marker(mMapView);
                    nodeMarker.setPosition(node.mLocation);
                    nodeMarker.setIcon(nodeIcon);
                    nodeMarker.setTitle("道順 " +i);
                    mMapView.getOverlays().add(nodeMarker);
                    nodeMarker.setSnippet(node.mInstructions);
                    nodeMarker.setSubDescription(Road.getLengthDurationText(getApplicationContext(), node.mLength, node.mDuration));
                }

                targetDot.setPosition(new GeoPoint(targetLat, targetLng));
                mMapView.getOverlays().add(targetDot);
                mMapView.invalidate();
                Drawable icon = ResourcesCompat.getDrawable(getResources(), R.drawable.center, null);
                targetDot.setIcon(icon);
                targetDot.setTitle("目的地");

                targetAddress = getAddress(targetLat, targetLng);
                targetDot.setSubDescription(targetAddress);


                return false;
            }

            @Override
            public boolean longPressHelper(GeoPoint p) {
                return false;
            }
        };

        MapEventsOverlay eventsOverlay = new MapEventsOverlay(getBaseContext(), mReceive);
        mMapView.getOverlays().add(eventsOverlay);

```

#緯度経度から住所文字列を得る

Geocoderを利用して位置情報をもとに住所を取得します。

```NavigationActivity.java
 /* 住所の取得 */
protected String getAddress(double latitude, double longitude){
    String address = new String();
    Geocoder geocoder = new Geocoder(this, Locale.getDefault());
    try {
        List<Address> list_address = geocoder.getFromLocation(latitude, longitude, 1);
        if(!list_address.isEmpty()) { //住所情報が空でなければ
            int index = list_address.get(0).getMaxAddressLineIndex(); //取得できる住所情報が何行あるか
            for(int i = 0; i <= index; i++) { //住所情報を取得し文字列変数に格納する
                address = list_address.get(0).getAddressLine(i);
            }
        }
   } catch (IOException e) {
        e.printStackTrace();
   }

    return address; //住所を返す
}
```

ここまでで、最低限の経路探索ができるマップアプリケーションが出来ました。

![地図アプリケーションの動作イメージ画像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88961/fc80e80d-7241-e459-b1a0-2aa6b82a03ca.png "動作イメージ")



#参考

+ [定期的に現在地の更新情報を受け取る | Android デベロッパー | Android Developers](https://developer.android.com/training/location/receive-location-updates?hl=ja)
+ [OSMBonusPack - Github](https://github.com/MKergall/osmbonuspack/wiki/Features)
+ [FusedLocationProviderClientを用いた位置情報の取得 - Qiita](https://qiita.com/maebaru/items/ba821938e66498d6ae76)
+ [osmdroid を使って OpenStreetMap の地図を Android に表示する - Qiita](https://qiita.com/ohwada/items/c3741fb14a5142035314)
+ [Geocoder | Android デベロッパー | Android Developers](https://developer.android.com/reference/android/location/Geocoder)
+ [【Androidアプリ開発】緯度・経度から住所を取得する方法 - 鈴木商店ブログ](https://blog.suzukishouten.co.jp/archives/761)