mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-10-03 00:54:34 -04:00
chore(deps): migrate to maplibre-compose 0.18.0 (#7374)
This commit is contained in:
1 parent
96a9a68d2e
commit
3057c9be26
9 files changed
+86
-47
No files matched your search
+6
-6
@@ -16,11 +16,11 @@
|
||||
*/
|
||||
package org.meshtastic.feature.map.maplibre
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.maplibre.compose.camera.CameraAnimation
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.camera.CameraUpdate
|
||||
import org.maplibre.compose.map.MapState
|
||||
import org.maplibre.compose.util.DpPadding
|
||||
import org.maplibre.spatialk.geojson.BoundingBox
|
||||
|
||||
/**
|
||||
@@ -35,7 +35,7 @@ import org.maplibre.spatialk.geojson.BoundingBox
|
||||
internal suspend fun MapState.zoomBy(delta: Double, range: ClosedFloatingPointRange<Float>) {
|
||||
val target = (cameraPosition.zoom + delta).coerceIn(range.start.toDouble(), range.endInclusive.toDouble())
|
||||
if (target != cameraPosition.zoom) {
|
||||
animateCameraPosition(cameraPosition.copy(zoom = target), animation = CameraAnimation.Ease())
|
||||
animateCamera(CameraUpdate(zoom = target), CameraAnimation.Ease())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,9 +46,9 @@ internal const val ZOOM_STEP = 1.0
|
||||
* Frames [bounds] without zooming past [FRAME_MAX_ZOOM]: the fit is computed first and capped before the camera moves,
|
||||
* so nodes standing metres apart open on their surroundings rather than on empty tiles. Eased, not the default flight.
|
||||
*/
|
||||
internal suspend fun MapState.frameBounds(bounds: BoundingBox, padding: PaddingValues = PaddingValues(0.dp)) {
|
||||
val fitted = cameraForBounds(bounds, padding = padding)
|
||||
animateCameraPosition(fitted.cappedTo(FRAME_MAX_ZOOM), CameraAnimation.Ease())
|
||||
internal suspend fun MapState.frameBounds(bounds: BoundingBox, padding: DpPadding = DpPadding.Zero) {
|
||||
val fitted = cameraForBounds(bounds, fitPadding = padding)
|
||||
animateCamera(fitted.cappedTo(FRAME_MAX_ZOOM).toCameraUpdate(), CameraAnimation.Ease())
|
||||
}
|
||||
|
||||
/** This position, zoomed out to [maxZoom] if it is tighter than that. */
|
||||
|
||||
+3
-12
@@ -38,6 +38,7 @@ import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.maplibre.compose.camera.CameraAnimation
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.camera.CameraUpdate
|
||||
import org.maplibre.compose.location.BearingUpdate
|
||||
import org.maplibre.compose.location.LocationPermission
|
||||
import org.maplibre.compose.location.LocationState
|
||||
@@ -343,12 +344,7 @@ private fun SitePlannerSlot(
|
||||
nodeNum = nodeNum,
|
||||
mapCenter = { mapState.cameraPosition.target },
|
||||
moveTo = { target ->
|
||||
scope.launch {
|
||||
mapState.animateCameraPosition(
|
||||
mapState.cameraPosition.copy(target = target),
|
||||
animation = CameraAnimation.Ease(),
|
||||
)
|
||||
}
|
||||
scope.launch { mapState.animateCamera(CameraUpdate(target = target), CameraAnimation.Ease()) }
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
),
|
||||
@@ -395,12 +391,7 @@ private fun BoxScope.MapToolbar(
|
||||
if (location.following) {
|
||||
location.onToggleBearingLock()
|
||||
} else {
|
||||
scope.launch {
|
||||
mapState.animateCameraPosition(
|
||||
mapState.cameraPosition.copy(bearing = 0.0),
|
||||
animation = CameraAnimation.Ease(),
|
||||
)
|
||||
}
|
||||
scope.launch { mapState.animateCamera(CameraUpdate(bearing = 0.0), CameraAnimation.Ease()) }
|
||||
}
|
||||
},
|
||||
filterDropdownContent = {
|
||||
|
||||
+42
-8
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.meshtastic.feature.map.maplibre
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -30,18 +31,19 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import org.maplibre.compose.camera.CameraAnimation
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.camera.CameraUpdate
|
||||
import org.maplibre.compose.expressions.dsl.const
|
||||
import org.maplibre.compose.interaction.ClickResult
|
||||
import org.maplibre.compose.interaction.MapInteractions
|
||||
import org.maplibre.compose.layers.CircleLayer
|
||||
import org.maplibre.compose.location.BearingUpdate
|
||||
import org.maplibre.compose.location.LocationPuck
|
||||
import org.maplibre.compose.location.LocationState
|
||||
import org.maplibre.compose.location.LocationTrackingEffect
|
||||
import org.maplibre.compose.location.updateCamera
|
||||
@@ -51,7 +53,8 @@ import org.maplibre.compose.map.LocalViewport
|
||||
import org.maplibre.compose.map.MapState
|
||||
import org.maplibre.compose.map.MaplibreMap
|
||||
import org.maplibre.compose.map.rememberMapState
|
||||
import org.maplibre.compose.material3.LocationPuckDefaults
|
||||
import org.maplibre.compose.material3.LocationIndicatorDefaults
|
||||
import org.maplibre.compose.material3.LocationIndicatorLayer
|
||||
import org.maplibre.compose.overlay.include
|
||||
import org.maplibre.compose.util.MaplibreComposable
|
||||
import org.maplibre.spatialk.geojson.BoundingBox
|
||||
@@ -81,6 +84,8 @@ import org.meshtastic.feature.map.maplibre.style.MapOverlay
|
||||
import org.meshtastic.feature.map.maplibre.style.toBaseStyle
|
||||
import org.meshtastic.feature.map.maplibre.style.zoomRange
|
||||
import kotlin.math.floor
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.maplibre.compose.layers.LocationIndicatorDefaults as CoreLocationIndicatorDefaults
|
||||
|
||||
/**
|
||||
* Everything the mesh map draws, as one [MapState].
|
||||
@@ -280,9 +285,9 @@ private fun MeshMapNodeLayers(
|
||||
val current = mapState.cameraPosition
|
||||
// A cluster that cannot report an expansion zoom answers with a sentinel (0 on
|
||||
// Android and desktop, -1 on iOS), so clamp — never zoom out on a tap.
|
||||
mapState.animateCameraPosition(
|
||||
current.copy(target = centre, zoom = maxOf(expansionZoom, current.zoom)),
|
||||
animation = CameraAnimation.Ease(),
|
||||
mapState.animateCamera(
|
||||
CameraUpdate(target = centre, zoom = maxOf(expansionZoom, current.zoom)),
|
||||
CameraAnimation.Ease(),
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -342,12 +347,41 @@ private fun FollowUserLocation(
|
||||
@Composable
|
||||
@MaplibreComposable
|
||||
private fun UserLocationPuck(locationState: LocationState?, visible: Boolean) {
|
||||
if (locationState == null || !visible) return
|
||||
if (locationState == null) return
|
||||
|
||||
// The state overload, which resolves the latest measurement and its most accurate bearing itself.
|
||||
LocationPuck(idPrefix = "user-location", locationState = locationState, colors = LocationPuckDefaults.colors())
|
||||
// The indicator has no notion of age, so a fix that stops arriving is greyed and loses its accuracy ring here,
|
||||
// or an old position reads as a current one.
|
||||
val mark = locationState.lastLocationMeasurementMark
|
||||
var stale by remember(mark) { mutableStateOf(mark != null && mark.elapsedNow() > STALE_LOCATION_AFTER) }
|
||||
LaunchedEffect(mark) {
|
||||
if (mark == null || stale) return@LaunchedEffect
|
||||
delay(STALE_LOCATION_AFTER - mark.elapsedNow())
|
||||
stale = true
|
||||
}
|
||||
val colors = MaterialTheme.colorScheme
|
||||
|
||||
// The state overload, which resolves the latest measurement and its most accurate bearing itself. Hidden rather
|
||||
// than unmounted when tracking stops, since layer additions are queued and a quick toggle could lose the re-add.
|
||||
LocationIndicatorLayer(
|
||||
id = "user-location",
|
||||
locationState = locationState,
|
||||
visible = visible,
|
||||
accuracyRadiusColor = if (stale) Color.Transparent else colors.primary.copy(alpha = ACCURACY_FILL_ALPHA),
|
||||
accuracyRadiusBorderColor = if (stale) Color.Transparent else colors.primary,
|
||||
topImage =
|
||||
if (stale) {
|
||||
CoreLocationIndicatorDefaults.topImage(colors.surfaceDim, colors.onPrimary)
|
||||
} else {
|
||||
LocationIndicatorDefaults.topImage()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** How long a location fix counts as current, the threshold the library's own puck used before 0.18. */
|
||||
private val STALE_LOCATION_AFTER = 30.seconds
|
||||
|
||||
private const val ACCURACY_FILL_ALPHA = 0.15f
|
||||
|
||||
/**
|
||||
* The first corner tapped while authoring a geofence box.
|
||||
*
|
||||
|
||||
+13
-1
@@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.interaction.MapInteractions
|
||||
@@ -34,6 +35,7 @@ import org.maplibre.compose.map.MapState
|
||||
import org.maplibre.compose.map.MapUiOptions
|
||||
import org.maplibre.compose.map.MaplibreMap
|
||||
import org.maplibre.compose.map.rememberMapState
|
||||
import org.maplibre.compose.util.DpPadding
|
||||
import org.maplibre.spatialk.geojson.BoundingBox
|
||||
import org.meshtastic.feature.map.component.MapEngineUnavailable
|
||||
import org.meshtastic.feature.map.maplibre.component.BasemapSelection
|
||||
@@ -153,8 +155,18 @@ internal fun FitBoundsOnceVisible(
|
||||
// is cancelled by user input as well as by [key]: a fit lost that way is not retried until [key] changes
|
||||
// again, which for these maps may be never.
|
||||
val hasViewport = mapState.viewport != null
|
||||
// MapLibre takes physical edges; resolving start/end here keeps the wide trailing inset on the zoom pair's side
|
||||
// in RTL too.
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
val fitPadding =
|
||||
DpPadding(
|
||||
left = padding.calculateLeftPadding(layoutDirection),
|
||||
top = padding.calculateTopPadding(),
|
||||
right = padding.calculateRightPadding(layoutDirection),
|
||||
bottom = padding.calculateBottomPadding(),
|
||||
)
|
||||
LaunchedEffect(key, hasViewport) {
|
||||
if (!hasViewport) return@LaunchedEffect
|
||||
currentBounds()?.let { mapState.frameBounds(it, padding = padding) }
|
||||
currentBounds()?.let { mapState.frameBounds(it, padding = fitPadding) }
|
||||
}
|
||||
}
|
||||
+2
-4
@@ -42,6 +42,7 @@ import kotlinx.serialization.json.put
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.maplibre.compose.camera.CameraAnimation
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.camera.CameraUpdate
|
||||
import org.maplibre.compose.expressions.dsl.asBoolean
|
||||
import org.maplibre.compose.expressions.dsl.const
|
||||
import org.maplibre.compose.expressions.dsl.feature
|
||||
@@ -117,10 +118,7 @@ fun MapLibreInlineMap(
|
||||
// first composition does not animate to where the camera already is.
|
||||
LaunchedEffect(target) {
|
||||
if (mapState.cameraPosition.target != target) {
|
||||
mapState.animateCameraPosition(
|
||||
mapState.cameraPosition.copy(target = target),
|
||||
animation = CameraAnimation.Ease(),
|
||||
)
|
||||
mapState.animateCamera(CameraUpdate(target = target), CameraAnimation.Ease())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -27,7 +27,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.maplibre.compose.map.LocalMapState
|
||||
import org.maplibre.compose.overlay.DisappearingScaleBar
|
||||
import org.maplibre.compose.overlay.LocalCameraPadding
|
||||
import org.maplibre.compose.overlay.LocalViewportInsets
|
||||
import org.maplibre.compose.overlay.include
|
||||
import org.maplibre.compose.overlay.MapOverlay as MaplibreOverlay
|
||||
|
||||
@@ -46,21 +46,21 @@ import org.maplibre.compose.overlay.MapOverlay as MaplibreOverlay
|
||||
*/
|
||||
internal val MeshMapOrnaments: MaplibreOverlay = MaplibreOverlay {
|
||||
val mapState = checkNotNull(LocalMapState.current)
|
||||
val cameraPadding = LocalCameraPadding.current
|
||||
val viewportInsets = LocalViewportInsets.current
|
||||
|
||||
// A custom overlay fills the map and the library keeps its own inset helper internal, so the scale bar has to
|
||||
// carry the camera padding, safe-area insets and edge margin that the built-in controls apply for themselves.
|
||||
// carry the viewport insets, safe-area insets and edge margin that the built-in controls apply for themselves.
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.padding(cameraPadding)
|
||||
.consumeWindowInsets(cameraPadding)
|
||||
.padding(viewportInsets)
|
||||
.consumeWindowInsets(viewportInsets)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.padding(MaplibreOverlay.Spacing),
|
||||
) {
|
||||
DisappearingScaleBar(
|
||||
metersPerDp = mapState.viewport?.metersPerDpAtTarget ?: 0.0,
|
||||
zoom = mapState.cameraPosition.zoom,
|
||||
metersPerDp = { mapState.viewport?.metersPerDpAtTarget ?: 0.0 },
|
||||
zoom = { mapState.cameraPosition.zoom },
|
||||
modifier = Modifier.align(Alignment.TopStart),
|
||||
)
|
||||
}
|
||||
|
||||
+2
-6
@@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import kotlinx.coroutines.launch
|
||||
import org.maplibre.compose.camera.CameraAnimation
|
||||
import org.maplibre.compose.camera.CameraUpdate
|
||||
import org.maplibre.compose.map.MapState
|
||||
import org.meshtastic.feature.map.component.MapControlsOverlay
|
||||
|
||||
@@ -54,12 +55,7 @@ internal fun SecondaryMapControls(
|
||||
filterDropdownContent = { filterMenu?.invoke(filterMenuExpanded) { filterMenuExpanded = false } },
|
||||
bearing = mapState.cameraPosition.bearing.toFloat(),
|
||||
onCompassClick = {
|
||||
scope.launch {
|
||||
mapState.animateCameraPosition(
|
||||
mapState.cameraPosition.copy(bearing = 0.0),
|
||||
animation = CameraAnimation.Ease(),
|
||||
)
|
||||
}
|
||||
scope.launch { mapState.animateCamera(CameraUpdate(bearing = 0.0), CameraAnimation.Ease()) }
|
||||
},
|
||||
mapTypeContent = { BasemapButton(selection = basemaps) },
|
||||
onToggleLocationTracking = null,
|
||||
|
||||
@@ -71,7 +71,7 @@ jetbrains-adaptive = "1.3.0-rc01"
|
||||
# 2.8.0–2.8.1). KML is now read by the app's own xmlutil-based converter and that parser is unused, so
|
||||
# the pairing — and the canary test that guarded it — is gone. The floor stays as a plain minimum.
|
||||
android-maps-utils = "5.2.0"
|
||||
maplibre-compose = "0.17.0"
|
||||
maplibre-compose = "0.18.0"
|
||||
maps-compose = "8.6.0"
|
||||
|
||||
# ML Kit
|
||||
|
||||
+10
-2
@@ -24,6 +24,7 @@ import androidx.compose.ui.graphics.toPixelMap
|
||||
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
||||
import androidx.compose.ui.text.font.createFontFamilyResolver
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
@@ -39,6 +40,7 @@ import org.maplibre.spatialk.geojson.Position
|
||||
import org.meshtastic.feature.map.maplibre.layers.NodeLayers
|
||||
import org.meshtastic.feature.map.maplibre.style.Basemaps
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/** One capture: the map area in dp at the screen density, and the zoom the form factor asks for. */
|
||||
internal data class MapArea(val widthDp: Int, val heightDp: Int, val density: Float, val zoom: Double)
|
||||
@@ -52,6 +54,12 @@ internal object MapSnapshot {
|
||||
private const val CAPTURE_TIMEOUT_MS = 180_000L
|
||||
private const val MAX_SETTLE_RUNTIMES = 4
|
||||
|
||||
// The generator is headless and has no Dispatchers.Main, so the runtime owns this thread instead, and every map
|
||||
// state call runs on it because map state rejects any other thread. Daemon, so it never holds the JVM open.
|
||||
private val mapThread =
|
||||
Executors.newSingleThreadExecutor { Thread(it, "maplibre-main").apply { isDaemon = true } }
|
||||
.asCoroutineDispatcher()
|
||||
|
||||
fun capture(areas: List<MapArea>, mesh: SampleMesh): Map<MapArea, ImageBitmap> =
|
||||
areas.associateWith { area -> captureUntilStable(area, mesh) }
|
||||
|
||||
@@ -62,7 +70,7 @@ internal object MapSnapshot {
|
||||
* shared tile cache: the first fills the cache from the network, the ones after it read the cache in a stable
|
||||
* order, and two consecutive runtimes agreeing means the run is reproducible. A warm runtime costs about a second.
|
||||
*/
|
||||
private fun captureUntilStable(area: MapArea, mesh: SampleMesh): ImageBitmap = runBlocking {
|
||||
private fun captureUntilStable(area: MapArea, mesh: SampleMesh): ImageBitmap = runBlocking(mapThread) {
|
||||
val cacheDir = Files.createTempDirectory("marketing-maplibre")
|
||||
val cacheFile = Path(cacheDir.resolve("cache.db").toString())
|
||||
try {
|
||||
@@ -82,7 +90,7 @@ internal object MapSnapshot {
|
||||
}
|
||||
|
||||
private suspend fun captureOnce(cacheFile: Path, area: MapArea, mesh: SampleMesh): ImageBitmap {
|
||||
val runtime = createMapRuntime(MapRuntimeOptions(cacheFile = cacheFile))
|
||||
val runtime = createMapRuntime(MapRuntimeOptions(cacheFile = cacheFile, mainDispatcher = mapThread))
|
||||
return try {
|
||||
withTimeout(CAPTURE_TIMEOUT_MS) { runtime.capture(area, mesh) }
|
||||
} finally {
|
||||
|
||||
Reference in new issue
Block a user