GPSで得られた現在地を表示する(アンドロイド地図アプリの開発 その8)

アンドロイド地図アプリの開発の第8回目です。
今回は、前回の予告に反して、GPSで位置情報を取得し、地図上に表示する処理を行います。
alasixosaka.hatenablog.com
実は、タイル抜けが発生していた以前のアプリでは実験済みでうまくいっていたので、問題なく動くだろうと思っていたところ、タイル抜けに対応するために、ファイルの構成が大幅に変わってしまったために結構はまってしまい、こちらの記事を先に書くことにしました。
alasixosaka.hatenablog.com
GPSで位置情報を得ることに関する詳細は以前に書いたので今回は省略します。

タイル抜けに対応した新しいアプリで、GPS位置情報を反映させるときに、どこにそのプログラムを書くかが最も問題でした。
現在の構成は、MainActivity.javaがあって、そこから、RotateMapViewer.javaを呼び出して、回転する地図を表示させています。本家Mapsforgeのサンプルプログラムでは、それと別個にGPSの位置情報を反映プログラムLocationOvalayMapViewer.javaがあります。
一応念のため、本家サンプルプログラムと同様に、LocationOvalayMapViewe.javaを配置して、MainActivity.javaから呼び出すことをしてみましたが、当然ですが回転する地図は表示されませんでした。
仕方がないので、RotateMapViewer.javaGPSの位置情報を反映させるプログラムを書いてみたところ、GPSで取得した現在地が地図の真ん中に来るようになるものの、現在地マークが表示されませんでした。そこで、RotateMapViewer.javaをよく見ると、

public class RotateMapViewer extends OverlayMapViewer {

のように、OvalayMapViewer.javaを継承しています。OvalayMapViewer.javaは、バルーンやらラインやらを地図にオーバーレイして表示をする処理を行っています。

f:id:alasixOsaka:20190812180709j:plain
OvalayMapViewer.javaは地図上にバルーンなどのマーク等を重ね書きする
これはこれで、この先に実装する予定の、ルートの表示やPOIの表示に使える処理ですし、現在地マークもオーバーレイで重ね書きしているので、この部分にGPSの処理を書き込むことにしました。
やったことのポイントだけ書いておきます。
まず、GPSを使うときはパーミッションが必要ですので、AndroidManifest.xmlに以下を追記します。

 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

現在地マークですが、Mapsforge本家サイトのMapsforge-SamplesーAndroidの下にあるresフォルダのさらに下にあるdrawable-hdpiにあります。ic_maps_indicator_current_position_pngというのがそれです。ただ、このままではマークが少々大きいので今回は70%に縮小して使いました。縮小するにあたっては、初めはペイントでファイルを読み込んで単純に70%に縮小して出力したのですが、どういう訳か背景が白になってしまい表示がおかしくなってしまいました。そこで、PowerPointで読み込んで縮小してみたところうまくいったので、この方法で出力したファイルを使いました。なぜこうなったのかは今でもよくわかりません。
ファイルは、PC上のどこかに適当に置いておいて、エクスプローラーでコピーし、AndroidStudioのres→drawableフォルダを開いてその状態でEdit→PasetすればOKでした。
次に、MainActivityのレイアウトを少し変更します。GPSの位置情報を反映させるかさせないかのスイッチを表示させるためです。なぜこのようなことをするかというと、GPSの位置情報を反映させると強制的に現在地が地図の真ん中に来てしまうので、例えばルートを確認したい時などに不便になるからです。
今回はラジオボタンを使ってOnとOffを選択できるようにしました。
activity_main.xnlに次の文を追記してラジオボタンを追加します。

<RadioGroup
        android:id="@+id/radiogroup"
        android:layout_width="262dp"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_marginStart="83dp"
        android:layout_marginLeft="83dp"
        android:layout_marginTop="317dp"
        android:layout_marginBottom="10dp"
        android:background="#df7401"
        android:orientation="horizontal"
        android:paddingTop="10dp"
        android:paddingBottom="10dp">

        <RadioButton
            android:id="@+id/rbOn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="25dp"
            android:layout_marginRight="25dp"
            android:background="#ffffff"
            android:text="@string/onGPS" />

        <RadioButton
            android:id="@+id/rbOff"
            android:layout_width="101dp"
            android:layout_height="match_parent"
            android:background="#ffffff"
            android:text="@string/offGPS" />

