From 51fa634e11759a620667fd133991869e8b8ec9b1 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Fri, 3 Oct 2025 20:19:37 -0400 Subject: [PATCH] More map modularization (#3319) --- app/detekt-baseline.xml | 1 - .../geeksville/mesh/ui/node/NodeMapScreen.kt | 4 +- .../geeksville/mesh/ui/node/NodeMapScreen.kt | 6 +- .../mesh/android/ContextExtensions.kt | 35 ----------- .../mesh/navigation/MapNavigation.kt | 2 +- .../bluetooth/BluetoothRepository.kt | 2 +- .../geeksville/mesh/service/MeshService.kt | 2 +- .../geeksville/mesh/ui/metrics/PositionLog.kt | 19 +----- .../mesh/ui/settings/SettingsScreen.kt | 2 +- core/common/build.gradle.kts | 2 +- .../core/common}/ContextServices.kt | 2 +- core/proto/build.gradle.kts | 3 + .../meshtastic/core/proto/ProtoExtensions.kt | 40 ++++++++++++ .../drawable/ic_baseline_location_on_24.xml | 0 .../res/drawable/ic_map_location_dot_24.xml | 0 feature/map/build.gradle.kts | 1 + .../org/meshtastic/feature}/map/MapView.kt | 25 +++----- .../feature}/map/MapViewWithLifecycle.kt | 5 +- .../feature/map}/SqlTileWriterExt.kt | 2 +- .../map/component}/EditWaypointDialog.kt | 6 +- .../org/meshtastic/feature}/map/MapView.kt | 62 ++++--------------- .../map/component}/ClusterItemsListDialog.kt | 4 +- .../map/component}/EditWaypointDialog.kt | 2 +- .../map/component}/NodeClusterMarkers.kt | 4 +- .../feature/map/component}/WaypointMarkers.kt | 5 +- .../feature/map/model/NodeClusterItem.kt | 50 +++++++++++++++ .../org/meshtastic/feature}/map/MapScreen.kt | 3 +- .../feature/map/node}/NodeMapViewModel.kt | 2 +- 28 files changed, 145 insertions(+), 146 deletions(-) delete mode 100644 app/src/main/java/com/geeksville/mesh/android/ContextExtensions.kt rename {app/src/main/java/com/geeksville/mesh/android => core/common/src/main/kotlin/org/meshtastic/core/common}/ContextServices.kt (98%) create mode 100644 core/proto/src/main/kotlin/org/meshtastic/core/proto/ProtoExtensions.kt rename {app => core/ui}/src/main/res/drawable/ic_baseline_location_on_24.xml (100%) rename {app => core/ui}/src/main/res/drawable/ic_map_location_dot_24.xml (100%) rename {app/src/fdroid/java/com/geeksville/mesh/ui => feature/map/src/fdroid/kotlin/org/meshtastic/feature}/map/MapView.kt (97%) rename {app/src/fdroid/java/com/geeksville/mesh/ui => feature/map/src/fdroid/kotlin/org/meshtastic/feature}/map/MapViewWithLifecycle.kt (97%) rename {app/src/fdroid/java/com/geeksville/mesh/util => feature/map/src/fdroid/kotlin/org/meshtastic/feature/map}/SqlTileWriterExt.kt (99%) rename {app/src/fdroid/java/com/geeksville/mesh/ui/map/components => feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component}/EditWaypointDialog.kt (99%) rename {app/src/google/java/com/geeksville/mesh/ui => feature/map/src/google/kotlin/org/meshtastic/feature}/map/MapView.kt (94%) rename {app/src/google/java/com/geeksville/mesh/ui/map/components => feature/map/src/google/kotlin/org/meshtastic/feature/map/component}/ClusterItemsListDialog.kt (96%) rename {app/src/google/java/com/geeksville/mesh/ui/map/components => feature/map/src/google/kotlin/org/meshtastic/feature/map/component}/EditWaypointDialog.kt (99%) rename {app/src/google/java/com/geeksville/mesh/ui/map/components => feature/map/src/google/kotlin/org/meshtastic/feature/map/component}/NodeClusterMarkers.kt (96%) rename {app/src/google/java/com/geeksville/mesh/ui/map/components => feature/map/src/google/kotlin/org/meshtastic/feature/map/component}/WaypointMarkers.kt (96%) create mode 100644 feature/map/src/google/kotlin/org/meshtastic/feature/map/model/NodeClusterItem.kt rename {app/src/main/java/com/geeksville/mesh/ui => feature/map/src/main/kotlin/org/meshtastic/feature}/map/MapScreen.kt (96%) rename {app/src/main/java/com/geeksville/mesh/ui/map => feature/map/src/main/kotlin/org/meshtastic/feature/map/node}/NodeMapViewModel.kt (96%) diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 3a36977bd..9898c03c4 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -331,7 +331,6 @@ UnusedParameter:ChannelSettingsItemList.kt$onBack: () -> Unit UnusedParameter:ChannelSettingsItemList.kt$title: String UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule - ViewModelForwarding:Main.kt$ScannedQrCodeDialog(uIViewModel, newChannelSet) ViewModelForwarding:Main.kt$VersionChecks(uIViewModel) ViewModelInjection:DebugSearch.kt$viewModel Wrapping:Message.kt${ event -> when (event) { is MessageScreenEvent.SendMessage -> { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -> viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -> { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -> viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.NodeDetails -> navigateToNodeDetails(event.node.num) is MessageScreenEvent.SetTitle -> viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -> navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -> navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -> onNavigateBack() is MessageScreenEvent.CopyToClipboard -> { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } } diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt b/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt index abb6ca82c..a78638a43 100644 --- a/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt +++ b/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt @@ -27,12 +27,12 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.model.MetricsViewModel -import com.geeksville.mesh.ui.map.NodeMapViewModel -import com.geeksville.mesh.ui.map.rememberMapViewWithLifecycle import org.meshtastic.feature.map.addCopyright import org.meshtastic.feature.map.addPolyline import org.meshtastic.feature.map.addPositionMarkers import org.meshtastic.feature.map.addScaleBarOverlay +import org.meshtastic.feature.map.node.NodeMapViewModel +import org.meshtastic.feature.map.rememberMapViewWithLifecycle import org.osmdroid.util.BoundingBox import org.osmdroid.util.GeoPoint diff --git a/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt b/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt index 52a0c7447..b420d276e 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt @@ -27,11 +27,9 @@ import androidx.compose.ui.Modifier import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.model.MetricsViewModel -import com.geeksville.mesh.ui.map.MapView -import com.geeksville.mesh.ui.map.NodeMapViewModel import org.meshtastic.core.ui.component.MainAppBar - -const val DEG_D = 1e-7 +import org.meshtastic.feature.map.MapView +import org.meshtastic.feature.map.node.NodeMapViewModel @Composable fun NodeMapScreen( diff --git a/app/src/main/java/com/geeksville/mesh/android/ContextExtensions.kt b/app/src/main/java/com/geeksville/mesh/android/ContextExtensions.kt deleted file mode 100644 index b2de39a7e..000000000 --- a/app/src/main/java/com/geeksville/mesh/android/ContextExtensions.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.geeksville.mesh.android - -import android.app.Activity -import android.content.Context -import android.view.inputmethod.InputMethodManager -import android.widget.Toast - -// / show a toast -fun Context.toast(message: CharSequence) = Toast.makeText(this, message, Toast.LENGTH_SHORT).show() - -// / Utility function to hide the soft keyboard per stack overflow -fun Activity.hideKeyboard() { - // Check if no view has focus: - currentFocus?.let { v -> - val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.hideSoftInputFromWindow(v.windowToken, 0) - } -} diff --git a/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt index 774350619..5de1c6933 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt @@ -21,10 +21,10 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.navDeepLink -import com.geeksville.mesh.ui.map.MapScreen import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI import org.meshtastic.core.navigation.MapRoutes import org.meshtastic.core.navigation.NodesRoutes +import org.meshtastic.feature.map.MapScreen fun NavGraphBuilder.mapGraph(navController: NavHostController) { composable(deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/map"))) { diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt index fa8272547..58756fa38 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt @@ -28,7 +28,6 @@ import androidx.annotation.RequiresPermission import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope import com.geeksville.mesh.CoroutineDispatchers -import com.geeksville.mesh.android.hasBluetoothPermission import com.geeksville.mesh.util.registerReceiverCompat import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -37,6 +36,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch +import org.meshtastic.core.common.hasBluetoothPermission import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 8555d1c30..bdf31fbfa 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -49,7 +49,6 @@ import com.geeksville.mesh.StoreAndForwardProtos import com.geeksville.mesh.TelemetryProtos import com.geeksville.mesh.TelemetryProtos.LocalStats import com.geeksville.mesh.XmodemProtos -import com.geeksville.mesh.android.hasLocationPermission import com.geeksville.mesh.concurrent.handledLaunch import com.geeksville.mesh.copy import com.geeksville.mesh.fromRadio @@ -78,6 +77,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.meshtastic.core.analytics.DataPair +import org.meshtastic.core.common.hasLocationPermission import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.PacketRepository diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt index ca08831c5..10a4bf3d2 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt @@ -67,11 +67,11 @@ import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.model.MetricsViewModel import org.meshtastic.core.model.util.metersIn import org.meshtastic.core.model.util.toString +import org.meshtastic.core.proto.formatPositionTime import org.meshtastic.core.strings.R import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.core.ui.theme.AppTheme import java.text.DateFormat -import kotlin.time.Duration.Companion.days @Composable private fun RowScope.PositionText(text: String, weight: Float) { @@ -106,7 +106,6 @@ private fun HeaderItem(compactWidth: Boolean) { const val DEG_D = 1e-7 const val HEADING_DEG = 1e-5 -private const val SECONDS_TO_MILLIS = 1000L @Composable fun PositionItem(compactWidth: Boolean, position: MeshProtos.Position, dateFormat: DateFormat, system: DisplayUnits) { @@ -122,24 +121,10 @@ fun PositionItem(compactWidth: Boolean, position: MeshProtos.Position, dateForma PositionText("${position.groundSpeed} Km/h", WEIGHT_15) PositionText("%.0f°".format(position.groundTrack * HEADING_DEG), WEIGHT_15) } - PositionText(formatPositionTime(position, dateFormat), WEIGHT_40) + PositionText(position.formatPositionTime(dateFormat), WEIGHT_40) } } -@Composable -fun formatPositionTime(position: MeshProtos.Position, dateFormat: DateFormat): String { - val currentTime = System.currentTimeMillis() - val sixMonthsAgo = currentTime - 180.days.inWholeMilliseconds - val isOlderThanSixMonths = position.time * SECONDS_TO_MILLIS < sixMonthsAgo - val timeText = - if (isOlderThanSixMonths) { - stringResource(id = R.string.unknown_age) - } else { - dateFormat.format(position.time * SECONDS_TO_MILLIS) - } - return timeText -} - @Composable private fun ActionButtons( clearButtonEnabled: Boolean, diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt index 17c9c489b..3950ecb74 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt @@ -60,7 +60,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.BuildConfig import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile -import com.geeksville.mesh.android.gpsDisabled import com.geeksville.mesh.navigation.getNavRouteFrom import com.geeksville.mesh.ui.settings.radio.RadioConfigItemList import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel @@ -71,6 +70,7 @@ import com.geeksville.mesh.util.LanguageUtils.getLanguageMap import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState import kotlinx.coroutines.delay +import org.meshtastic.core.common.gpsDisabled import org.meshtastic.core.navigation.Route import org.meshtastic.core.strings.R import org.meshtastic.core.ui.component.MainAppBar diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 43363fe44..73a41424b 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -22,4 +22,4 @@ plugins { android { namespace = "org.meshtastic.core.common" } -dependencies {} +dependencies { implementation(libs.core.ktx) } diff --git a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt b/core/common/src/main/kotlin/org/meshtastic/core/common/ContextServices.kt similarity index 98% rename from app/src/main/java/com/geeksville/mesh/android/ContextServices.kt rename to core/common/src/main/kotlin/org/meshtastic/core/common/ContextServices.kt index 818c23a8e..51ed91048 100644 --- a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt +++ b/core/common/src/main/kotlin/org/meshtastic/core/common/ContextServices.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.geeksville.mesh.android +package org.meshtastic.core.common import android.Manifest import android.content.Context diff --git a/core/proto/build.gradle.kts b/core/proto/build.gradle.kts index e914679f3..10bc12232 100644 --- a/core/proto/build.gradle.kts +++ b/core/proto/build.gradle.kts @@ -34,6 +34,7 @@ plugins { alias(libs.plugins.meshtastic.android.library) + alias(libs.plugins.meshtastic.android.library.compose) alias(libs.plugins.protobuf) } @@ -53,6 +54,8 @@ protobuf { } dependencies { + implementation(projects.core.strings) + // This needs to be API for consuming modules api(libs.protobuf.kotlin) } diff --git a/core/proto/src/main/kotlin/org/meshtastic/core/proto/ProtoExtensions.kt b/core/proto/src/main/kotlin/org/meshtastic/core/proto/ProtoExtensions.kt new file mode 100644 index 000000000..5b6d9bab6 --- /dev/null +++ b/core/proto/src/main/kotlin/org/meshtastic/core/proto/ProtoExtensions.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.meshtastic.core.proto + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.geeksville.mesh.MeshProtos +import java.text.DateFormat +import kotlin.time.Duration.Companion.days + +private const val SECONDS_TO_MILLIS = 1000L + +@Composable +fun MeshProtos.Position.formatPositionTime(dateFormat: DateFormat): String { + val currentTime = System.currentTimeMillis() + val sixMonthsAgo = currentTime - 180.days.inWholeMilliseconds + val isOlderThanSixMonths = time * SECONDS_TO_MILLIS < sixMonthsAgo + val timeText = + if (isOlderThanSixMonths) { + stringResource(id = org.meshtastic.core.strings.R.string.unknown_age) + } else { + dateFormat.format(time * SECONDS_TO_MILLIS) + } + return timeText +} diff --git a/app/src/main/res/drawable/ic_baseline_location_on_24.xml b/core/ui/src/main/res/drawable/ic_baseline_location_on_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_baseline_location_on_24.xml rename to core/ui/src/main/res/drawable/ic_baseline_location_on_24.xml diff --git a/app/src/main/res/drawable/ic_map_location_dot_24.xml b/core/ui/src/main/res/drawable/ic_map_location_dot_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_map_location_dot_24.xml rename to core/ui/src/main/res/drawable/ic_map_location_dot_24.xml diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts index b0c04419d..cba6a7b29 100644 --- a/feature/map/build.gradle.kts +++ b/feature/map/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation(libs.bundles.osm) googleImplementation(libs.bundles.maps.compose) + implementation(libs.accompanist.permissions) implementation(libs.annotation) implementation(libs.timber) } diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapView.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt similarity index 97% rename from app/src/fdroid/java/com/geeksville/mesh/ui/map/MapView.kt rename to feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt index 27d0c79d6..2b3874feb 100644 --- a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapView.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.geeksville.mesh.ui.map +package org.meshtastic.feature.map import android.Manifest // Added for Accompanist import android.content.Context @@ -63,31 +63,25 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.MeshProtos.Waypoint -import com.geeksville.mesh.android.gpsDisabled -import com.geeksville.mesh.android.hasGps import com.geeksville.mesh.copy -import com.geeksville.mesh.ui.map.components.EditWaypointDialog -import com.geeksville.mesh.util.SqlTileWriterExt import com.geeksville.mesh.waypoint import com.google.accompanist.permissions.ExperimentalPermissionsApi // Added for Accompanist import com.google.accompanist.permissions.rememberMultiplePermissionsState // Added for Accompanist import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.meshtastic.core.common.gpsDisabled +import org.meshtastic.core.common.hasGps 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.formatAgo import org.meshtastic.core.strings.R -import org.meshtastic.feature.map.MapViewModel -import org.meshtastic.feature.map.addCopyright -import org.meshtastic.feature.map.addScaleBarOverlay import org.meshtastic.feature.map.cluster.RadiusMarkerClusterer import org.meshtastic.feature.map.component.CacheLayout import org.meshtastic.feature.map.component.DownloadButton +import org.meshtastic.feature.map.component.EditWaypointDialog import org.meshtastic.feature.map.component.MapButton -import org.meshtastic.feature.map.createLatLongGrid import org.meshtastic.feature.map.model.CustomTileSource import org.meshtastic.feature.map.model.MarkerWithLabel -import org.meshtastic.feature.map.zoomIn import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable import org.osmdroid.config.Configuration import org.osmdroid.events.MapEventsReceiver @@ -277,10 +271,11 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails: MyLocationNewOverlay(this).apply { enableMyLocation() enableFollowLocation() - getBitmapFromVectorDrawable(context, com.geeksville.mesh.R.drawable.ic_map_location_dot_24)?.let { - setPersonIcon(it) - setPersonAnchor(0.5f, 0.5f) - } + getBitmapFromVectorDrawable(context, org.meshtastic.core.ui.R.drawable.ic_map_location_dot_24) + ?.let { + setPersonIcon(it) + setPersonAnchor(0.5f, 0.5f) + } getBitmapFromVectorDrawable(context, org.meshtastic.core.ui.R.drawable.ic_map_navigation_24)?.let { setDirectionIcon(it) setDirectionAnchor(0.5f, 0.5f) @@ -309,7 +304,7 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails: val waypoints by mapViewModel.waypoints.collectAsStateWithLifecycle(emptyMap()) val markerIcon = remember { - AppCompatResources.getDrawable(context, com.geeksville.mesh.R.drawable.ic_baseline_location_on_24) + AppCompatResources.getDrawable(context, org.meshtastic.core.ui.R.drawable.ic_baseline_location_on_24) } fun MapView.onNodesChanged(nodes: Collection): List { diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewWithLifecycle.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewWithLifecycle.kt similarity index 97% rename from app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewWithLifecycle.kt rename to feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewWithLifecycle.kt index bdfe8c019..090ae1b98 100644 --- a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewWithLifecycle.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewWithLifecycle.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.geeksville.mesh.ui.map +package org.meshtastic.feature.map import android.annotation.SuppressLint import android.content.Context @@ -33,7 +33,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner -import org.meshtastic.feature.map.requiredZoomLevel import org.osmdroid.config.Configuration import org.osmdroid.tileprovider.tilesource.ITileSource import org.osmdroid.tileprovider.tilesource.TileSourceFactory @@ -72,7 +71,7 @@ private const val DEFAULT_ZOOM_LEVEL = 15.0 @Suppress("MagicNumber") @Composable -internal fun rememberMapViewWithLifecycle( +fun rememberMapViewWithLifecycle( applicationId: String, box: BoundingBox, tileSource: ITileSource = TileSourceFactory.DEFAULT_TILE_SOURCE, diff --git a/app/src/fdroid/java/com/geeksville/mesh/util/SqlTileWriterExt.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/SqlTileWriterExt.kt similarity index 99% rename from app/src/fdroid/java/com/geeksville/mesh/util/SqlTileWriterExt.kt rename to feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/SqlTileWriterExt.kt index 896359d61..e5999e2e1 100644 --- a/app/src/fdroid/java/com/geeksville/mesh/util/SqlTileWriterExt.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/SqlTileWriterExt.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.geeksville.mesh.util +package org.meshtastic.feature.map import android.database.Cursor import org.osmdroid.tileprovider.modules.DatabaseFileArchive diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/map/components/EditWaypointDialog.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt similarity index 99% rename from app/src/fdroid/java/com/geeksville/mesh/ui/map/components/EditWaypointDialog.kt rename to feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt index 227be8a9c..7ce69740b 100644 --- a/app/src/fdroid/java/com/geeksville/mesh/ui/map/components/EditWaypointDialog.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt @@ -15,9 +15,11 @@ * along with this program. If not, see . */ -package com.geeksville.mesh.ui.map.components +package org.meshtastic.feature.map.component import android.app.DatePickerDialog +import android.app.TimePickerDialog +import android.text.format.DateFormat import android.widget.DatePicker import android.widget.TimePicker import androidx.compose.foundation.Image @@ -76,7 +78,7 @@ import java.util.Locale @Suppress("LongMethod") @OptIn(ExperimentalLayoutApi::class) @Composable -internal fun EditWaypointDialog( +fun EditWaypointDialog( waypoint: Waypoint, onSendClicked: (Waypoint) -> Unit, onDeleteClicked: (Waypoint) -> Unit, diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt similarity index 94% rename from app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt rename to feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt index d8ad936e8..246360fe8 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt @@ -17,7 +17,7 @@ @file:Suppress("MagicNumber") -package com.geeksville.mesh.ui.map +package org.meshtastic.feature.map import android.app.Activity import android.content.Intent @@ -67,13 +67,6 @@ import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits import com.geeksville.mesh.MeshProtos.Position import com.geeksville.mesh.MeshProtos.Waypoint import com.geeksville.mesh.copy -import com.geeksville.mesh.ui.map.components.ClusterItemsListDialog -import com.geeksville.mesh.ui.map.components.EditWaypointDialog -import com.geeksville.mesh.ui.map.components.NodeClusterMarkers -import com.geeksville.mesh.ui.map.components.WaypointMarkers -import com.geeksville.mesh.ui.metrics.HEADING_DEG -import com.geeksville.mesh.ui.metrics.formatPositionTime -import com.geeksville.mesh.ui.node.DEG_D import com.geeksville.mesh.waypoint import com.google.android.gms.location.LocationCallback import com.google.android.gms.location.LocationRequest @@ -87,7 +80,6 @@ import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.JointType import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds -import com.google.maps.android.clustering.ClusterItem import com.google.maps.android.compose.ComposeMapColorScheme import com.google.maps.android.compose.GoogleMap import com.google.maps.android.compose.MapEffect @@ -110,19 +102,23 @@ import org.meshtastic.core.model.util.metersIn import org.meshtastic.core.model.util.mpsToKmph import org.meshtastic.core.model.util.mpsToMph import org.meshtastic.core.model.util.toString +import org.meshtastic.core.proto.formatPositionTime import org.meshtastic.core.strings.R import org.meshtastic.core.ui.component.NodeChip -import org.meshtastic.feature.map.LastHeardFilter -import org.meshtastic.feature.map.LayerType -import org.meshtastic.feature.map.LocationPermissionsHandler -import org.meshtastic.feature.map.MapViewModel +import org.meshtastic.feature.map.component.ClusterItemsListDialog import org.meshtastic.feature.map.component.CustomMapLayersSheet import org.meshtastic.feature.map.component.CustomTileProviderManagerSheet +import org.meshtastic.feature.map.component.EditWaypointDialog import org.meshtastic.feature.map.component.MapControlsOverlay +import org.meshtastic.feature.map.component.NodeClusterMarkers +import org.meshtastic.feature.map.component.WaypointMarkers +import org.meshtastic.feature.map.model.NodeClusterItem import timber.log.Timber import java.text.DateFormat private const val MIN_TRACK_POINT_DISTANCE_METERS = 20f +private const val DEG_D = 1e-7 +private const val HEADING_DEG = 1e-5 @Suppress("CyclomaticComplexMethod", "LongMethod") @OptIn(MapsComposeExperimentalApi::class, ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -651,34 +647,6 @@ fun Uri.getFileName(context: android.content.Context): String { return name } -data class NodeClusterItem(val node: Node, val nodePosition: LatLng, val nodeTitle: String, val nodeSnippet: String) : - ClusterItem { - override fun getPosition(): LatLng = nodePosition - - override fun getTitle(): String = nodeTitle - - override fun getSnippet(): String = nodeSnippet - - override fun getZIndex(): Float? = null - - fun getPrecisionMeters(): Double? { - val precisionMap = - mapOf( - 10 to 23345.484932, - 11 to 11672.7369, - 12 to 5836.36288, - 13 to 2918.175876, - 14 to 1459.0823719999053, - 15 to 729.53562, - 16 to 364.7622, - 17 to 182.375556, - 18 to 91.182212, - 19 to 45.58554, - ) - return precisionMap[this.node.position.precisionBits] - } -} - @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable @Suppress("LongMethod") @@ -698,15 +666,9 @@ private fun PositionInfoWindowContent( Card { Column(modifier = Modifier.padding(8.dp)) { - PositionRow( - label = stringResource(R.string.latitude), - value = "%.5f".format(position.latitudeI * com.geeksville.mesh.ui.metrics.DEG_D), - ) + PositionRow(label = stringResource(R.string.latitude), value = "%.5f".format(position.latitudeI * DEG_D)) - PositionRow( - label = stringResource(R.string.longitude), - value = "%.5f".format(position.longitudeI * com.geeksville.mesh.ui.metrics.DEG_D), - ) + PositionRow(label = stringResource(R.string.longitude), value = "%.5f".format(position.longitudeI * DEG_D)) PositionRow(label = stringResource(R.string.sats), value = position.satsInView.toString()) @@ -722,7 +684,7 @@ private fun PositionInfoWindowContent( value = "%.0f°".format(position.groundTrack * HEADING_DEG), ) - PositionRow(label = stringResource(R.string.timestamp), value = formatPositionTime(position, dateFormat)) + PositionRow(label = stringResource(R.string.timestamp), value = position.formatPositionTime(dateFormat)) } } } diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/components/ClusterItemsListDialog.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/ClusterItemsListDialog.kt similarity index 96% rename from app/src/google/java/com/geeksville/mesh/ui/map/components/ClusterItemsListDialog.kt rename to feature/map/src/google/kotlin/org/meshtastic/feature/map/component/ClusterItemsListDialog.kt index 6b87b1ed2..576e01594 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/components/ClusterItemsListDialog.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/ClusterItemsListDialog.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.geeksville.mesh.ui.map.components +package org.meshtastic.feature.map.component import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.PaddingValues @@ -31,9 +31,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.geeksville.mesh.ui.map.NodeClusterItem import org.meshtastic.core.strings.R import org.meshtastic.core.ui.component.NodeChip +import org.meshtastic.feature.map.model.NodeClusterItem @Composable fun ClusterItemsListDialog( diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/components/EditWaypointDialog.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt similarity index 99% rename from app/src/google/java/com/geeksville/mesh/ui/map/components/EditWaypointDialog.kt rename to feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt index a05d34e4c..74aa4612f 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/components/EditWaypointDialog.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.geeksville.mesh.ui.map.components +package org.meshtastic.feature.map.component import android.app.DatePickerDialog import android.app.TimePickerDialog diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/components/NodeClusterMarkers.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/NodeClusterMarkers.kt similarity index 96% rename from app/src/google/java/com/geeksville/mesh/ui/map/components/NodeClusterMarkers.kt rename to feature/map/src/google/kotlin/org/meshtastic/feature/map/component/NodeClusterMarkers.kt index f9eb7136f..2640a204e 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/components/NodeClusterMarkers.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/NodeClusterMarkers.kt @@ -15,12 +15,11 @@ * along with this program. If not, see . */ -package com.geeksville.mesh.ui.map.components +package org.meshtastic.feature.map.component import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.graphics.Color -import com.geeksville.mesh.ui.map.NodeClusterItem import com.google.maps.android.clustering.Cluster import com.google.maps.android.clustering.view.DefaultClusterRenderer import com.google.maps.android.compose.Circle @@ -28,6 +27,7 @@ import com.google.maps.android.compose.MapsComposeExperimentalApi import com.google.maps.android.compose.clustering.Clustering import org.meshtastic.core.ui.component.NodeChip import org.meshtastic.feature.map.BaseMapViewModel +import org.meshtastic.feature.map.model.NodeClusterItem @OptIn(MapsComposeExperimentalApi::class) @Suppress("NestedBlockDepth") diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/components/WaypointMarkers.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/WaypointMarkers.kt similarity index 96% rename from app/src/google/java/com/geeksville/mesh/ui/map/components/WaypointMarkers.kt rename to feature/map/src/google/kotlin/org/meshtastic/feature/map/component/WaypointMarkers.kt index 24126075c..669efa9ac 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/components/WaypointMarkers.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/WaypointMarkers.kt @@ -15,13 +15,12 @@ * along with this program. If not, see . */ -package com.geeksville.mesh.ui.map.components +package org.meshtastic.feature.map.component import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import com.geeksville.mesh.MeshProtos -import com.geeksville.mesh.ui.node.DEG_D import com.google.android.gms.maps.model.BitmapDescriptor import com.google.android.gms.maps.model.LatLng import com.google.maps.android.compose.Marker @@ -29,6 +28,8 @@ import com.google.maps.android.compose.rememberUpdatedMarkerState import org.meshtastic.core.strings.R import org.meshtastic.feature.map.BaseMapViewModel +private const val DEG_D = 1e-7 + @Composable fun WaypointMarkers( displayableWaypoints: List, diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/NodeClusterItem.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/NodeClusterItem.kt new file mode 100644 index 000000000..c1f171ad6 --- /dev/null +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/NodeClusterItem.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.meshtastic.feature.map.model + +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.clustering.ClusterItem +import org.meshtastic.core.database.model.Node + +data class NodeClusterItem(val node: Node, val nodePosition: LatLng, val nodeTitle: String, val nodeSnippet: String) : + ClusterItem { + override fun getPosition(): LatLng = nodePosition + + override fun getTitle(): String = nodeTitle + + override fun getSnippet(): String = nodeSnippet + + override fun getZIndex(): Float? = null + + fun getPrecisionMeters(): Double? { + val precisionMap = + mapOf( + 10 to 23345.484932, + 11 to 11672.7369, + 12 to 5836.36288, + 13 to 2918.175876, + 14 to 1459.0823719999053, + 15 to 729.53562, + 16 to 364.7622, + 17 to 182.375556, + 18 to 91.182212, + 19 to 45.58554, + ) + return precisionMap[this.node.position.precisionBits] + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/map/MapScreen.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/MapScreen.kt similarity index 96% rename from app/src/main/java/com/geeksville/mesh/ui/map/MapScreen.kt rename to feature/map/src/main/kotlin/org/meshtastic/feature/map/MapScreen.kt index 3706f010a..fa95ccc7d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/map/MapScreen.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/MapScreen.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.geeksville.mesh.ui.map +package org.meshtastic.feature.map import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding @@ -28,7 +28,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.meshtastic.core.strings.R import org.meshtastic.core.ui.component.MainAppBar -import org.meshtastic.feature.map.MapViewModel @Composable fun MapScreen( diff --git a/app/src/main/java/com/geeksville/mesh/ui/map/NodeMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt similarity index 96% rename from app/src/main/java/com/geeksville/mesh/ui/map/NodeMapViewModel.kt rename to feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt index 0491f938d..3b29d6257 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/map/NodeMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.geeksville.mesh.ui.map +package org.meshtastic.feature.map.node import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel