refactor(map): fix scalebar and compass (#2973)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich
2025-09-04 22:18:26 -05:00
committed by GitHub
parent 755038219b
commit b06ebd4436
4 changed files with 52 additions and 63 deletions

View File

@@ -116,7 +116,7 @@ import com.google.maps.android.compose.Polyline
import com.google.maps.android.compose.TileOverlay
import com.google.maps.android.compose.rememberCameraPositionState
import com.google.maps.android.compose.rememberUpdatedMarkerState
import com.google.maps.android.compose.widgets.DisappearingScaleBar
import com.google.maps.android.compose.widgets.ScaleBar
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -207,7 +207,6 @@ fun MapView(
val mapFilterState by mapViewModel.mapFilterStateFlow.collectAsStateWithLifecycle()
val ourNodeInfo by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle()
var editingWaypoint by remember { mutableStateOf<Waypoint?>(null) }
val savedCameraPosition by mapViewModel.cameraPosition.collectAsStateWithLifecycle()
val selectedGoogleMapType by mapViewModel.selectedGoogleMapType.collectAsStateWithLifecycle()
val currentCustomTileProviderUrl by mapViewModel.selectedCustomTileProviderUrl.collectAsStateWithLifecycle()
@@ -215,13 +214,7 @@ fun MapView(
var mapTypeMenuExpanded by remember { mutableStateOf(false) }
var showCustomTileManagerSheet by remember { mutableStateOf(false) }
val defaultLatLng = LatLng(0.0, 0.0)
val cameraPositionState = rememberCameraPositionState {
position =
savedCameraPosition?.let {
CameraPosition(LatLng(it.targetLat, it.targetLng), it.zoom, it.tilt, it.bearing)
} ?: CameraPosition.fromLatLngZoom(defaultLatLng, 7f)
}
val cameraPositionState = rememberCameraPositionState {}
// Location tracking functionality
val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) }
@@ -339,7 +332,6 @@ fun MapView(
}
var showClusterItemsDialog by remember { mutableStateOf<List<NodeClusterItem>?>(null) }
LaunchedEffect(cameraPositionState.position) { mapViewModel.onCameraPositionChanged(cameraPositionState.position) }
Scaffold { paddingValues ->
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
@@ -351,7 +343,7 @@ fun MapView(
MapUiSettings(
zoomControlsEnabled = true,
mapToolbarEnabled = true,
compassEnabled = true,
compassEnabled = false,
myLocationButtonEnabled = false, // Disabled - we use custom location button
rotationGesturesEnabled = true,
scrollGesturesEnabled = true,
@@ -370,32 +362,27 @@ fun MapView(
}
},
onMapLoaded = {
if (
savedCameraPosition?.targetLat == defaultLatLng.latitude &&
savedCameraPosition?.targetLng == defaultLatLng.longitude
) {
val pointsToBound: List<LatLng> =
when {
!nodeTrack.isNullOrEmpty() -> nodeTrack.map { it.toLatLng() }
val pointsToBound: List<LatLng> =
when {
!nodeTrack.isNullOrEmpty() -> nodeTrack.map { it.toLatLng() }
allNodes.isNotEmpty() || displayableWaypoints.isNotEmpty() ->
allNodes.mapNotNull { it.toLatLng() } + displayableWaypoints.map { it.toLatLng() }
allNodes.isNotEmpty() || displayableWaypoints.isNotEmpty() ->
allNodes.mapNotNull { it.toLatLng() } + displayableWaypoints.map { it.toLatLng() }
else -> emptyList()
}
if (pointsToBound.isNotEmpty()) {
val bounds = LatLngBounds.builder().apply { pointsToBound.forEach(::include) }.build()
val padding = if (!pointsToBound.isEmpty()) 100 else 48
try {
coroutineScope.launch {
cameraPositionState.animate(CameraUpdateFactory.newLatLngBounds(bounds, padding))
}
} catch (e: IllegalStateException) {
warn("MapView Could not animate to bounds: ${e.message}")
else -> emptyList()
}
if (pointsToBound.isNotEmpty()) {
val bounds = LatLngBounds.builder().apply { pointsToBound.forEach(::include) }.build()
val padding = if (!pointsToBound.isEmpty()) 100 else 48
try {
coroutineScope.launch {
cameraPositionState.animate(CameraUpdateFactory.newLatLngBounds(bounds, padding))
}
} catch (e: IllegalStateException) {
warn("MapView Could not animate to bounds: ${e.message}")
}
}
},
@@ -537,12 +524,10 @@ fun MapView(
}
}
val currentCameraPosition = cameraPositionState.position
var displayedZoom by remember { mutableStateOf(currentCameraPosition.zoom) }
if (displayedZoom != 0f) {
DisappearingScaleBar(cameraPositionState = cameraPositionState)
}
ScaleBar(
cameraPositionState = cameraPositionState,
modifier = Modifier.align(Alignment.BottomStart).padding(bottom = 48.dp),
)
editingWaypoint?.let { waypointToEdit ->
EditWaypointDialog(
waypoint = waypointToEdit,
@@ -613,6 +598,7 @@ fun MapView(
isLocationTrackingEnabled = !isLocationTrackingEnabled
}
},
bearing = cameraPositionState.position.bearing,
onOrientNorth = {
coroutineScope.launch {
try {

View File

@@ -30,7 +30,6 @@ import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.map.CustomTileProviderRepository
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.TileProvider
import com.google.android.gms.maps.model.UrlTileProvider
import com.google.maps.android.compose.MapType
@@ -101,10 +100,6 @@ constructor(
private val _selectedGoogleMapType = MutableStateFlow(MapType.NORMAL)
val selectedGoogleMapType: StateFlow<MapType> = _selectedGoogleMapType.asStateFlow()
private val _cameraPosition = MutableStateFlow<MapCameraPosition?>(null)
val cameraPosition: StateFlow<MapCameraPosition?> = _cameraPosition.asStateFlow()
val displayUnits =
radioConfigRepository.deviceProfileFlow
.mapNotNull { it.config.display.units }
@@ -114,17 +109,6 @@ constructor(
initialValue = ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC,
)
fun onCameraPositionChanged(cameraPosition: CameraPosition) {
_cameraPosition.value =
MapCameraPosition(
targetLat = cameraPosition.target.latitude,
targetLng = cameraPosition.target.longitude,
zoom = cameraPosition.zoom,
tilt = cameraPosition.tilt,
bearing = cameraPosition.bearing,
)
}
fun addCustomTileProvider(name: String, urlTemplate: String) {
viewModelScope.launch {
if (name.isBlank() || urlTemplate.isBlank() || !isValidTileUrlTemplate(urlTemplate)) {

View File

@@ -19,13 +19,25 @@ package com.geeksville.mesh.ui.map.components
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
@Composable
fun MapButton(icon: ImageVector, contentDescription: String, modifier: Modifier = Modifier, onClick: () -> Unit) {
fun MapButton(
modifier: Modifier = Modifier,
icon: ImageVector,
iconTint: Color? = null,
contentDescription: String,
onClick: () -> Unit,
) {
FilledIconButton(onClick = onClick, modifier = modifier) {
Icon(imageVector = icon, contentDescription = contentDescription)
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = iconTint ?: IconButtonDefaults.filledIconButtonColors().contentColor,
)
}
}

View File

@@ -20,17 +20,20 @@ package com.geeksville.mesh.ui.map.components
import androidx.compose.foundation.layout.Box
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationDisabled
import androidx.compose.material.icons.outlined.Explore
import androidx.compose.material.icons.outlined.Layers
import androidx.compose.material.icons.outlined.Map
import androidx.compose.material.icons.outlined.MyLocation
import androidx.compose.material.icons.outlined.Navigation
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.HorizontalFloatingToolbar
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.stringResource
import com.geeksville.mesh.R
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.map.MapViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -51,6 +54,7 @@ fun MapControlsOverlay(
hasLocationPermission: Boolean = false,
isLocationTrackingEnabled: Boolean = false,
onToggleLocationTracking: () -> Unit = {},
bearing: Float = 0f,
onOrientNorth: () -> Unit = {},
) {
HorizontalFloatingToolbar(
@@ -59,6 +63,7 @@ fun MapControlsOverlay(
leadingContent = {},
trailingContent = {},
content = {
CompassButton(onOrientNorth = onOrientNorth, bearing = bearing)
if (showFilterButton) {
Box {
MapButton(
@@ -94,8 +99,6 @@ fun MapControlsOverlay(
onClick = onManageLayersClicked,
)
CompassButton(onOrientNorth = onOrientNorth)
// Location tracking button
if (hasLocationPermission) {
MapButton(
@@ -114,9 +117,13 @@ fun MapControlsOverlay(
}
@Composable
private fun CompassButton(onOrientNorth: () -> Unit) {
private fun CompassButton(onOrientNorth: () -> Unit, bearing: Float) {
val icon = Icons.Outlined.Navigation
MapButton(
icon = Icons.Outlined.Explore,
modifier = Modifier.rotate(-bearing),
icon = icon,
iconTint = MaterialTheme.colorScheme.StatusRed.takeIf { bearing == 0f },
contentDescription = stringResource(id = R.string.orient_north),
onClick = onOrientNorth,
)