    </RadioGroup>

RadioGroupをまず作って、その中に必要な数だけRatioButtonを配置します。見栄えあまりよくないですがこんな感じになります。見栄えはおいおい直してゆこうと思っています。

f:id:alasixOsaka:20190824181338j:plain
ラジオボタンを追加したメイン画面
文字も"GPS On"と”GPS Off"となっていますが、GPSの機能そのものをOn/Offしているわけではないので、表示を改める必要を感じていますが、とりあえず今はこのままにしてあります。
この文字を表示させるのは、string.xmlに下記を追記して行います。

<string name="onGPS">GPS On</string>
<string name="offGPS">GPS Off</string>

このOn/Offの状態を保存するためにグローバル変数を使いました。また、ラジオボタンを使うこと、GPSを使うことでMainActivityの冒頭は下記のようになりました。

public class MainActivity extends Activity implements  RadioGroup.OnCheckedChangeListener  {

    private final static int PERMISSION_REQUEST_CODE = 1;
    private final static int PERMISSION_GPS_CODE = 1001;

    public static Uri launchUrl;
    private SamplesApplication setGPS;

implements RadioGroup.OnCheckedChangeListenerでラジオボタンを押した場合のリスナーを実装。また、private final static int PERMISSION_GPS_CODE = 1001としてGPSパーミッションチェックに使うコードを登録。そして、private SamplesApplication setGPSとして、グローバル変数setGPSを登録しています。
また、OnCreateの最初の部分は

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setGPS=(SamplesApplication)getApplication();
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED)  {
            String[] permissions = {Manifest.permission.ACCESS_FINE_LOCATION};
            ActivityCompat.requestPermissions(MainActivity.this, permissions, 1001);
            return;
        }

のように、setGPS=(SamplesApplication)getApplication();としてグローバル変数を使えるようにします。
そして、ラジオボタンが押された時の処理は

public void onCheckedChanged(RadioGroup group, int checkedId) {
        if (checkedId == R.id.rbOn){
            setGPS.setGPScondition(true);

        }else{
            setGPS.setGPScondition(false);
        }

    }

として、checkedIdの結果からラジオボタンの”GPS On”が押された場合は、setGPS.setGPScondition(true)としsetGPSをtrueにしています。”GPS off”が押された場合はsetGPS.setGPScondition(fale)としてsetGPSをfalseにしています。
また、clickWait()に下記を追記します。

private void clickWait(){

        ~省略~

        RadioGroup group = (RadioGroup)findViewById(R.id.radiogroup);
        group.setOnCheckedChangeListener((RadioGroup.OnCheckedChangeListener) this);
        if(setGPS.getGPScondition()==false){
            group.check(R.id.rbOff);
        }else {
            group.check(R.id.rbOn);
        }

    }

RadioGroup group = (RadioGroup)findViewById(R.id.radiogroup)でラジオグループを取得。
group.setOnCheckedChangeListener((RadioGroup.OnCheckedChangeListener) this)でリスナーを登録。
その下のif文はsetGPS.getGPScondition()でグローバル変数setGPSの値を取得し、falseならgroup.check(R.id.rbOff)として、ラジオボタンのoffの方にマークを入れます。この場合はOnとOffしかないので、elseでoffの場合の処理をしています。
一応MainActivityの全文を載せます。importは省略。

public class MainActivity extends Activity implements  RadioGroup.OnCheckedChangeListener  {

    private final static int PERMISSION_REQUEST_CODE = 1;
    private final static int PERMISSION_GPS_CODE = 1001;

    public static Uri launchUrl;
    private SamplesApplication setGPS;



    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setGPS=(SamplesApplication)getApplication();
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED)  {
            String[] permissions = {Manifest.permission.ACCESS_FINE_LOCATION};
            ActivityCompat.requestPermissions(MainActivity.this, permissions, 1001);
            return;
        }


        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;
                }
            case PERMISSION_GPS_CODE:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){

                    if(ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION)
                            != PackageManager.PERMISSION_GRANTED){
                        return;
                    }

                }

            default:
        }

        // アプリを終了
        this.finish();
    }
    private void clickWait(){

        Button btClick = findViewById(R.id.btRotationalView);

        BtListener listener = new BtListener();
        btClick.setOnClickListener(listener);
        RadioGroup group = (RadioGroup)findViewById(R.id.radiogroup);
        group.setOnCheckedChangeListener((RadioGroup.OnCheckedChangeListener) this);
        if(setGPS.getGPScondition()==false){
            group.check(R.id.rbOff);
        }else {
            group.check(R.id.rbOn);
        }

    }


    private class BtListener implements View.OnClickListener {
        @Override
        public void onClick(View view){

            int id = view.getId();

            switch (id) {

                case R.id.btRotationalView:
                    Intent RMV = new Intent(getApplicationContext(),RotateMapViewer.class);
                    startActivity(RMV);
                    break;


            }
            //Intent SMV = new Intent(getApplicationContext(),SimplestMapViewer.class);
            //startActivity(SMV);


        }
    }
    public void onCheckedChanged(RadioGroup group, int checkedId) {
        if (checkedId == R.id.rbOn){
            setGPS.setGPScondition(true);

        }else{
            setGPS.setGPScondition(false);
        }

    }
}

