Android long running activity (#3512)

This commit is contained in:
Adrian Cojocaru
2025-08-18 13:41:40 +03:00
committed by GitHub
parent 153a980acf
commit b090713177
20 changed files with 23236 additions and 41 deletions

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@
/cmake-build-debug /cmake-build-debug
/platform/android/build /platform/android/build
/platform/android/MapLibreAndroid/src/main/assets/sdk_versions/com.mapbox.mapboxsdk /platform/android/MapLibreAndroid/src/main/assets/sdk_versions/com.mapbox.mapboxsdk
/platform/android/MapLibreAndroid/android-binaries-*.zip
*.code-workspace *.code-workspace
local.properties local.properties

View File

@@ -103,6 +103,11 @@ kotlin {
dependencies { dependencies {
implementation(project(":MapLibreAndroid")) implementation(project(":MapLibreAndroid"))
implementation(libs.maplibreNavigation) {
exclude(group = "org.maplibre.gl", module = "android-sdk")
}
implementation(libs.maplibreJavaTurf) implementation(libs.maplibreJavaTurf)
implementation(libs.supportRecyclerView) implementation(libs.supportRecyclerView)

View File

@@ -10,9 +10,12 @@
# GMS # GMS
-dontnote com.google.android.gms.** -dontnote com.google.android.gms.**
-dontwarn com.google.android.gms.**
-keep class com.google.android.gms.** { *; }
-keep class org.maplibre.android.testapp.model.customlayer.ExampleCustomLayer { *; } -keep class org.maplibre.android.testapp.model.customlayer.ExampleCustomLayer { *; }
# okhttp # okhttp
-dontwarn org.bouncycastle.jsse.BCSSLSocket -dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.BCSSLParameters -dontwarn org.bouncycastle.jsse.BCSSLParameters

View File

@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application <application
android:name=".MapLibreApplication" android:name=".MapLibreApplication"
@@ -212,6 +213,31 @@
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity" /> android:value=".activity.FeatureOverviewActivity" />
</activity> </activity>
<activity
android:name=".activity.stability.LongRunningActivity"
android:description="@string/description_long_running_maps"
android:exported="true"
android:label="@string/activity_long_running_maps"
android:screenOrientation="portrait"
android:launchMode="singleInstance">
<meta-data
android:name="@string/category"
android:value="@string/category_integration" />
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity" />
</activity>
<activity
android:name=".activity.stability.UserMapActivity"
android:exported="false"
android:launchMode="singleInstance">
</activity>
<activity
android:name=".activity.stability.NavigationMapActivity"
android:exported="false"
android:screenOrientation="portrait"
android:launchMode="singleInstance">
</activity>
<activity <activity
android:name=".activity.fragment.MultiMapActivity" android:name=".activity.fragment.MultiMapActivity"
android:description="@string/description_multimap" android:description="@string/description_multimap"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -35,54 +35,15 @@ import org.maplibre.android.testapp.utils.BenchmarkResult
import org.maplibre.android.testapp.utils.BenchmarkRun import org.maplibre.android.testapp.utils.BenchmarkRun
import org.maplibre.android.testapp.utils.BenchmarkRunResult import org.maplibre.android.testapp.utils.BenchmarkRunResult
import org.maplibre.android.testapp.utils.FrameTimeStore import org.maplibre.android.testapp.utils.FrameTimeStore
import org.maplibre.android.testapp.utils.animateCameraSuspend
import org.maplibre.android.testapp.utils.jsonPayload import org.maplibre.android.testapp.utils.jsonPayload
import org.maplibre.android.testapp.utils.setStyleSuspend
import java.io.File import java.io.File
import java.util.ArrayList import java.util.ArrayList
import kotlin.collections.flatMap import kotlin.collections.flatMap
import kotlin.collections.toTypedArray import kotlin.collections.toTypedArray
import kotlin.coroutines.resume import kotlin.coroutines.resume
suspend fun MapLibreMap.animateCameraSuspend(
cameraUpdate: CameraUpdate,
durationMs: Int
): Unit = suspendCancellableCoroutine { continuation ->
animateCamera(cameraUpdate, durationMs, object : CancelableCallback {
var resumed = false
override fun onCancel() {
continuation.cancel()
}
override fun onFinish() {
if (!resumed) {
resumed = true
continuation.resume(Unit)
}
}
})
}
suspend fun MapView.setStyleSuspend(styleUrl: String): Unit =
suspendCancellableCoroutine { continuation ->
var listener: MapView.OnDidFinishLoadingStyleListener? = null
var resumed = false
listener = MapView.OnDidFinishLoadingStyleListener {
if (!resumed) {
resumed = true
listener?.let { removeOnDidFinishLoadingStyleListener(it) }
continuation.resume(Unit)
}
}
addOnDidFinishLoadingStyleListener(listener)
getMapAsync { map -> map.setStyle(styleUrl) }
continuation.invokeOnCancellation {
removeOnDidFinishLoadingStyleListener(listener)
}
}
/** /**
* Benchmark using a [android.view.TextureView] * Benchmark using a [android.view.TextureView]
*/ */

View File

@@ -0,0 +1,84 @@
package org.maplibre.android.testapp.activity.stability
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.ActivityOptions
import android.content.Context
import android.content.Intent
import android.hardware.display.DisplayManager
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.maplibre.android.testapp.R
import java.util.logging.Logger
class LongRunningActivity : AppCompatActivity() {
// config
companion object {
private val LOG = Logger.getLogger(LongRunningActivity::class.java.name)
// activity lifetime (seconds)
private const val DURATION = 10 * 60 * 60
// start one activity/view per display if available
private const val USE_SECONDARY_DISPLAY = true
@SuppressLint("NewApi")
fun printStats(context: Context) {
val activityManager = context.getSystemService(ACTIVITY_SERVICE) as ActivityManager
val sysMemInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(sysMemInfo)
LOG.info("System: \n" +
"\tavailable memory - ${sysMemInfo.availMem / 1048576} MB\n" +
"\ttotal memory - ${sysMemInfo.totalMem / 1048576} MB\n" +
"\tlow memory threshold - ${sysMemInfo.threshold / 1048576} MB\n" +
"\tlow memory - ${sysMemInfo.lowMemory}\n"
)
val appMemInfo = activityManager.getProcessMemoryInfo(intArrayOf(android.os.Process.myPid())).first()
LOG.info("Application memory: \n" +
appMemInfo.memoryStats.map { "\t${it.key} - ${it.value} KB" }.joinToString("\n")
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_long_running_maps)
if (USE_SECONDARY_DISPLAY && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager
val displays = displayManager.displays
if (displays.size > 1) {
// remove navigation map from layout
supportFragmentManager
.beginTransaction()
.remove(supportFragmentManager.findFragmentById(R.id.navigation_map)!!)
.commit()
// and move it to it's own activity
val activityOptions = ActivityOptions.makeBasic()
activityOptions.launchDisplayId = displays[1].displayId
startActivity(Intent(this, NavigationMapActivity::class.java), activityOptions.toBundle())
}
}
LOG.info("Activity running for $DURATION seconds")
val context = this
lifecycleScope.launch {
delay(DURATION * 1000L)
printStats(context)
finish()
}
}
}

View File

@@ -0,0 +1,373 @@
package org.maplibre.android.testapp.activity.stability
import android.annotation.SuppressLint
import android.location.Location
import android.os.Bundle
import android.os.PersistableBundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.google.gson.Gson
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.location.LocationComponentActivationOptions
import org.maplibre.android.location.OnLocationCameraTransitionListener
import org.maplibre.android.location.modes.CameraMode
import org.maplibre.android.location.modes.RenderMode
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.SupportMapFragment
import org.maplibre.android.testapp.activity.stability.LongRunningActivity.Companion.printStats
import org.maplibre.android.testapp.styles.TestStyles
import org.maplibre.android.testapp.utils.GeoParseUtil
import org.maplibre.android.testapp.utils.setStyleSuspend
import org.maplibre.geojson.Point
import org.maplibre.navigation.android.navigation.v5.location.replay.ReplayRouteLocationEngine
import org.maplibre.navigation.android.navigation.v5.milestone.BannerInstructionMilestone
import org.maplibre.navigation.android.navigation.v5.milestone.Milestone
import org.maplibre.navigation.android.navigation.v5.milestone.MilestoneEventListener
import org.maplibre.navigation.android.navigation.v5.models.DirectionsResponse
import org.maplibre.navigation.android.navigation.v5.models.DirectionsRoute
import org.maplibre.navigation.android.navigation.v5.models.RouteOptions
import org.maplibre.navigation.android.navigation.v5.navigation.MapLibreNavigation
import org.maplibre.navigation.android.navigation.v5.navigation.MapLibreNavigationOptions
import org.maplibre.navigation.android.navigation.v5.navigation.NavigationMapRoute
import org.maplibre.navigation.android.navigation.v5.routeprogress.ProgressChangeListener
import org.maplibre.navigation.android.navigation.v5.routeprogress.RouteProgress
import java.util.Locale
import java.util.logging.Logger
import kotlin.random.Random
class NavigationMap : SupportMapFragment(), ProgressChangeListener, MilestoneEventListener {
private lateinit var map: MapLibreMap
private lateinit var mapView: MapView
private lateinit var navigation: MapLibreNavigation
private lateinit var navigationMapRoute: NavigationMapRoute
private val replayRouteLocationEngine = ReplayRouteLocationEngine()
private var routeUpdateTimer = 0.0
// config
companion object {
private val LOG = Logger.getLogger(NavigationMap::class.java.name)
private const val RANDOM_SEED = 42
private val RANDOM = Random(RANDOM_SEED)
private val STYLES = arrayListOf(
TestStyles.AMERICANA,
TestStyles.OPENFREEMAP_LIBERTY,
TestStyles.OPENFREEMAP_BRIGHT,
TestStyles.AWS_OPEN_DATA_STANDARD_LIGHT,
TestStyles.PROTOMAPS_LIGHT,
TestStyles.PROTOMAPS_DARK,
TestStyles.PROTOMAPS_GRAYSCALE,
TestStyles.PROTOMAPS_WHITE,
TestStyles.PROTOMAPS_BLACK,
)
enum class RouteProvider {
Local,
OSRM,
Valhalla,
}
private val ROUTE_PROVIDER = RouteProvider.Local
private const val ROUTE_UPDATE_INTERVAL = 10.0
// used by remote providers
private val ROUTES = arrayOf(
Pair(LatLng(52.521487, 13.409961), LatLng(52.502501, 13.423342)), // BER short
Pair(LatLng(52.46314, 13.339116), LatLng(52.545929, 13.457635)), // BER long
Pair(LatLng(38.903727, -77.000668), LatLng(38.890509, -77.025958)), // DC short
Pair(LatLng(38.876664, -77.206788), LatLng(38.957716, -77.027674)), // DC long
Pair(LatLng(37.781832, -122.401477), LatLng(37.771035, -122.410592)), // SF short
Pair(LatLng(37.736431, -122.504263), LatLng(37.785594, -122.401209)), // SF long
)
private fun random(min: Double, max: Double): Double = min + RANDOM.nextDouble() * (max - min)
private fun random(min: Int, max: Int): Int = RANDOM.nextInt(min, max)
private fun randomZoom(): Double = random(13.0, 18.0)
private fun randomTilt(): Double = random(30.0, 60.0)
private fun randomSpeed(): Int = random(30, 130) // in km/h
private fun randomWaitTime(): Long = random(5000, 10000).toLong()
}
override fun onMapReady(maplibreMap: MapLibreMap) {
super.onMapReady(maplibreMap)
this.map = maplibreMap
this.mapView = view as MapView
LOG.info("NavigationMap seed $RANDOM_SEED")
run()
}
private fun run() {
lifecycleScope.launch {
mapView.setStyleSuspend(STYLES.random(RANDOM))
enableLocation()
navigation = MapLibreNavigation(requireContext(), MapLibreNavigationOptions
.builder()
.snapToRoute(true)
.build()
).apply {
snapEngine
addProgressChangeListener(this@NavigationMap)
addMilestoneEventListener(this@NavigationMap)
}
navigationMapRoute = NavigationMapRoute(navigation, mapView, map)
navigation.locationEngine = replayRouteLocationEngine
startNewRoute()
}
}
private fun startNewRoute() {
lifecycleScope.launch {
delay(randomWaitTime())
printStats(requireContext())
navigation.stopNavigation()
navigationMapRoute.removeRoute()
mapView.setStyleSuspend(STYLES.random(RANDOM))
val route = getRoute() ?: return@launch
// display route
navigationMapRoute = NavigationMapRoute(navigation, mapView, map)
navigationMapRoute.addRoute(route)
// force tile load at starting position
val startingPoint = route.routeOptions()?.coordinates()?.firstOrNull()
if (startingPoint != null) {
map.locationComponent.forceLocationUpdate(
Location("StartingLocation").apply {
latitude = startingPoint.latitude()
longitude = startingPoint.longitude()
}
)
}
delay(randomWaitTime())
replayRouteLocationEngine.assign(route)
routeUpdateTimer = route.duration()
navigation.startNavigation(route)
}
}
@SuppressLint("MissingPermission")
private fun enableLocation() {
map.locationComponent.activateLocationComponent(
LocationComponentActivationOptions
.builder(requireContext(), map.style!!)
.useDefaultLocationEngine(false)
.build()
)
map.locationComponent.isLocationComponentEnabled = true
map.locationComponent.renderMode = RenderMode.GPS
map.locationComponent.setCameraMode(
CameraMode.TRACKING_GPS,
object :
OnLocationCameraTransitionListener {
override fun onLocationCameraTransitionFinished(cameraMode: Int) {
map.locationComponent.zoomWhileTracking(randomZoom())
map.locationComponent.tiltWhileTracking(randomTilt())
}
override fun onLocationCameraTransitionCanceled(cameraMode: Int) {}
}
)
}
private fun getRoute(): DirectionsRoute? {
var location: LatLng? = null
var destination: LatLng? = null
val routeString: String? = when (ROUTE_PROVIDER) {
RouteProvider.Local -> {
val context = requireContext()
val routeFile = context.assets.list("routes/")!!.random(RANDOM)
LOG.info("Navigation - local route: $routeFile")
val routeStr = GeoParseUtil.loadStringFromAssets(context, "routes/$routeFile")
// quick parse to get the waypoints before starting the route
val json = Json.parseToJsonElement(routeStr).jsonObject
val waypoints = json["waypoints"]?.jsonArray
val startWaypoint = waypoints?.first()?.jsonObject?.get("location")?.jsonArray
val endWaypoint = waypoints?.last()?.jsonObject?.get("location")?.jsonArray
if (startWaypoint != null) {
location = LatLng(
Json.decodeFromJsonElement<Double>(startWaypoint.last()),
Json.decodeFromJsonElement<Double>(startWaypoint.first()),
)
}
if (endWaypoint != null) {
destination = LatLng(
Json.decodeFromJsonElement<Double>(endWaypoint.last()),
Json.decodeFromJsonElement<Double>(endWaypoint.first()),
)
}
routeStr
}
RouteProvider.OSRM -> {
val routePoints = ROUTES.random(RANDOM)
location = routePoints.first
destination = routePoints.second
val get = "${location.longitude},${location.latitude};" +
"${destination.longitude},${destination.latitude}" +
"?steps=true"
val request = Request
.Builder()
.header("User-Agent", "MapLibre Android")
.url("https://router.project-osrm.org/route/v1/driving/$get")
.get()
.build()
LOG.info("Navigation - OSRM route request " +
"(${location.longitude},${location.latitude}) -> " +
"(${destination.longitude},${destination.latitude})"
)
val response = OkHttpClient().newCall(request).execute()
response.body?.string()
}
RouteProvider.Valhalla -> {
val routePoints = ROUTES.random(RANDOM)
location = routePoints.first
destination = routePoints.second
val requestBody = Gson().toJson(
mapOf(
"format" to "osrm",
"costing" to "auto",
"banner_instructions" to true,
"voice_instructions" to true,
"language" to Locale.getDefault().language,
"directions_options" to mapOf(
"units" to "kilometers"
),
"costing_options" to mapOf(
"auto" to mapOf(
"top_speed" to 130
)
),
"locations" to listOf(
mapOf(
"lon" to location.longitude,
"lat" to location.latitude,
"type" to "break"
),
mapOf(
"lon" to destination.longitude,
"lat" to destination.latitude,
"type" to "break"
)
)
)
).toRequestBody("application/json; charset=utf-8".toMediaType())
val request = Request
.Builder()
.header("User-Agent", "MapLibre Android")
.url("https://valhalla1.openstreetmap.de/route")
.post(requestBody)
.build()
LOG.info("Navigation - Valhalla route request " +
"(${location.longitude},${location.latitude}) -> " +
"(${destination.longitude},${destination.latitude})"
)
val response = OkHttpClient().newCall(request).execute()
response.body?.string()
}
}
if (routeString == null) {
LOG.warning("Failed to get route data")
return null
}
val directionsResponse = DirectionsResponse.fromJson(routeString)
val route = directionsResponse.routes().first()
val routeOptions = RouteOptions
.builder()
.baseUrl("https://maplibre.org")
.profile("maplibre")
.user("maplibre")
.accessToken("maplibre")
.voiceInstructions(true)
.bannerInstructions(true)
.language(Locale.getDefault().language)
.coordinates(mutableListOf(
location?.let { Point.fromLngLat(it.longitude, location.latitude) },
destination?.let { Point.fromLngLat(it.longitude, destination.latitude) },
))
.requestUuid("0000-0000-0000-0000")
.build()
return route.toBuilder().routeOptions(routeOptions).build()
}
override fun onProgressChange(location: Location, progress: RouteProgress) {
map.locationComponent.forceLocationUpdate(location)
if (routeUpdateTimer - progress.durationRemaining() > ROUTE_UPDATE_INTERVAL) {
map.locationComponent.zoomWhileTracking(randomZoom())
map.locationComponent.tiltWhileTracking(randomTilt())
replayRouteLocationEngine.updateSpeed(randomSpeed())
routeUpdateTimer = progress.durationRemaining()
LOG.info("Navigation - remaining duration ${progress.durationRemaining()}")
}
}
override fun onMilestoneEvent(routeProgress: RouteProgress, instruction: String, milestone: Milestone) {
if (milestone !is BannerInstructionMilestone) {
return
}
if (milestone.bannerInstructions.primary().type() == "arrive") {
startNewRoute()
}
}
}
class NavigationMapActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportFragmentManager
.beginTransaction()
.replace(android.R.id.content, NavigationMap())
.commit()
}
}

View File

@@ -0,0 +1,283 @@
package org.maplibre.android.testapp.activity.stability
import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.maplibre.android.annotations.IconFactory
import org.maplibre.android.annotations.MarkerOptions
import org.maplibre.android.annotations.PolygonOptions
import org.maplibre.android.annotations.PolylineOptions
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.camera.CameraUpdateFactory
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.geometry.LatLngBounds
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.SupportMapFragment
import org.maplibre.android.testapp.styles.TestStyles
import org.maplibre.android.testapp.utils.animateCameraSuspend
import org.maplibre.android.testapp.utils.setStyleSuspend
import java.lang.reflect.Field
import java.util.logging.Logger
import kotlin.math.min
import kotlin.random.Random
class UserMap : SupportMapFragment() {
private lateinit var map: MapLibreMap
private lateinit var mapView: MapView
private lateinit var bitmapDrawables: List<Drawable>
companion object {
private val LOG = Logger.getLogger(UserMap::class.java.name)
private const val RANDOM_SEED = 42
private val RANDOM = Random(RANDOM_SEED)
private val STYLES = arrayListOf(
TestStyles.DEMOTILES,
TestStyles.AMERICANA,
TestStyles.OPENFREEMAP_LIBERTY,
TestStyles.OPENFREEMAP_BRIGHT,
TestStyles.AWS_OPEN_DATA_STANDARD_LIGHT,
TestStyles.PROTOMAPS_LIGHT,
TestStyles.PROTOMAPS_DARK,
TestStyles.PROTOMAPS_GRAYSCALE,
TestStyles.PROTOMAPS_WHITE,
TestStyles.PROTOMAPS_BLACK,
)
private val PLACES = arrayListOf(
LatLng(37.7749, -122.4194), // SF
LatLng(38.9072, -77.0369), // DC
LatLng(52.3702, 4.8952), // AMS
LatLng(60.1699, 24.9384), // HEL
LatLng(-13.1639, -74.2236), // AYA
LatLng(52.5200, 13.4050), // BER
LatLng(12.9716, 77.5946), // BAN
LatLng(31.2304, 121.4737), // SHA
)
// controls the list of icons available
// false -> use `ICONS` list
// true -> use all bitmap drawables in `org.maplibre.android.R.drawable.*`
private const val USE_ALL_DRAWABLES = false
private val ICONS = arrayListOf(
org.maplibre.android.R.drawable.maplibre_info_icon_default,
org.maplibre.android.R.drawable.maplibre_user_icon,
org.maplibre.android.R.drawable.maplibre_marker_icon_default,
org.maplibre.android.R.drawable.maplibre_compass_icon,
org.maplibre.android.R.drawable.maplibre_user_puck_icon,
)
private fun random(min: Double, max: Double): Double = min + RANDOM.nextDouble() * (max - min)
private fun random(min: Int, max: Int): Int = RANDOM.nextInt(min, max)
private fun <T> weightedRandom(values: List<Pair<T, Double>>): T {
val cumulativeWeights = values.scan(0.0) { acc, value -> acc + value.second }
var index =
cumulativeWeights.binarySearch(random(0.00001, cumulativeWeights.last()))
if (index < 0)
index = -index - 1
return values[index - 1].first
}
private fun randomSlowDuration(): Int = random(3000, 5000)
private fun randomFastDuration(): Int = random(500, 1000)
private fun randomPlacePoints(): Int = random(10, 20)
private fun randomPlaceActions(): Int = random(10, 20)
private fun randomLatLng(bounds: LatLngBounds): LatLng =
LatLng(
random(bounds.latitudeSouth, bounds.latitudeNorth),
random(bounds.longitudeWest, bounds.longitudeEast)
)
private fun randomLatLng(map: MapLibreMap): LatLng =
randomLatLng(map.projection.visibleRegion.latLngBounds)
private fun randomZoom(): Double = random(14.0, 20.0)
private fun randomTilt(): Double = random(0.0, 60.0)
private fun randomBearing(): Double = random(0.0, 360.0)
private fun randomAnnotationRemove(): Int = random(4, 8)
private fun randomAnnotationAdd(): Int = random(2, 4)
private fun randomPolyPoints(bounds: LatLngBounds): List<LatLng> {
val boundMultiplier = 0.25
val lat = random(bounds.latitudeSouth, bounds.latitudeNorth)
val lng = random(bounds.longitudeWest, bounds.longitudeEast)
val latRange = bounds.latitudeSpan * boundMultiplier / 2.0
val lngRange = bounds.longitudeSpan * boundMultiplier / 2.0
return List(random(3, 15)) {
LatLng(
lat + random(-latRange, latRange),
lng + random(-lngRange, lngRange)
)
}
}
private fun randomLineWidth(): Float = random(1.0, 4.0).toFloat()
private fun randomColor(): Int =
Color.argb(
RANDOM.nextInt(127, 256),
RANDOM.nextInt(256),
RANDOM.nextInt(256),
RANDOM.nextInt(256)
)
}
override fun onMapReady(maplibreMap: MapLibreMap) {
super.onMapReady(maplibreMap)
this.map = maplibreMap
this.mapView = view as MapView
LOG.info("UserMap seed $RANDOM_SEED")
run()
}
private fun run() {
if (USE_ALL_DRAWABLES) {
bitmapDrawables =
org.maplibre.android.R.drawable::class.java.fields.map { field: Field ->
ResourcesCompat.getDrawable(
this.resources,
field.getInt(null),
requireActivity().theme
)
}.filterIsInstance<BitmapDrawable>()
} else {
bitmapDrawables = ICONS.map { id ->
ResourcesCompat.getDrawable(
this.resources,
id,
requireActivity().theme
)!!
}
}
lifecycleScope.launch {
// since the random generator was not reset each run will have different values
while (true) {
runStyleActions()
}
}
}
private suspend fun runStyleActions() {
for (style in STYLES.shuffled(RANDOM)) {
mapView.setStyleSuspend(style)
runCameraActions()
}
}
private suspend fun runCameraActions() {
// camera actions with different weights
// position updates are more frequent
val actions = listOf(
Pair({ CameraUpdateFactory.newLatLng(randomLatLng(map)) }, 2.0),
Pair({ CameraUpdateFactory.zoomTo(randomZoom()) }, 1.0),
Pair({ CameraUpdateFactory.tiltTo(randomTilt()) }, 1.0),
Pair({ CameraUpdateFactory.bearingTo(randomBearing()) }, 1.0),
)
for (placeCenter in PLACES.shuffled(RANDOM)) {
// update all values to simulate a long jump
// (generated by the app, searching for a city/street, etc)
val cameraPosition = CameraPosition
.Builder()
.target(placeCenter)
.zoom(randomZoom())
.tilt(randomTilt())
.bearing(randomBearing())
.build()
map.animateCameraSuspend(
CameraUpdateFactory.newCameraPosition(cameraPosition),
randomSlowDuration()
)
repeat(randomPlacePoints()) {
// perform a series of fast camera actions
repeat(randomPlaceActions()) {
// update each value individually to simulate user interaction
map.animateCameraSuspend(weightedRandom(actions)(), randomFastDuration())
}
runAnnotationActions()
}
map.removeAnnotations()
}
}
private fun runAnnotationActions() {
// remove some annotations
repeat(min(map.annotations.size, randomAnnotationRemove())) {
map.removeAnnotation(map.annotations.random(RANDOM))
}
// add some annotations
val bounds = map.projection.visibleRegion.latLngBounds
// markers
repeat(randomAnnotationAdd()) {
// get a random drawable
map.addMarker(
MarkerOptions()
.position(randomLatLng(bounds))
.icon(
IconFactory.getInstance(requireContext()).fromBitmap(
bitmapDrawables
.random(RANDOM)
.mutate()
.apply { setTint(randomColor()) }
.toBitmap(),
)
)
)
}
// polylines
repeat(randomAnnotationAdd()) {
map.addPolyline(
PolylineOptions()
.color(randomColor())
.width(randomLineWidth())
.addAll(randomPolyPoints(bounds)),
)
}
// polygons
repeat(randomAnnotationAdd()) {
map.addPolygon(
PolygonOptions()
.fillColor(randomColor())
.strokeColor(randomColor())
.addAll(randomPolyPoints(bounds)),
)
}
}
}
class UserMapActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportFragmentManager
.beginTransaction()
.replace(android.R.id.content, UserMap())
.commit()
}
}

View File

@@ -0,0 +1,47 @@
package org.maplibre.android.testapp.utils
import kotlinx.coroutines.suspendCancellableCoroutine
import org.maplibre.android.camera.CameraUpdate
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.MapLibreMap.CancelableCallback
import org.maplibre.android.maps.MapView
import kotlin.coroutines.resume
suspend fun MapView.setStyleSuspend(styleUrl: String): Unit =
suspendCancellableCoroutine { continuation ->
var listener: MapView.OnDidFinishLoadingStyleListener? = null
var resumed = false
listener = MapView.OnDidFinishLoadingStyleListener {
if (!resumed) {
resumed = true
listener?.let { removeOnDidFinishLoadingStyleListener(it) }
continuation.resume(Unit)
}
}
addOnDidFinishLoadingStyleListener(listener)
getMapAsync { map -> map.setStyle(styleUrl) }
continuation.invokeOnCancellation {
removeOnDidFinishLoadingStyleListener(listener)
}
}
suspend fun MapLibreMap.animateCameraSuspend(cameraUpdate: CameraUpdate, durationMs: Int): Unit =
suspendCancellableCoroutine { continuation ->
animateCamera(cameraUpdate, durationMs, object : CancelableCallback {
var resumed = false
override fun onCancel() {
continuation.cancel()
}
override fun onFinish() {
if (!resumed) {
resumed = true
continuation.resume(Unit)
}
}
})
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:ignore="NestedWeights">
<fragment
android:id="@+id/user_map"
class="org.maplibre.android.testapp.activity.stability.UserMap"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="0.5" />
<fragment
android:id="@+id/navigation_map"
class="org.maplibre.android.testapp.activity.stability.NavigationMap"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="0.5" />
</LinearLayout>

View File

@@ -6,6 +6,7 @@
<string name="description_map_fragment">Showcase MapFragment</string> <string name="description_map_fragment">Showcase MapFragment</string>
<string name="description_map_fragment_support">Showcase SupportMapFragment</string> <string name="description_map_fragment_support">Showcase SupportMapFragment</string>
<string name="description_map_fragment_backstack">Showcase using a Map Fragment with a fragment backstack</string> <string name="description_map_fragment_backstack">Showcase using a Map Fragment with a fragment backstack</string>
<string name="description_long_running_maps">Showcase a long running scenario</string>
<string name="description_multimap">Activity with multiple maps on screen</string> <string name="description_multimap">Activity with multiple maps on screen</string>
<string name="description_press_for_marker">Add marker to map on long press</string> <string name="description_press_for_marker">Add marker to map on long press</string>
<string name="description_add_makers_by_json_api">Use JSON API to add markers</string> <string name="description_add_makers_by_json_api">Use JSON API to add markers</string>

View File

@@ -4,6 +4,7 @@
<string name="activity_map_fragment">Map Fragment</string> <string name="activity_map_fragment">Map Fragment</string>
<string name="activity_map_fragment_backstack">Backstack Map Fragment</string> <string name="activity_map_fragment_backstack">Backstack Map Fragment</string>
<string name="activity_multimap">Multiple Maps on Screen</string> <string name="activity_multimap">Multiple Maps on Screen</string>
<string name="activity_long_running_maps">Long running Maps</string>
<string name="activity_add_bulk_markers">Add Markers In Bulk</string> <string name="activity_add_bulk_markers">Add Markers In Bulk</string>
<string name="activity_animated_symbollayer">Animated SymbolLayer</string> <string name="activity_animated_symbollayer">Animated SymbolLayer</string>
<string name="activity_dynamic_marker">Dynamic Marker</string> <string name="activity_dynamic_marker">Dynamic Marker</string>

View File

@@ -36,6 +36,7 @@ kotlinxCoroutinesAndroid = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-
maplibreJavaGeoJSON = { group = "org.maplibre.gl", name = "android-sdk-geojson", version.ref = "mapLibreServices" } maplibreJavaGeoJSON = { group = "org.maplibre.gl", name = "android-sdk-geojson", version.ref = "mapLibreServices" }
maplibreGestures = { group = "org.maplibre.gl", name = "maplibre-android-gestures", version = "0.0.4" } maplibreGestures = { group = "org.maplibre.gl", name = "maplibre-android-gestures", version = "0.0.4" }
maplibreJavaTurf = { group = "org.maplibre.gl", name = "android-sdk-turf", version.ref = "mapLibreServices" } maplibreJavaTurf = { group = "org.maplibre.gl", name = "android-sdk-turf", version.ref = "mapLibreServices" }
maplibreNavigation = { group = "com.github.maplibre.maplibre-navigation-android", name = "libandroid-navigation", version = "4.0.0" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
mockito = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } mockito = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" }

View File

@@ -16,6 +16,7 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url = uri("https://jitpack.io") }
} }
} }