feat(map): replace Google Maps + OSMDroid with unified MapLibre Compose Multiplatform

Replace the dual flavor-specific map implementations (Google Maps for google,
OSMDroid for fdroid) with a single MapLibre Compose Multiplatform implementation
in feature:map/commonMain, eliminating ~8,500 lines of duplicated code.

Key changes:
- Add maplibre-compose v0.12.1 dependency (KMP: Android, Desktop, iOS)
- Create unified MapViewModel with camera persistence via MapCameraPrefs
- Create MapScreen, MaplibreMapContent, NodeTrackLayers, TracerouteLayers,
  InlineMap, NodeTrackMap, TracerouteMap, NodeMapScreen in commonMain
- Create MapStyle enum with predefined OpenFreeMap tile styles
- Create GeoJsonConverters for Node/Waypoint/Position to GeoJSON
- Move TracerouteMapScreen from feature:node/androidMain to commonMain
- Wire navigation to use direct imports instead of CompositionLocal providers
- Delete 61 flavor-specific map files (google + fdroid source sets)
- Remove 8 CompositionLocal map providers from core:ui
- Remove SharedMapViewModel (replaced by new MapViewModel)
- Remove dead google-maps and osmdroid entries from version catalog
- Add MapViewModelTest with 10 test cases in commonTest

Baseline verified: spotlessCheck, detekt, assembleGoogleDebug, allTests all pass.
This commit is contained in:
James Rich 2026-04-12 18:25:15 -05:00
parent a2763bdfeb
commit 598cae564e
86 changed files with 1653 additions and 8333 deletions

View file

@ -43,7 +43,7 @@ import org.meshtastic.core.resources.open_compass
import org.meshtastic.core.ui.icon.Compass
import org.meshtastic.core.ui.icon.Distance
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.util.LocalInlineMapProvider
import org.meshtastic.feature.map.component.InlineMap
import org.meshtastic.feature.node.model.NodeDetailAction
import org.meshtastic.proto.Config
@ -85,7 +85,7 @@ internal fun PositionInlineContent(
private fun PositionMap(node: Node, distance: String?) {
Box(modifier = Modifier.padding(vertical = 4.dp)) {
Surface(shape = MaterialTheme.shapes.large, modifier = Modifier.fillMaxWidth().height(MAP_HEIGHT_DP.dp)) {
LocalInlineMapProvider.current(node, Modifier.fillMaxSize())
InlineMap(node, Modifier.fillMaxSize())
}
if (distance != null && distance.isNotEmpty()) {
Surface(

View file

@ -31,8 +31,8 @@ import org.meshtastic.core.resources.position_log
import org.meshtastic.core.ui.icon.Delete
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.util.LocalNodeTrackMapProvider
import org.meshtastic.core.ui.util.rememberSaveFileLauncher
import org.meshtastic.feature.map.component.NodeTrackMap
@Composable
fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
@ -41,9 +41,6 @@ fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
val exportPositionLauncher = rememberSaveFileLauncher { uri -> viewModel.savePositionCSV(uri, positions) }
val trackMap = LocalNodeTrackMapProvider.current
val destNum = state.node?.num ?: 0
BaseMetricScreen(
onNavigateUp = onNavigateUp,
telemetryType = null,
@ -66,7 +63,12 @@ fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
},
chartPart = { modifier, selectedX, _, onPointSelected ->
val selectedTime = selectedX?.toInt()
trackMap(destNum, positions, modifier, selectedTime) { time -> onPointSelected(time.toDouble()) }
NodeTrackMap(
positions = positions,
modifier = modifier,
selectedPositionTime = selectedTime,
onPositionSelected = { time -> onPointSelected(time.toDouble()) },
)
},
listPart = { modifier, selectedX, lazyListState, onCardClick ->
LazyColumn(modifier = modifier.fillMaxSize(), state = lazyListState) {

View file

@ -52,8 +52,7 @@ import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Route
import org.meshtastic.core.ui.theme.TracerouteColors
import org.meshtastic.core.ui.util.LocalTracerouteMapOverlayInsetsProvider
import org.meshtastic.core.ui.util.LocalTracerouteMapProvider
import org.meshtastic.feature.map.component.TracerouteMap
import org.meshtastic.proto.Position
@Composable
@ -102,7 +101,6 @@ private fun TracerouteMapScaffold(
) {
var tracerouteNodesShown by remember { mutableStateOf(0) }
var tracerouteNodesTotal by remember { mutableStateOf(0) }
val insets = LocalTracerouteMapOverlayInsetsProvider.current
Scaffold(
topBar = {
MainAppBar(
@ -117,18 +115,18 @@ private fun TracerouteMapScaffold(
},
) { paddingValues ->
Box(modifier = modifier.fillMaxSize().padding(paddingValues)) {
LocalTracerouteMapProvider.current(
overlay,
snapshotPositions,
{ shown: Int, total: Int ->
TracerouteMap(
tracerouteOverlay = overlay,
tracerouteNodePositions = snapshotPositions,
onMappableCountChanged = { shown: Int, total: Int ->
tracerouteNodesShown = shown
tracerouteNodesTotal = total
},
Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
)
Column(
modifier = Modifier.align(insets.overlayAlignment).padding(insets.overlayPadding),
horizontalAlignment = insets.contentHorizontalAlignment,
modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
TracerouteNodeCount(shown = tracerouteNodesShown, total = tracerouteNodesTotal)

View file

@ -66,6 +66,7 @@ import org.meshtastic.feature.node.metrics.PositionLogScreen
import org.meshtastic.feature.node.metrics.PowerMetricsScreen
import org.meshtastic.feature.node.metrics.SignalMetricsScreen
import org.meshtastic.feature.node.metrics.TracerouteLogScreen
import org.meshtastic.feature.node.metrics.TracerouteMapScreen
import kotlin.reflect.KClass
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@ -143,8 +144,14 @@ fun EntryProviderScope<NavKey>.nodeDetailGraph(
}
entry<NodeDetailRoute.TracerouteMap>(metadata = { ListDetailSceneStrategy.extraPane() }) { args ->
val tracerouteMapScreen = org.meshtastic.core.ui.util.LocalTracerouteMapScreenProvider.current
tracerouteMapScreen(args.destNum, args.requestId, args.logUuid) { backStack.removeLastOrNull() }
val metricsViewModel = koinViewModel<MetricsViewModel> { parametersOf(args.destNum) }
metricsViewModel.setNodeId(args.destNum)
TracerouteMapScreen(
metricsViewModel = metricsViewModel,
requestId = args.requestId,
logUuid = args.logUuid,
onNavigateUp = { backStack.removeLastOrNull() },
)
}
NodeDetailScreen.entries.forEach { routeInfo ->