グローバル変数を使うのにはもう少し処理が必要です。
まずManifestのapplicationに下記を追記します。

<application
        android:name=".SamplesApplication"

SamplesApplicationはActivityを継承し、全体の設定などを記述しています。これに、

public class SamplesApplication extends Application {

    ~省略~

    private boolean setGPS;

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

として、グローバル変数 setGPSをbooleanとして登録しています。
また、SamplesApplicationに下記を追記します。

 public boolean getGPScondition() {
        return setGPS;
    }
    public void setGPScondition(boolean setGPS) {
        this.setGPS = setGPS;
    }

getGPScondition()はグローバル変数setGPSの値を返す処理をします。setGPSconditionはグローバル変数に値をセットする処理を行います。
SamplesApplicationも全文を載せておきます。setGPSの初期値はfalseにしてあり、プログラムがスタートしたときはGPS位置情報を反映させない設定になっています。

public class SamplesApplication extends Application {

    public static final String TAG = "Mapsforge Samples";

    public static final String SETTING_DEBUG_TIMING = "debug_timing";
    public static final String SETTING_LANGUAGE_SHOWLOCAL = "language_showlocal";
    public static final String SETTING_PREFERRED_LANGUAGE = "language_selection";
    public static final String SETTING_RENDERING_THREADS = "rendering_threads";
    public static final String SETTING_SCALE = "scale";
    public static final String SETTING_TEXTWIDTH = "textwidth";
    public static final String SETTING_TILECACHE_PERSISTENCE = "tilecache_persistence";
    public static final String SETTING_WAYFILTERING = "wayfiltering";
    public static final String SETTING_WAYFILTERING_DISTANCE = "wayfiltering_distance";

    private boolean setGPS;

    @Override
    public void onCreate() {
        super.onCreate();
        AndroidGraphicFactory.createInstance(this);
        Log.e(TAG,
                "Device scale factor "
                        + Float.toString(DisplayModel.getDeviceScaleFactor()));
        SharedPreferences preferences = PreferenceManager
                .getDefaultSharedPreferences(this);
        float fs = Float.valueOf(preferences.getString(SETTING_SCALE,
                Float.toString(DisplayModel.getDefaultUserScaleFactor())));
        Log.e(TAG, "User ScaleFactor " + Float.toString(fs));
        if (fs != DisplayModel.getDefaultUserScaleFactor()) {
            DisplayModel.setDefaultUserScaleFactor(fs);
        }

        MapFile.wayFilterEnabled = preferences.getBoolean(SETTING_WAYFILTERING, true);
        if (MapFile.wayFilterEnabled) {
            MapFile.wayFilterDistance = Integer.parseInt(preferences.getString(SETTING_WAYFILTERING_DISTANCE, "20"));
        }
        MapWorkerPool.DEBUG_TIMING = preferences.getBoolean(SETTING_DEBUG_TIMING, false);

        setGPS = false;
    }

    public boolean getGPScondition() {
        return setGPS;
    }
    public void setGPScondition(boolean setGPS) {
        this.setGPS = setGPS;
    }

}

ここまで出来たらようやく、OvalayMapViewer.javaを改造します。はじめの部分は下記のように、
まず、implements LocationListenerとして、位置情報のリスナーを実装。
それから、private final static int PERMISSION_GPS_CODE = 1001として、パーミッションチェックのコードを登録。
private LocationManager locationManagerでロケーションマネージャーを登録。
さらに、private SamplesApplication setGPSでグローバル変数を使えるようにします。

public class OverlayMapViewer extends DefaultTheme implements LocationListener {

    private final static int PERMISSION_GPS_CODE = 1001;

    private LocationManager locationManager;
    private SamplesApplication setGPS;

