fix(map): location perms and button visibility, breadcrumb taps (#4651)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-02-26 09:39:15 -06:00 committed by GitHub
parent 27e7669366
commit b60d67297d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 160 additions and 85 deletions

View file

@ -66,6 +66,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
@ -83,12 +84,13 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.gpsDisabled
import org.meshtastic.core.common.hasGps
import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.util.toString
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.calculating
import org.meshtastic.core.resources.cancel
@ -98,7 +100,10 @@ import org.meshtastic.core.resources.delete_for_everyone
import org.meshtastic.core.resources.delete_for_me
import org.meshtastic.core.resources.expires
import org.meshtastic.core.resources.getString
import org.meshtastic.core.resources.heading
import org.meshtastic.core.resources.latitude
import org.meshtastic.core.resources.location_disabled
import org.meshtastic.core.resources.longitude
import org.meshtastic.core.resources.map_cache_info
import org.meshtastic.core.resources.map_cache_manager
import org.meshtastic.core.resources.map_cache_size
@ -116,6 +121,7 @@ import org.meshtastic.core.resources.map_style_selection
import org.meshtastic.core.resources.map_subDescription
import org.meshtastic.core.resources.map_tile_source
import org.meshtastic.core.resources.only_favorites
import org.meshtastic.core.resources.position
import org.meshtastic.core.resources.show_precision_circle
import org.meshtastic.core.resources.show_waypoints
import org.meshtastic.core.resources.toggle_my_position
@ -134,6 +140,7 @@ import org.meshtastic.feature.map.component.MapButton
import org.meshtastic.feature.map.model.CustomTileSource
import org.meshtastic.feature.map.model.MarkerWithLabel
import org.meshtastic.feature.map.model.TracerouteOverlay
import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits
import org.meshtastic.proto.Position
import org.meshtastic.proto.Waypoint
import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable
@ -166,12 +173,26 @@ import kotlin.math.sin
private fun MapView.updateMarkers(
nodeMarkers: List<MarkerWithLabel>,
waypointMarkers: List<MarkerWithLabel>,
trackMarkers: List<Marker>,
trackPolylines: List<Polyline>,
nodeClusterer: RadiusMarkerClusterer,
) {
Logger.d { "Showing on map: ${nodeMarkers.size} nodes ${waypointMarkers.size} waypoints" }
overlays.removeAll { it is MarkerWithLabel }
// overlays.addAll(nodeMarkers + waypointMarkers)
Logger.d {
"Showing on map: ${nodeMarkers.size} nodes ${waypointMarkers.size} waypoints ${trackMarkers.size} tracks"
}
val trackOverlayIds = (trackMarkers + trackPolylines).toSet()
overlays.removeAll { overlay ->
overlay is MarkerWithLabel ||
(overlay is Marker && overlay !in nodeClusterer.items && overlay !in trackOverlayIds) ||
(overlay is Polyline && overlay !in trackOverlayIds)
}
overlays.addAll(waypointMarkers)
overlays.addAll(trackPolylines)
overlays.addAll(trackMarkers)
nodeClusterer.items.clear()
nodeClusterer.items.addAll(nodeMarkers)
nodeClusterer.invalidate()
@ -246,8 +267,6 @@ fun MapView(
val haptic = LocalHapticFeedback.current
fun performHapticFeedback() = haptic.performHapticFeedback(HapticFeedbackType.LongPress)
val hasGps = remember { context.hasGps() }
// Accompanist permissions state for location
val locationPermissionsState =
rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION))
@ -671,6 +690,51 @@ fun MapView(
}
}
fun MapView.onTracksChanged(nodeTracks: List<Position>?, focusedNodeNum: Int?): Pair<List<Marker>, List<Polyline>> {
if (nodeTracks == null || focusedNodeNum == null) return emptyList<Marker>() to emptyList<Polyline>()
val lastHeardTrackFilter = mapFilterState.lastHeardTrackFilter
val timeFilteredPositions =
nodeTracks.filter {
lastHeardTrackFilter == LastHeardFilter.Any || it.time > nowSeconds - lastHeardTrackFilter.seconds
}
val sortedPositions = timeFilteredPositions.sortedBy { it.time }
val focusedNode = nodes.find { it.num == focusedNodeNum } ?: return emptyList<Marker>() to emptyList<Polyline>()
val color = focusedNode.colors.second
val trackPolylines = mutableListOf<Polyline>()
if (sortedPositions.size > 1) {
val segments = sortedPositions.windowed(size = 2, step = 1, partialWindows = false)
segments.forEachIndexed { index, segmentPoints ->
val alpha = (index.toFloat() / (segments.size.toFloat() - 1))
val polyline =
Polyline().apply {
setPoints(
segmentPoints.map { GeoPoint((it.latitude_i ?: 0) * 1e-7, (it.longitude_i ?: 0) * 1e-7) },
)
outlinePaint.color = Color(color).copy(alpha = alpha).toArgb()
outlinePaint.strokeWidth = 8f
}
trackPolylines.add(polyline)
}
}
val trackMarkers =
sortedPositions.mapIndexedNotNull { index, position ->
if (index == sortedPositions.lastIndex) return@mapIndexedNotNull null
Marker(this).apply {
this.position = GeoPoint((position.latitude_i ?: 0) * 1e-7, (position.longitude_i ?: 0) * 1e-7)
icon = AppCompatResources.getDrawable(context, R.drawable.ic_map_location_dot)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
title = getString(Res.string.position)
snippet = formatAgo(position.time)
}
}
return trackMarkers to trackPolylines
}
Scaffold(
floatingActionButton = {
DownloadButton(showDownloadButton && downloadRegionBoundingBox == null) { showCacheManagerDialog = true }
@ -687,10 +751,13 @@ fun MapView(
modifier = Modifier.fillMaxSize(),
update = { mapView ->
mapView.updateTracerouteOverlay(tracerouteForwardOffsetPoints, tracerouteReturnOffsetPoints)
val (trackMarkers, trackPolylines) = mapView.onTracksChanged(nodeTracks, focusedNodeNum)
with(mapView) {
updateMarkers(
onNodesChanged(nodesForMarkers),
onWaypointChanged(waypoints.values, selectedWaypointId),
trackMarkers,
trackPolylines,
nodeClusterer,
)
}
@ -723,7 +790,7 @@ fun MapView(
MapButton(
onClick = { mapFilterExpanded = true },
icon = Icons.Outlined.Tune,
contentDescription = Res.string.map_filter,
contentDescription = stringResource(Res.string.map_filter),
)
DropdownMenu(
expanded = mapFilterExpanded,
@ -808,22 +875,20 @@ fun MapView(
)
}
}
if (hasGps) {
MapButton(
icon =
if (myLocationOverlay == null) {
Icons.Outlined.MyLocation
} else {
Icons.Rounded.LocationDisabled
},
contentDescription = stringResource(Res.string.toggle_my_position),
) {
if (locationPermissionsState.allPermissionsGranted) {
map.toggleMyLocation()
} else {
triggerLocationToggleAfterPermission = true
locationPermissionsState.launchMultiplePermissionRequest()
}
MapButton(
icon =
if (myLocationOverlay == null) {
Icons.Outlined.MyLocation
} else {
Icons.Rounded.LocationDisabled
},
contentDescription = stringResource(Res.string.toggle_my_position),
) {
if (locationPermissionsState.allPermissionsGranted) {
map.toggleMyLocation()
} else {
triggerLocationToggleAfterPermission = true
locationPermissionsState.launchMultiplePermissionRequest()
}
}
}