JetPack composeで地図を表示

今回はJetpack Composeで地図を表示するプログラムにポンコツ2人組が挑戦しました! 最初はGoogleMapを利用するつもりでしたが、GoogleMapは従量課金制のため、 今回は基本的には無料で利用できる「osmdroid」を利用することにしました。 osmdroidはAndroid上でOpenStreetMapを使えるようにしたライブラリです。 開発環境はAndroid Studio Giraffe(2022.3.1)です。

※この記事は2024/04/20時点の情報です。

build.gradle.kts(Module:app)

dependenciesに以下を追加します。
implementation("org.osmdroid:osmdroid-android:6.1.10")
implementation("org.osmdroid:osmdroid-mapsforge:6.1.10")
implementation("org.osmdroid:osmdroid-geopackage:6.1.10")
implementation("org.osmdroid:osmdroid-android:6.1.11")
implementation("com.github.MKergall:osmbonuspack:6.6.0")

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}
android {
    namespace = "com.example.mapap"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.example.mapap"
        minSdk = 30
        targetSdk = 33
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary = true
        }
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
    buildFeatures {
        compose = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.4.3"
    }
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
}

dependencies {

    implementation("androidx.core:core-ktx:1.9.0")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
    implementation("androidx.activity:activity-compose:1.8.2")
    implementation(platform("androidx.compose:compose-bom:2023.03.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-tooling")
    debugImplementation("androidx.compose.ui:ui-test-manifest")
    debugImplementation("androidx.compose.ui:ui")
    implementation("org.osmdroid:osmdroid-android:6.1.10")
    implementation("org.osmdroid:osmdroid-mapsforge:6.1.10")
    implementation("org.osmdroid:osmdroid-geopackage:6.1.10")
    implementation("org.osmdroid:osmdroid-android:6.1.11")
    implementation("com.github.MKergall:osmbonuspack:6.6.0")
}

setting.gradle.kts

dependencyResolutionManagementに以下を追加します。
これを追加しないとgradleを同期させた時にosmbonuspackがダウンロード失敗します。 maven { url = uri("https://jitpack.io") }

pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}

rootProject.name = "MapAp"
include(":app")
 

AndroidManifest.xml

manifestタグの中に以下を追加します。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MapAp"
        tools:targetApi="31"
        tools:replace="android:allowBackup">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.MapAp">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

MapDisplayManager.kt

Androidアプリ内で地図を表示し、指定された始点と終点間の経路を取得して線を表示します。 setupOSMDroid関数でOSMDroidの初期設定を行います。 引数として、デフォルトの地点(def)、初期ズームレベル(zoom)、経路の始点と終点(start、end)を受け取ります。 パーミッションが許可されているかどうかを確認し、地図の初期化と経路の取得を行います。

package com.example.mapap

import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.views.MapView
import androidx.compose.ui.platform.LocalContext
import org.osmdroid.util.GeoPoint
import org.osmdroid.bonuspack.routing.OSRMRoadManager
import org.osmdroid.bonuspack.routing.RoadManager
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MapDisplayManager(private val context: Context) {
    private lateinit var mapView: MapView

    // OSM(OpenStreetMap)Droidの初期設定を行う
    @Composable
    fun setupOSMDroid(def:GeoPoint, zoom:Double, start:GeoPoint, end:GeoPoint) {
        val osmConfig = Configuration.getInstance()
        osmConfig.userAgentValue = context.packageName // ユーザーエージェントを設定する

        val permissionsNeeded = arrayOf(
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION
        )

        if (!arePermissionsGranted(permissionsNeeded)) {
            ActivityCompat.requestPermissions(context as Activity, permissionsNeeded, 1)
        } else {
            initializeMapView(def, zoom)
            fetchRoad(start, end)
        }
    }

    // 指定されたパーミッションがすべて許可されているかどうかを確認
    @Composable
    fun arePermissionsGranted(permissions: Array<String>): Boolean {
        return permissions.all {
            ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        }
    }

    // デフォルトのタイルソースを設定し、初期表示範囲とズームレベルを設定
    @Composable
    fun initializeMapView(def:GeoPoint, zoom:Double) {
        mapView = remember {
            MapView(context).apply {
                setTileSource(TileSourceFactory.DEFAULT_TILE_SOURCE)
                // 初期表示範囲とズームレベルを設定する
                controller.setCenter(def)
                controller.setZoom(zoom)
            }
        }
    }

    // 初期化された地図ビューを返す
    @Composable
    fun getMapView(): MapView {
        return mapView
    }

    // 指定された始点と終点間の経路を取得し、地図上に表示
    private fun fetchRoad(start:GeoPoint, end:GeoPoint) {
        GlobalScope.launch(Dispatchers.IO) {
            val roadManager = OSRMRoadManager(context)
            val waypoints = ArrayList<GeoPoint>()
            waypoints.add(start)
            waypoints.add(end)

            val road = roadManager.getRoad(waypoints)

            withContext(Dispatchers.Main) {
                // メインスレッドでUIを更新する必要がある場合はここで行う
                val roadOverlay = RoadManager.buildRoadOverlay(road, Color.BLUE, 5f)
                mapView.overlays.add(roadOverlay)
                mapView.invalidate() // 地図を再描画する
            }
        }
    }
}

MainActivity.kt
メインのクラスです。MapDisplayManagerにパラメータを渡して地図を表示しています。

package com.example.mapap

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.osmdroid.views.MapView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import org.osmdroid.util.GeoPoint

class MainActivity : ComponentActivity() {
    private lateinit var mapDisplayManager: MapDisplayManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val def = GeoPoint(35.6809, 139.7673)    //初期位置 東京駅
            val zoom = 15.0                          //ズームレベル
            val start = GeoPoint(35.6809, 139.7673)  //開始位置 東京駅
            val end = GeoPoint(35.6918, 139.7709)    //終了位置 神田駅

            mapDisplayManager = MapDisplayManager(LocalContext.current)
            mapDisplayManager.setupOSMDroid(def, zoom, start, end)
            val mapView = mapDisplayManager.getMapView()
            MapView(mapView)
        }
    }
}

@Composable
fun MapView(mapView: MapView) {
    androidx.compose.ui.viewinterop.AndroidView(
        modifier = Modifier
            .fillMaxSize()
        ,
        factory = { mapView }
    )
}

実行結果
以下が実行結果です。東京駅周辺の地図が表示され、東京駅から神田駅まで線が引かれていますね。 ※このコードは始点と終点を直線で引くだけなので、道なりに線は引かれません

Jetpack composeで地図を表示したスクリーンショットです

以上がAndroidで地図を表示させるプログラムになります。 画面が黒くなったり、青くなったりしてなかなか上手くいきませんでしたが、 何とか動作するようになりました!機会があればGoogleMapにも挑戦したいと思います。
OSMDroidを使って地図を表示するプログラムを理解できましたか?

地図を表示するプログラムを覚えられましたか?

OSMDroidを使いこなせば、もっといろいろな操作ができると思いますので興味がある人はチャレンジしてみてください。

管理人情報