    private MyLocationOverlay myLocationOverlay;

それから、現在位置マークのレイヤーを作成する処理を行います。これは、サンプルプログラムのLocationOvalayMapViewer.javaと同じです。

@Override
    protected void createLayers() {
        super.createLayers();
        setGPS=(SamplesApplication)getApplication();

        // we just add a few more overlays
        addOverlayLayers(mapView.getLayerManager().getLayers());
       
        Drawable drawable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? getDrawable(R.drawable.ic_maps_indicator_current_position) : getResources().getDrawable(R.drawable.ic_maps_indicator_current_position);
        Marker marker = new Marker(null, AndroidGraphicFactory.convertToBitmap(drawable), 0, 0);
        Circle circle = new Circle(null, 0,
                getPaint(AndroidGraphicFactory.INSTANCE.createColor(48, 0, 0, 255), 0, Style.FILL),
                getPaint(AndroidGraphicFactory.INSTANCE.createColor(160, 0, 0, 255), 2, Style.STROKE));

        myLocationOverlay = new MyLocationOverlay(marker, circle);
        mapView.getLayerManager().getLayers().add(myLocationOverlay);
    }

それから、PaintとMarkerの処理もサンプルプログラムと同じものをコピーします。Markerは使ってないみたいですが、一応コピーしました。

    private static Paint getPaint(int color, int strokeWidth, Style style) {
        Paint paint = AndroidGraphicFactory.INSTANCE.createPaint();
        paint.setColor(color);
        paint.setStrokeWidth(strokeWidth);
        paint.setStyle(style);
        return paint;
    }
    private Marker createMarker(LatLong latlong, int resource) {
        Drawable drawable = getResources().getDrawable(resource);
        Bitmap bitmap = convertToBitmap(drawable);

        return new Marker(latlong, bitmap, 0, -bitmap.getHeight() / 2);
    }

次からがGPS関係の処理です。まず、onCreateでinitLocationManeger()を呼び出して、LocationManagerを取得します。

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initLocationManager();

    }

    private void initLocationManager() {
        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
    }

それから、onStartではlocationStart()を呼び出して、GPSの位置情報取得を開始します。ここで、if文を使って、グローバル変数setGPSがtrueになっているかチェックしています。falseの場合はスタートしないようにしています。
また、onStopではlocationStop()を呼び出して位置情報取得を停止します。

@Override
    public void onStart() {
        super.onStart();
        Log.d("DEBUG", "GPSStart");
        if (setGPS.getGPScondition()==true) {
            locationStart();
        }

    }

    @Override
    public void onStop() {
        super.onStop();
        Log.d("DEBUG", "GPSStop");
        locationStop();
    }

locationStart()はcheckPermissionでパーミッションチェックを行い、 locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,0,0,this)として、ロケーションマネージャーを起動します。
locationStop()は、locationManager.removeUpdates(this)としてロケーションマネージャーを停止します。

    private void locationStart() {
        checkPermission();
        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,0,0,this);
    }

    private void locationStop() {
        locationManager.removeUpdates(this);
    }

checkPermission()は通常の手続きと同じです。

    private void checkPermission() {
        if(ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION},1001);
        }

    }

GPSの取得位置に変化があった場合の処理は、下記のようになります。これはサンプルプログラムと同じです。

@Override
    public void onLocationChanged(Location location) {
        this.myLocationOverlay.setPosition(location.getLatitude(), location.getLongitude(), location.getAccuracy());

        // Follow location
        this.mapView.setCenter(new LatLong(location.getLatitude(), location.getLongitude()));
    }

あとかっこの中に何も書く必要はないですが、下の三つは書いておく必要があるそうです。

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
        
    }

    @Override
    public void onProviderDisabled(String provider) {
        
    }

    @Override
    public void onProviderEnabled(String provider) {
        
    }

これで、GPSの位置を反映させて、回転表示ができるようになりました。

f:id:alasixOsaka:20190824201118j:plain
GPSで現在位置を表示して回転している地図
ようやくここまでました。だいたい70%くらいはできたかと思います。次こそは実機を使って、方向を合わせる処理をしてみたいと思います。そこまでできればなんとなく地図アプリとしての格好がつきそうです。
参考にしたサイト
[Android 開発] GPS を使う - Open MagicVox.net
Android位置情報の取得 - Qiita
ラジオボタン
ラジオボタンで複数の選択肢から1つを選択させる / Getting Started | TechBooster
プロジェクト内のアクティビティでグローバル変数を使う方法 - Android Studioでアプリ開発!
Activity間でのデータの共有 | Sharing data within activities | Android | Programming Cafe
Activityにまたがってグローバルに変数などのオブジェクトを共有するには | Technology-Gym