mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat(wire): migrate from protobuf -> wire (#4401)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
9dbc8b7fbf
commit
25657e8f8f
239 changed files with 7149 additions and 6144 deletions
|
|
@ -133,10 +133,8 @@ 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.MeshProtos.Position
|
||||
import org.meshtastic.proto.MeshProtos.Waypoint
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.waypoint
|
||||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable
|
||||
import org.osmdroid.config.Configuration
|
||||
import org.osmdroid.events.MapEventsReceiver
|
||||
|
|
@ -325,7 +323,7 @@ fun MapView(
|
|||
LaunchedEffect(selectedWaypointId, waypoints) {
|
||||
if (selectedWaypointId != null && waypoints.containsKey(selectedWaypointId)) {
|
||||
waypoints[selectedWaypointId]?.data?.waypoint?.let { pt ->
|
||||
val geoPoint = GeoPoint(pt.latitudeI * 1e-7, pt.longitudeI * 1e-7)
|
||||
val geoPoint = GeoPoint((pt.latitude_i ?: 0) * 1e-7, (pt.longitude_i ?: 0) * 1e-7)
|
||||
map.controller.setCenter(geoPoint)
|
||||
map.controller.setZoom(WAYPOINT_ZOOM)
|
||||
}
|
||||
|
|
@ -396,7 +394,8 @@ fun MapView(
|
|||
fun MapView.onNodesChanged(nodes: Collection<Node>): List<MarkerWithLabel> {
|
||||
val nodesWithPosition = nodes.filter { it.validPosition != null }
|
||||
val ourNode = mapViewModel.ourNodeInfo.value
|
||||
val displayUnits = mapViewModel.config.display.units
|
||||
val displayUnits =
|
||||
mapViewModel.config.display?.units ?: org.meshtastic.proto.Config.DisplayConfig.DisplayUnits.METRIC
|
||||
val mapFilterStateValue = mapViewModel.mapFilterStateFlow.value // Access mapFilterState directly
|
||||
return nodesWithPosition.mapNotNull { node ->
|
||||
if (
|
||||
|
|
@ -410,9 +409,9 @@ fun MapView(
|
|||
|
||||
val (p, u) = node.position to node.user
|
||||
val nodePosition = GeoPoint(node.latitude, node.longitude)
|
||||
MarkerWithLabel(mapView = this, label = "${u.shortName} ${formatAgo(p.time)}").apply {
|
||||
MarkerWithLabel(mapView = this, label = "${u.short_name} ${formatAgo(p.time)}").apply {
|
||||
id = u.id
|
||||
title = u.longName
|
||||
title = u.long_name
|
||||
snippet =
|
||||
com.meshtastic.core.strings.getString(
|
||||
Res.string.map_node_popup_details,
|
||||
|
|
@ -436,7 +435,7 @@ fun MapView(
|
|||
if (!mapFilterStateValue.showPrecisionCircle) {
|
||||
setPrecisionBits(0)
|
||||
} else {
|
||||
setPrecisionBits(p.precisionBits)
|
||||
setPrecisionBits(p.precision_bits ?: 0)
|
||||
}
|
||||
setOnLongClickListener {
|
||||
navigateToNodeDetails(node.num)
|
||||
|
|
@ -456,10 +455,10 @@ fun MapView(
|
|||
Logger.d { "User deleted waypoint ${waypoint.id} for me" }
|
||||
mapViewModel.deleteWaypoint(waypoint.id)
|
||||
}
|
||||
if (waypoint.lockedTo in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
|
||||
if ((waypoint.locked_to ?: 0) in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
|
||||
builder.setPositiveButton(com.meshtastic.core.strings.getString(Res.string.delete_for_everyone)) { _, _ ->
|
||||
Logger.d { "User deleted waypoint ${waypoint.id} for everyone" }
|
||||
mapViewModel.sendWaypoint(waypoint.copy { expire = 1 })
|
||||
mapViewModel.sendWaypoint(waypoint.copy(expire = 1))
|
||||
mapViewModel.deleteWaypoint(waypoint.id)
|
||||
}
|
||||
}
|
||||
|
|
@ -484,7 +483,7 @@ fun MapView(
|
|||
Logger.d { "marker long pressed id=$id" }
|
||||
val waypoint = waypoints[id]?.data?.waypoint ?: return
|
||||
// edit only when unlocked or lockedTo myNodeNum
|
||||
if (waypoint.lockedTo in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
|
||||
if ((waypoint.locked_to ?: 0) in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
|
||||
showEditWaypointDialog = waypoint
|
||||
} else {
|
||||
showDeleteMarkerDialog(waypoint)
|
||||
|
|
@ -494,7 +493,7 @@ fun MapView(
|
|||
fun getUsername(id: String?) = if (id == DataPacket.ID_LOCAL || (myId != null && id == myId)) {
|
||||
com.meshtastic.core.strings.getString(Res.string.you)
|
||||
} else {
|
||||
mapViewModel.getUser(id).longName
|
||||
mapViewModel.getUser(id).long_name
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
|
|
@ -502,20 +501,20 @@ fun MapView(
|
|||
return waypoints.mapNotNull { waypoint ->
|
||||
val pt = waypoint.data.waypoint ?: return@mapNotNull null
|
||||
if (!mapFilterState.showWaypoints) return@mapNotNull null // Use collected mapFilterState
|
||||
val lock = if (pt.lockedTo != 0) "\uD83D\uDD12" else ""
|
||||
val lock = if ((pt.locked_to ?: 0) != 0) "\uD83D\uDD12" else ""
|
||||
val time =
|
||||
DateUtils.formatDateTime(
|
||||
context,
|
||||
waypoint.received_time,
|
||||
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL,
|
||||
)
|
||||
val label = pt.name + " " + formatAgo((waypoint.received_time / 1000).toInt())
|
||||
val emoji = String(Character.toChars(if (pt.icon == 0) 128205 else pt.icon))
|
||||
val label = (pt.name ?: "") + " " + formatAgo((waypoint.received_time / 1000).toInt())
|
||||
val emoji = String(Character.toChars(if ((pt.icon ?: 0) == 0) 128205 else pt.icon!!))
|
||||
val now = System.currentTimeMillis()
|
||||
val expireTimeMillis = pt.expire * 1000L
|
||||
val expireTimeMillis = (pt.expire ?: 0) * 1000L
|
||||
val expireTimeStr =
|
||||
when {
|
||||
pt.expire == 0 || pt.expire == Int.MAX_VALUE -> "Never"
|
||||
(pt.expire ?: 0) == 0 || pt.expire == Int.MAX_VALUE -> "Never"
|
||||
expireTimeMillis <= now -> "Expired"
|
||||
else ->
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
|
|
@ -533,7 +532,7 @@ fun MapView(
|
|||
"[$time] ${pt.description} " +
|
||||
com.meshtastic.core.strings.getString(Res.string.expires) +
|
||||
": $expireTimeStr"
|
||||
position = GeoPoint(pt.latitudeI * 1e-7, pt.longitudeI * 1e-7)
|
||||
position = GeoPoint((pt.latitude_i ?: 0) * 1e-7, (pt.longitude_i ?: 0) * 1e-7)
|
||||
if (selectedWaypointId == pt.id) {
|
||||
showInfoWindow()
|
||||
}
|
||||
|
|
@ -557,10 +556,8 @@ fun MapView(
|
|||
val enabled = isConnected && downloadRegionBoundingBox == null
|
||||
|
||||
if (enabled) {
|
||||
showEditWaypointDialog = waypoint {
|
||||
latitudeI = (p.latitude * 1e7).toInt()
|
||||
longitudeI = (p.longitude * 1e7).toInt()
|
||||
}
|
||||
showEditWaypointDialog =
|
||||
Waypoint(latitude_i = (p.latitude * 1e7).toInt(), longitude_i = (p.longitude * 1e7).toInt())
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -895,14 +892,22 @@ fun MapView(
|
|||
onSendClicked = { waypoint ->
|
||||
Logger.d { "User clicked send waypoint ${waypoint.id}" }
|
||||
showEditWaypointDialog = null
|
||||
|
||||
val newId =
|
||||
if (waypoint.id == 0) mapViewModel.generatePacketId() ?: return@EditWaypointDialog else waypoint.id
|
||||
val newName = if (waypoint.name.isNullOrEmpty()) "Dropped Pin" else waypoint.name
|
||||
val newExpire = if ((waypoint.expire ?: 0) == 0) Int.MAX_VALUE else (waypoint.expire ?: Int.MAX_VALUE)
|
||||
val newLockedTo = if ((waypoint.locked_to ?: 0) != 0) mapViewModel.myNodeNum ?: 0 else 0
|
||||
val newIcon = if ((waypoint.icon ?: 0) == 0) 128205 else waypoint.icon
|
||||
|
||||
mapViewModel.sendWaypoint(
|
||||
waypoint.copy {
|
||||
if (id == 0) id = mapViewModel.generatePacketId() ?: return@EditWaypointDialog
|
||||
if (name == "") name = "Dropped Pin"
|
||||
if (expire == 0) expire = Int.MAX_VALUE
|
||||
lockedTo = if (waypoint.lockedTo != 0) mapViewModel.myNodeNum ?: 0 else 0
|
||||
if (waypoint.icon == 0) icon = 128205
|
||||
},
|
||||
waypoint.copy(
|
||||
id = newId,
|
||||
name = newName,
|
||||
expire = newExpire,
|
||||
locked_to = newLockedTo,
|
||||
icon = newIcon,
|
||||
),
|
||||
)
|
||||
},
|
||||
onDeleteClicked = { waypoint ->
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map
|
||||
|
||||
import android.graphics.Color
|
||||
|
|
@ -26,7 +25,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.meshtastic.core.ui.R
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.Position
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.CopyrightOverlay
|
||||
|
|
@ -125,15 +124,15 @@ fun MapView.addPolyline(density: Density, geoPoints: List<GeoPoint>, onClick: ()
|
|||
return polyline
|
||||
}
|
||||
|
||||
fun MapView.addPositionMarkers(positions: List<MeshProtos.Position>, onClick: () -> Unit): List<Marker> {
|
||||
fun MapView.addPositionMarkers(positions: List<Position>, onClick: () -> Unit): List<Marker> {
|
||||
val navIcon = ContextCompat.getDrawable(context, R.drawable.ic_map_navigation_24)
|
||||
val markers =
|
||||
positions.map {
|
||||
Marker(this).apply {
|
||||
icon = navIcon
|
||||
rotation = (it.groundTrack * 1e-5).toFloat()
|
||||
rotation = ((it.ground_track ?: 0) * 1e-5).toFloat()
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
||||
position = GeoPoint(it.latitudeI * 1e-7, it.longitudeI * 1e-7)
|
||||
position = GeoPoint((it.latitude_i ?: 0) * 1e-7, (it.longitude_i ?: 0) * 1e-7)
|
||||
setOnMarkerClickListener { _, _ ->
|
||||
onClick()
|
||||
true
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import org.meshtastic.core.navigation.MapRoutes
|
|||
import org.meshtastic.core.prefs.map.MapPrefs
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -57,8 +57,7 @@ constructor(
|
|||
mapPrefs.mapStyle = value
|
||||
}
|
||||
|
||||
val localConfig =
|
||||
radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig.getDefaultInstance())
|
||||
val localConfig = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig())
|
||||
|
||||
val config
|
||||
get() = localConfig.value
|
||||
|
|
|
|||
|
|
@ -76,9 +76,7 @@ import org.meshtastic.core.strings.waypoint_new
|
|||
import org.meshtastic.core.ui.component.EditTextPreference
|
||||
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.proto.MeshProtos.Waypoint
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.waypoint
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import java.util.Calendar
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
|
|
@ -95,15 +93,16 @@ fun EditWaypointDialog(
|
|||
val title = if (waypoint.id == 0) Res.string.waypoint_new else Res.string.waypoint_edit
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
val emoji = if (waypointInput.icon == 0) 128205 else waypointInput.icon
|
||||
val emoji = if ((waypointInput.icon ?: 0) == 0) 128205 else waypointInput.icon!!
|
||||
var showEmojiPickerView by remember { mutableStateOf(false) }
|
||||
|
||||
// Get current context for dialogs
|
||||
val context = LocalContext.current
|
||||
val calendar = remember {
|
||||
Calendar.getInstance().apply {
|
||||
if (waypoint.expire != 0 && waypoint.expire != Int.MAX_VALUE) {
|
||||
timeInMillis = waypoint.expire * 1000L
|
||||
val expire = waypoint.expire ?: 0
|
||||
if (expire != 0 && expire != Int.MAX_VALUE) {
|
||||
timeInMillis = expire * 1000L
|
||||
} else {
|
||||
timeInMillis = System.currentTimeMillis()
|
||||
@Suppress("MagicNumber")
|
||||
|
|
@ -121,7 +120,7 @@ fun EditWaypointDialog(
|
|||
// State to hold selected date and time
|
||||
var selectedDate by remember {
|
||||
mutableStateOf(
|
||||
if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
dateFormat.format(calendar.time)
|
||||
} else {
|
||||
""
|
||||
|
|
@ -130,7 +129,7 @@ fun EditWaypointDialog(
|
|||
}
|
||||
var selectedTime by remember {
|
||||
mutableStateOf(
|
||||
if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
timeFormat.format(calendar.time)
|
||||
} else {
|
||||
""
|
||||
|
|
@ -139,8 +138,8 @@ fun EditWaypointDialog(
|
|||
}
|
||||
var epochTime by remember {
|
||||
mutableStateOf<Long?>(
|
||||
if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
waypointInput.expire * 1000L
|
||||
if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
(waypointInput.expire ?: 0) * 1000L
|
||||
} else {
|
||||
null
|
||||
},
|
||||
|
|
@ -164,14 +163,14 @@ fun EditWaypointDialog(
|
|||
)
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.name),
|
||||
value = waypointInput.name,
|
||||
value = waypointInput.name ?: "",
|
||||
maxSize = 29,
|
||||
enabled = true,
|
||||
isError = false,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = {}),
|
||||
onValueChanged = { waypointInput = waypointInput.copy { name = it } },
|
||||
onValueChanged = { waypointInput = waypointInput.copy(name = it) },
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { showEmojiPickerView = true }) {
|
||||
Text(
|
||||
|
|
@ -187,14 +186,14 @@ fun EditWaypointDialog(
|
|||
)
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.description),
|
||||
value = waypointInput.description,
|
||||
value = waypointInput.description ?: "",
|
||||
maxSize = 99,
|
||||
enabled = true,
|
||||
isError = false,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = {}),
|
||||
onValueChanged = { waypointInput = waypointInput.copy { description = it } },
|
||||
onValueChanged = { waypointInput = waypointInput.copy(description = it) },
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().size(48.dp),
|
||||
|
|
@ -204,8 +203,8 @@ fun EditWaypointDialog(
|
|||
Text(stringResource(Res.string.locked))
|
||||
Switch(
|
||||
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End),
|
||||
checked = waypointInput.lockedTo != 0,
|
||||
onCheckedChange = { waypointInput = waypointInput.copy { lockedTo = if (it) 1 else 0 } },
|
||||
checked = (waypointInput.locked_to ?: 0) != 0,
|
||||
onCheckedChange = { waypointInput = waypointInput.copy(locked_to = if (it) 1 else 0) },
|
||||
)
|
||||
}
|
||||
val datePickerDialog =
|
||||
|
|
@ -229,7 +228,7 @@ fun EditWaypointDialog(
|
|||
calendar.set(Calendar.MINUTE, selectedMinute)
|
||||
epochTime = calendar.timeInMillis
|
||||
selectedTime = timeFormat.format(calendar.time)
|
||||
waypointInput = waypointInput.copy { expire = (calendar.timeInMillis / 1000).toInt() }
|
||||
waypointInput = waypointInput.copy(expire = (calendar.timeInMillis / 1000).toInt())
|
||||
},
|
||||
calendar.get(Calendar.HOUR_OF_DAY),
|
||||
calendar.get(Calendar.MINUTE),
|
||||
|
|
@ -247,7 +246,7 @@ fun EditWaypointDialog(
|
|||
Text(stringResource(Res.string.expires))
|
||||
Switch(
|
||||
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End),
|
||||
checked = waypointInput.expire != Int.MAX_VALUE && waypointInput.expire != 0,
|
||||
checked = waypointInput.expire != Int.MAX_VALUE && (waypointInput.expire ?: 0) != 0,
|
||||
onCheckedChange = { isChecked ->
|
||||
if (isChecked) {
|
||||
// Default to now if not already set
|
||||
|
|
@ -256,18 +255,17 @@ fun EditWaypointDialog(
|
|||
}
|
||||
selectedDate = dateFormat.format(calendar.time)
|
||||
selectedTime = timeFormat.format(calendar.time)
|
||||
waypointInput =
|
||||
waypointInput.copy { expire = (calendar.timeInMillis / 1000).toInt() }
|
||||
waypointInput = waypointInput.copy(expire = (calendar.timeInMillis / 1000).toInt())
|
||||
} else {
|
||||
selectedDate = ""
|
||||
selectedTime = ""
|
||||
waypointInput = waypointInput.copy { expire = Int.MAX_VALUE }
|
||||
waypointInput = waypointInput.copy(expire = Int.MAX_VALUE)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (waypointInput.expire != Int.MAX_VALUE && waypointInput.expire != 0) {
|
||||
if (waypointInput.expire != Int.MAX_VALUE && (waypointInput.expire ?: 0) != 0) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
|
||||
|
|
@ -308,7 +306,7 @@ fun EditWaypointDialog(
|
|||
Button(
|
||||
modifier = modifier.weight(1f),
|
||||
onClick = { onDeleteClicked(waypointInput) },
|
||||
enabled = waypointInput.name.isNotEmpty(),
|
||||
enabled = !(waypointInput.name.isNullOrEmpty()),
|
||||
) {
|
||||
Text(stringResource(Res.string.delete))
|
||||
}
|
||||
|
|
@ -322,7 +320,7 @@ fun EditWaypointDialog(
|
|||
} else {
|
||||
EmojiPickerDialog(onDismiss = { showEmojiPickerView = false }) {
|
||||
showEmojiPickerView = false
|
||||
waypointInput = waypointInput.copy { icon = it.codePointAt(0) }
|
||||
waypointInput = waypointInput.copy(icon = it.codePointAt(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -334,13 +332,13 @@ private fun EditWaypointFormPreview() {
|
|||
AppTheme {
|
||||
EditWaypointDialog(
|
||||
waypoint =
|
||||
waypoint {
|
||||
id = 123
|
||||
name = "Test 123"
|
||||
description = "This is only a test"
|
||||
icon = 128169
|
||||
expire = (System.currentTimeMillis() / 1000 + 8 * 3600).toInt()
|
||||
},
|
||||
Waypoint(
|
||||
id = 123,
|
||||
name = "Test 123",
|
||||
description = "This is only a test",
|
||||
icon = 128169,
|
||||
expire = (System.currentTimeMillis() / 1000 + 8 * 3600).toInt(),
|
||||
),
|
||||
onSendClicked = {},
|
||||
onDeleteClicked = {},
|
||||
onDismissRequest = {},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map.node
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
|
@ -39,7 +38,7 @@ private const val DEG_D = 1e-7
|
|||
fun NodeMapScreen(nodeMapViewModel: NodeMapViewModel, onNavigateUp: () -> Unit) {
|
||||
val density = LocalDensity.current
|
||||
val positionLogs by nodeMapViewModel.positionLogs.collectAsStateWithLifecycle()
|
||||
val geoPoints = positionLogs.map { GeoPoint(it.latitudeI * DEG_D, it.longitudeI * DEG_D) }
|
||||
val geoPoints = positionLogs.map { GeoPoint((it.latitude_i ?: 0) * DEG_D, (it.longitude_i ?: 0) * DEG_D) }
|
||||
val cameraView = remember { BoundingBox.fromGeoPoints(geoPoints) }
|
||||
val mapView =
|
||||
rememberMapViewWithLifecycle(
|
||||
|
|
|
|||
|
|
@ -119,11 +119,9 @@ import org.meshtastic.feature.map.component.NodeClusterMarkers
|
|||
import org.meshtastic.feature.map.component.WaypointMarkers
|
||||
import org.meshtastic.feature.map.model.NodeClusterItem
|
||||
import org.meshtastic.feature.map.model.TracerouteOverlay
|
||||
import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig.DisplayUnits
|
||||
import org.meshtastic.proto.MeshProtos.Position
|
||||
import org.meshtastic.proto.MeshProtos.Waypoint
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.waypoint
|
||||
import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits
|
||||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
|
||||
|
|
@ -295,12 +293,12 @@ fun MapView(
|
|||
|
||||
val nodeClusterItems =
|
||||
displayNodes.map { node ->
|
||||
val latLng = LatLng(node.position.latitudeI * DEG_D, node.position.longitudeI * DEG_D)
|
||||
val latLng = LatLng((node.position.latitude_i ?: 0) * DEG_D, (node.position.longitude_i ?: 0) * DEG_D)
|
||||
NodeClusterItem(
|
||||
node = node,
|
||||
nodePosition = latLng,
|
||||
nodeTitle = "${node.user.shortName} ${formatAgo(node.position.time)}",
|
||||
nodeSnippet = "${node.user.longName}",
|
||||
nodeTitle = "${node.user.short_name} ${formatAgo(node.position.time)}",
|
||||
nodeSnippet = "${node.user.long_name}",
|
||||
)
|
||||
}
|
||||
val isConnected by mapViewModel.isConnected.collectAsStateWithLifecycle()
|
||||
|
|
@ -438,10 +436,11 @@ fun MapView(
|
|||
MapProperties(mapType = effectiveGoogleMapType, isMyLocationEnabled = hasLocationPermission),
|
||||
onMapLongClick = { latLng ->
|
||||
if (isConnected) {
|
||||
val newWaypoint = waypoint {
|
||||
latitudeI = (latLng.latitude / DEG_D).toInt()
|
||||
longitudeI = (latLng.longitude / DEG_D).toInt()
|
||||
}
|
||||
val newWaypoint =
|
||||
Waypoint(
|
||||
latitude_i = (latLng.latitude / DEG_D).toInt(),
|
||||
longitude_i = (latLng.longitude / DEG_D).toInt(),
|
||||
)
|
||||
editingWaypoint = newWaypoint
|
||||
}
|
||||
},
|
||||
|
|
@ -617,18 +616,18 @@ fun MapView(
|
|||
onSendClicked = { updatedWp ->
|
||||
var finalWp = updatedWp
|
||||
if (updatedWp.id == 0) {
|
||||
finalWp = finalWp.copy { id = mapViewModel.generatePacketId() ?: 0 }
|
||||
finalWp = finalWp.copy(id = mapViewModel.generatePacketId() ?: 0)
|
||||
}
|
||||
if (updatedWp.icon == 0) {
|
||||
finalWp = finalWp.copy { icon = 0x1F4CD }
|
||||
if ((updatedWp.icon ?: 0) == 0) {
|
||||
finalWp = finalWp.copy(icon = 0x1F4CD)
|
||||
}
|
||||
|
||||
mapViewModel.sendWaypoint(finalWp)
|
||||
editingWaypoint = null
|
||||
},
|
||||
onDeleteClicked = { wpToDelete ->
|
||||
if (wpToDelete.lockedTo == 0 && isConnected && wpToDelete.id != 0) {
|
||||
val deleteMarkerWp = wpToDelete.copy { expire = 1 }
|
||||
if ((wpToDelete.locked_to ?: 0) == 0 && isConnected && wpToDelete.id != 0) {
|
||||
val deleteMarkerWp = wpToDelete.copy(expire = 1)
|
||||
mapViewModel.sendWaypoint(deleteMarkerWp)
|
||||
}
|
||||
mapViewModel.deleteWaypoint(wpToDelete.id)
|
||||
|
|
@ -683,25 +682,25 @@ fun MapView(
|
|||
followPhoneBearing = followPhoneBearing,
|
||||
)
|
||||
}
|
||||
if (showLayersBottomSheet) {
|
||||
ModalBottomSheet(onDismissRequest = { showLayersBottomSheet = false }) {
|
||||
CustomMapLayersSheet(mapLayers, onToggleVisibility, onRemoveLayer, onAddLayerClicked)
|
||||
}
|
||||
}
|
||||
if (showLayersBottomSheet) {
|
||||
ModalBottomSheet(onDismissRequest = { showLayersBottomSheet = false }) {
|
||||
CustomMapLayersSheet(mapLayers, onToggleVisibility, onRemoveLayer, onAddLayerClicked)
|
||||
}
|
||||
showClusterItemsDialog?.let {
|
||||
ClusterItemsListDialog(
|
||||
items = it,
|
||||
onDismiss = { showClusterItemsDialog = null },
|
||||
onItemClick = { item ->
|
||||
navigateToNodeDetails(item.node.num)
|
||||
showClusterItemsDialog = null
|
||||
},
|
||||
)
|
||||
}
|
||||
if (showCustomTileManagerSheet) {
|
||||
ModalBottomSheet(onDismissRequest = { showCustomTileManagerSheet = false }) {
|
||||
CustomTileProviderManagerSheet(mapViewModel = mapViewModel)
|
||||
}
|
||||
}
|
||||
showClusterItemsDialog?.let {
|
||||
ClusterItemsListDialog(
|
||||
items = it,
|
||||
onDismiss = { showClusterItemsDialog = null },
|
||||
onItemClick = { item ->
|
||||
navigateToNodeDetails(item.node.num)
|
||||
showClusterItemsDialog = null
|
||||
},
|
||||
)
|
||||
}
|
||||
if (showCustomTileManagerSheet) {
|
||||
ModalBottomSheet(onDismissRequest = { showCustomTileManagerSheet = false }) {
|
||||
CustomTileProviderManagerSheet(mapViewModel = mapViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -763,25 +762,28 @@ private fun PositionInfoWindowContent(position: Position, displayUnits: DisplayU
|
|||
|
||||
Card {
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
PositionRow(label = stringResource(Res.string.latitude), value = "%.5f".format(position.latitudeI * DEG_D))
|
||||
PositionRow(
|
||||
label = stringResource(Res.string.latitude),
|
||||
value = "%.5f".format((position.latitude_i ?: 0) * DEG_D),
|
||||
)
|
||||
|
||||
PositionRow(
|
||||
label = stringResource(Res.string.longitude),
|
||||
value = "%.5f".format(position.longitudeI * DEG_D),
|
||||
value = "%.5f".format((position.longitude_i ?: 0) * DEG_D),
|
||||
)
|
||||
|
||||
PositionRow(label = stringResource(Res.string.sats), value = position.satsInView.toString())
|
||||
PositionRow(label = stringResource(Res.string.sats), value = position.sats_in_view?.toString() ?: "")
|
||||
|
||||
PositionRow(
|
||||
label = stringResource(Res.string.alt),
|
||||
value = position.altitude.metersIn(displayUnits).toString(displayUnits),
|
||||
value = (position.altitude ?: 0).metersIn(displayUnits).toString(displayUnits),
|
||||
)
|
||||
|
||||
PositionRow(label = stringResource(Res.string.speed), value = speedFromPosition(position, displayUnits))
|
||||
|
||||
PositionRow(
|
||||
label = stringResource(Res.string.heading),
|
||||
value = "%.0f°".format(position.groundTrack * HEADING_DEG),
|
||||
value = "%.0f°".format((position.ground_track ?: 0) * HEADING_DEG),
|
||||
)
|
||||
|
||||
PositionRow(label = stringResource(Res.string.timestamp), value = position.formatPositionTime())
|
||||
|
|
@ -791,13 +793,13 @@ private fun PositionInfoWindowContent(position: Position, displayUnits: DisplayU
|
|||
|
||||
@Composable
|
||||
private fun speedFromPosition(position: Position, displayUnits: DisplayUnits): String {
|
||||
val speedInMps = position.groundSpeed
|
||||
val speedInMps = position.ground_speed ?: 0
|
||||
val mpsText = "%d m/s".format(speedInMps)
|
||||
val speedText =
|
||||
if (speedInMps > 10) {
|
||||
when (displayUnits) {
|
||||
DisplayUnits.METRIC -> "%.1f Km/h".format(position.groundSpeed.mpsToKmph())
|
||||
DisplayUnits.IMPERIAL -> "%.1f mph".format(position.groundSpeed.mpsToMph())
|
||||
DisplayUnits.METRIC -> "%.1f Km/h".format(speedInMps.mpsToKmph())
|
||||
DisplayUnits.IMPERIAL -> "%.1f mph".format(speedInMps.mpsToMph())
|
||||
else -> mpsText // Fallback or handle UNRECOGNIZED
|
||||
}
|
||||
} else {
|
||||
|
|
@ -806,11 +808,11 @@ private fun speedFromPosition(position: Position, displayUnits: DisplayUnits): S
|
|||
return speedText
|
||||
}
|
||||
|
||||
internal fun Position.toLatLng(): LatLng = LatLng(this.latitudeI * DEG_D, this.longitudeI * DEG_D)
|
||||
internal fun Position.toLatLng(): LatLng = LatLng((this.latitude_i ?: 0) * DEG_D, (this.longitude_i ?: 0) * DEG_D)
|
||||
|
||||
private fun Node.toLatLng(): LatLng? = this.position.toLatLng()
|
||||
|
||||
private fun Waypoint.toLatLng(): LatLng = LatLng(this.latitudeI * DEG_D, this.longitudeI * DEG_D)
|
||||
private fun Waypoint.toLatLng(): LatLng = LatLng((this.latitude_i ?: 0) * DEG_D, (this.longitude_i ?: 0) * DEG_D)
|
||||
|
||||
private fun offsetPolyline(
|
||||
points: List<LatLng>,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ import org.meshtastic.core.prefs.map.GoogleMapsPrefs
|
|||
import org.meshtastic.core.prefs.map.MapPrefs
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.proto.ConfigProtos
|
||||
import org.meshtastic.proto.Config
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
|
@ -133,8 +133,8 @@ constructor(
|
|||
|
||||
val displayUnits =
|
||||
radioConfigRepository.deviceProfileFlow
|
||||
.mapNotNull { it.config.display.units }
|
||||
.stateInWhileSubscribed(initialValue = ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC)
|
||||
.mapNotNull { it.config?.display?.units }
|
||||
.stateInWhileSubscribed(initialValue = Config.DisplayConfig.DisplayUnits.METRIC)
|
||||
|
||||
fun addCustomTileProvider(name: String, urlTemplate: String) {
|
||||
viewModelScope.launch {
|
||||
|
|
@ -270,7 +270,7 @@ constructor(
|
|||
val wpMap = waypoints.first { it.containsKey(wpId) }
|
||||
wpMap[wpId]?.let { packet ->
|
||||
val waypoint = packet.data.waypoint!!
|
||||
val latLng = LatLng(waypoint.latitudeI / 1e7, waypoint.longitudeI / 1e7)
|
||||
val latLng = LatLng((waypoint.latitude_i ?: 0) / 1e7, (waypoint.longitude_i ?: 0) / 1e7)
|
||||
cameraPositionState.position = CameraPosition.fromLatLngZoom(latLng, 15f)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,8 +74,7 @@ import org.meshtastic.core.strings.time
|
|||
import org.meshtastic.core.strings.waypoint_edit
|
||||
import org.meshtastic.core.strings.waypoint_new
|
||||
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
|
||||
import org.meshtastic.proto.MeshProtos.Waypoint
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import java.util.Calendar
|
||||
import java.util.TimeZone
|
||||
|
||||
|
|
@ -92,7 +91,7 @@ fun EditWaypointDialog(
|
|||
var waypointInput by remember { mutableStateOf(waypoint) }
|
||||
val title = if (waypoint.id == 0) Res.string.waypoint_new else Res.string.waypoint_edit
|
||||
val defaultEmoji = 0x1F4CD // 📍 Round Pushpin
|
||||
val currentEmojiCodepoint = if (waypointInput.icon == 0) defaultEmoji else waypointInput.icon
|
||||
val currentEmojiCodepoint = if ((waypointInput.icon ?: 0) == 0) defaultEmoji else waypointInput.icon!!
|
||||
var showEmojiPickerView by remember { mutableStateOf(false) }
|
||||
|
||||
val context = LocalContext.current
|
||||
|
|
@ -102,7 +101,7 @@ fun EditWaypointDialog(
|
|||
var selectedDateString by remember { mutableStateOf("") }
|
||||
var selectedTimeString by remember { mutableStateOf("") }
|
||||
var isExpiryEnabled by remember {
|
||||
mutableStateOf(waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE)
|
||||
mutableStateOf((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE)
|
||||
}
|
||||
|
||||
val dateFormat = remember { android.text.format.DateFormat.getDateFormat(context) }
|
||||
|
|
@ -111,15 +110,16 @@ fun EditWaypointDialog(
|
|||
timeFormat.timeZone = TimeZone.getDefault()
|
||||
|
||||
LaunchedEffect(waypointInput.expire, isExpiryEnabled) {
|
||||
val expireValue = waypointInput.expire ?: 0
|
||||
if (isExpiryEnabled) {
|
||||
if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
calendar.timeInMillis = waypointInput.expire * 1000L
|
||||
if (expireValue != 0 && expireValue != Int.MAX_VALUE) {
|
||||
calendar.timeInMillis = expireValue * 1000L
|
||||
selectedDateString = dateFormat.format(calendar.time)
|
||||
selectedTimeString = timeFormat.format(calendar.time)
|
||||
} else { // If enabled but not set, default to 8 hours from now
|
||||
calendar.timeInMillis = System.currentTimeMillis()
|
||||
calendar.add(Calendar.HOUR_OF_DAY, 8)
|
||||
waypointInput = waypointInput.copy { expire = (calendar.timeInMillis / 1000).toInt() }
|
||||
waypointInput = waypointInput.copy(expire = (calendar.timeInMillis / 1000).toInt())
|
||||
}
|
||||
} else {
|
||||
selectedDateString = ""
|
||||
|
|
@ -141,8 +141,8 @@ fun EditWaypointDialog(
|
|||
text = {
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
OutlinedTextField(
|
||||
value = waypointInput.name,
|
||||
onValueChange = { waypointInput = waypointInput.copy { name = it.take(29) } },
|
||||
value = waypointInput.name ?: "",
|
||||
onValueChange = { waypointInput = waypointInput.copy(name = it.take(29)) },
|
||||
label = { Text(stringResource(Res.string.name)) },
|
||||
singleLine = true,
|
||||
keyboardOptions =
|
||||
|
|
@ -162,8 +162,8 @@ fun EditWaypointDialog(
|
|||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
OutlinedTextField(
|
||||
value = waypointInput.description,
|
||||
onValueChange = { waypointInput = waypointInput.copy { description = it.take(99) } },
|
||||
value = waypointInput.description ?: "",
|
||||
onValueChange = { waypointInput = waypointInput.copy(description = it.take(99)) },
|
||||
label = { Text(stringResource(Res.string.description)) },
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
|
|
@ -187,8 +187,8 @@ fun EditWaypointDialog(
|
|||
Text(stringResource(Res.string.locked))
|
||||
}
|
||||
Switch(
|
||||
checked = waypointInput.lockedTo != 0,
|
||||
onCheckedChange = { waypointInput = waypointInput.copy { lockedTo = if (it) 1 else 0 } },
|
||||
checked = (waypointInput.locked_to ?: 0) != 0,
|
||||
onCheckedChange = { waypointInput = waypointInput.copy(locked_to = if (it) 1 else 0) },
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
|
|
@ -210,17 +210,17 @@ fun EditWaypointDialog(
|
|||
onCheckedChange = { checked ->
|
||||
isExpiryEnabled = checked
|
||||
if (checked) {
|
||||
val expireValue = waypointInput.expire ?: 0
|
||||
// Default to 8 hours from now if not already set
|
||||
if (waypointInput.expire == 0 || waypointInput.expire == Int.MAX_VALUE) {
|
||||
if (expireValue == 0 || expireValue == Int.MAX_VALUE) {
|
||||
val cal = Calendar.getInstance()
|
||||
cal.timeInMillis = System.currentTimeMillis()
|
||||
cal.add(Calendar.HOUR_OF_DAY, 8)
|
||||
waypointInput =
|
||||
waypointInput.copy { expire = (cal.timeInMillis / 1000).toInt() }
|
||||
waypointInput = waypointInput.copy(expire = (cal.timeInMillis / 1000).toInt())
|
||||
}
|
||||
// LaunchedEffect will update date/time strings
|
||||
} else {
|
||||
waypointInput = waypointInput.copy { expire = Int.MAX_VALUE }
|
||||
waypointInput = waypointInput.copy(expire = Int.MAX_VALUE)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -229,8 +229,9 @@ fun EditWaypointDialog(
|
|||
if (isExpiryEnabled) {
|
||||
val currentCalendar =
|
||||
Calendar.getInstance().apply {
|
||||
if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
timeInMillis = waypointInput.expire * 1000L
|
||||
val expireValue = waypointInput.expire ?: 0
|
||||
if (expireValue != 0 && expireValue != Int.MAX_VALUE) {
|
||||
timeInMillis = expireValue * 1000L
|
||||
} else {
|
||||
timeInMillis = System.currentTimeMillis()
|
||||
add(Calendar.HOUR_OF_DAY, 8) // Default if re-enabling
|
||||
|
|
@ -247,14 +248,14 @@ fun EditWaypointDialog(
|
|||
context,
|
||||
{ _: DatePicker, selectedYear: Int, selectedMonth: Int, selectedDay: Int ->
|
||||
val tempCal = Calendar.getInstance()
|
||||
if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
tempCal.timeInMillis = waypointInput.expire * 1000L
|
||||
val expireValue = waypointInput.expire ?: 0
|
||||
if (expireValue != 0 && expireValue != Int.MAX_VALUE) {
|
||||
tempCal.timeInMillis = expireValue * 1000L
|
||||
} else {
|
||||
tempCal.add(Calendar.HOUR_OF_DAY, 8)
|
||||
}
|
||||
tempCal.set(selectedYear, selectedMonth, selectedDay)
|
||||
waypointInput =
|
||||
waypointInput.copy { expire = (tempCal.timeInMillis / 1000).toInt() }
|
||||
waypointInput = waypointInput.copy(expire = (tempCal.timeInMillis / 1000).toInt())
|
||||
},
|
||||
year,
|
||||
month,
|
||||
|
|
@ -267,15 +268,15 @@ fun EditWaypointDialog(
|
|||
{ _: TimePicker, selectedHour: Int, selectedMinute: Int ->
|
||||
// Keep the existing date part
|
||||
val tempCal = Calendar.getInstance()
|
||||
if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
tempCal.timeInMillis = waypointInput.expire * 1000L
|
||||
val expireValue = waypointInput.expire ?: 0
|
||||
if (expireValue != 0 && expireValue != Int.MAX_VALUE) {
|
||||
tempCal.timeInMillis = expireValue * 1000L
|
||||
} else {
|
||||
tempCal.add(Calendar.HOUR_OF_DAY, 8)
|
||||
}
|
||||
tempCal.set(Calendar.HOUR_OF_DAY, selectedHour)
|
||||
tempCal.set(Calendar.MINUTE, selectedMinute)
|
||||
waypointInput =
|
||||
waypointInput.copy { expire = (tempCal.timeInMillis / 1000).toInt() }
|
||||
waypointInput = waypointInput.copy(expire = (tempCal.timeInMillis / 1000).toInt())
|
||||
},
|
||||
hour,
|
||||
minute,
|
||||
|
|
@ -324,7 +325,10 @@ fun EditWaypointDialog(
|
|||
TextButton(onClick = onDismissRequest, modifier = Modifier.padding(end = 8.dp)) {
|
||||
Text(stringResource(Res.string.cancel))
|
||||
}
|
||||
Button(onClick = { onSendClicked(waypointInput) }, enabled = waypointInput.name.isNotBlank()) {
|
||||
Button(
|
||||
onClick = { onSendClicked(waypointInput) },
|
||||
enabled = (waypointInput.name ?: "").isNotBlank(),
|
||||
) {
|
||||
Text(stringResource(Res.string.send))
|
||||
}
|
||||
}
|
||||
|
|
@ -335,7 +339,7 @@ fun EditWaypointDialog(
|
|||
} else {
|
||||
EmojiPickerDialog(onDismiss = { showEmojiPickerView = false }) { selectedEmoji ->
|
||||
showEmojiPickerView = false
|
||||
waypointInput = waypointInput.copy { icon = selectedEmoji.codePointAt(0) }
|
||||
waypointInput = waypointInput.copy(icon = selectedEmoji.codePointAt(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,18 +29,18 @@ import org.meshtastic.core.strings.Res
|
|||
import org.meshtastic.core.strings.locked
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
import org.meshtastic.feature.map.BaseMapViewModel
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.Waypoint
|
||||
|
||||
private const val DEG_D = 1e-7
|
||||
|
||||
@Composable
|
||||
fun WaypointMarkers(
|
||||
displayableWaypoints: List<MeshProtos.Waypoint>,
|
||||
displayableWaypoints: List<Waypoint>,
|
||||
mapFilterState: BaseMapViewModel.MapFilterState,
|
||||
myNodeNum: Int,
|
||||
isConnected: Boolean,
|
||||
unicodeEmojiToBitmapProvider: (Int) -> BitmapDescriptor,
|
||||
onEditWaypointRequest: (MeshProtos.Waypoint) -> Unit,
|
||||
onEditWaypointRequest: (Waypoint) -> Unit,
|
||||
selectedWaypointId: Int? = null,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
|
@ -48,7 +48,9 @@ fun WaypointMarkers(
|
|||
if (mapFilterState.showWaypoints) {
|
||||
displayableWaypoints.forEach { waypoint ->
|
||||
val markerState =
|
||||
rememberUpdatedMarkerState(position = LatLng(waypoint.latitudeI * DEG_D, waypoint.longitudeI * DEG_D))
|
||||
rememberUpdatedMarkerState(
|
||||
position = LatLng((waypoint.latitude_i ?: 0) * DEG_D, (waypoint.longitude_i ?: 0) * DEG_D),
|
||||
)
|
||||
|
||||
LaunchedEffect(selectedWaypointId) {
|
||||
if (selectedWaypointId == waypoint.id) {
|
||||
|
|
@ -59,16 +61,16 @@ fun WaypointMarkers(
|
|||
Marker(
|
||||
state = markerState,
|
||||
icon =
|
||||
if (waypoint.icon == 0) {
|
||||
if ((waypoint.icon ?: 0) == 0) {
|
||||
unicodeEmojiToBitmapProvider(PUSHPIN) // Default icon (Round Pushpin)
|
||||
} else {
|
||||
unicodeEmojiToBitmapProvider(waypoint.icon)
|
||||
unicodeEmojiToBitmapProvider(waypoint.icon!!)
|
||||
},
|
||||
title = waypoint.name.replace('\n', ' ').replace('\b', ' '),
|
||||
snippet = waypoint.description.replace('\n', ' ').replace('\b', ' '),
|
||||
title = (waypoint.name ?: "").replace('\n', ' ').replace('\b', ' '),
|
||||
snippet = (waypoint.description ?: "").replace('\n', ' ').replace('\b', ' '),
|
||||
visible = true,
|
||||
onInfoWindowClick = {
|
||||
if (waypoint.lockedTo == 0 || waypoint.lockedTo == myNodeNum || !isConnected) {
|
||||
if ((waypoint.locked_to ?: 0) == 0 || waypoint.locked_to == myNodeNum || !isConnected) {
|
||||
onEditWaypointRequest(waypoint)
|
||||
} else {
|
||||
scope.launch { context.showToast(Res.string.locked) }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map.model
|
||||
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
|
|
@ -45,6 +44,6 @@ data class NodeClusterItem(val node: Node, val nodePosition: LatLng, val nodeTit
|
|||
18 to 91.182212,
|
||||
19 to 45.58554,
|
||||
)
|
||||
return precisionMap[this.node.position.precisionBits]
|
||||
return precisionMap[this.node.position.precision_bits ?: 0]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map.node
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -36,7 +35,7 @@ fun NodeMapScreen(nodeMapViewModel: NodeMapViewModel, onNavigateUp: () -> Unit)
|
|||
Scaffold(
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
title = node?.user?.longName ?: "",
|
||||
title = node?.user?.long_name ?: "",
|
||||
ourNode = null,
|
||||
showNodeChip = false,
|
||||
canNavigateUp = true,
|
||||
|
|
|
|||
|
|
@ -43,7 +43,9 @@ import org.meshtastic.core.strings.one_hour
|
|||
import org.meshtastic.core.strings.two_days
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.map.model.TracerouteOverlay
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.User
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
|
|
@ -93,7 +95,8 @@ abstract class BaseMapViewModel(
|
|||
list
|
||||
.associateBy { packet -> packet.data.waypoint!!.id }
|
||||
.filterValues {
|
||||
it.data.waypoint!!.expire == 0 || it.data.waypoint!!.expire > System.currentTimeMillis() / 1000
|
||||
val expire = it.data.waypoint!!.expire ?: 0
|
||||
expire == 0 || expire > System.currentTimeMillis() / 1000
|
||||
}
|
||||
}
|
||||
.stateInWhileSubscribed(initialValue = emptyMap())
|
||||
|
|
@ -122,9 +125,9 @@ abstract class BaseMapViewModel(
|
|||
|
||||
fun getNodeByNum(nodeNum: Int): Node? = nodeRepository.nodeDBbyNum.value[nodeNum]
|
||||
|
||||
open fun getUser(userId: String?): MeshProtos.User = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST)
|
||||
open fun getUser(userId: String?): User = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST)
|
||||
|
||||
fun getUser(nodeNum: Int): MeshProtos.User = nodeRepository.getUser(nodeNum)
|
||||
fun getUser(nodeNum: Int): User = nodeRepository.getUser(nodeNum)
|
||||
|
||||
fun getNodeOrFallback(nodeNum: Int): Node = getNodeByNum(nodeNum) ?: Node(num = nodeNum, user = getUser(nodeNum))
|
||||
|
||||
|
|
@ -160,7 +163,7 @@ abstract class BaseMapViewModel(
|
|||
|
||||
fun deleteWaypoint(id: Int) = viewModelScope.launch(Dispatchers.IO) { packetRepository.deleteWaypoint(id) }
|
||||
|
||||
fun sendWaypoint(wpt: MeshProtos.Waypoint, contactKey: String = "0${DataPacket.ID_BROADCAST}") {
|
||||
fun sendWaypoint(wpt: Waypoint, contactKey: String = "0${DataPacket.ID_BROADCAST}") {
|
||||
// contactKey: unique contact key filter (channel)+(nodeId)
|
||||
val channel = contactKey[0].digitToIntOrNull()
|
||||
val dest = if (channel != null) contactKey.substring(1) else contactKey
|
||||
|
|
@ -215,7 +218,7 @@ data class TracerouteNodeSelection(
|
|||
|
||||
fun BaseMapViewModel.tracerouteNodeSelection(
|
||||
tracerouteOverlay: TracerouteOverlay?,
|
||||
tracerouteNodePositions: Map<Int, MeshProtos.Position>,
|
||||
tracerouteNodePositions: Map<Int, Position>,
|
||||
nodes: List<Node>,
|
||||
): TracerouteNodeSelection {
|
||||
val overlayNodeNums = tracerouteOverlay?.relatedNodeNums ?: emptySet()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map.node
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
|
|
@ -35,8 +34,8 @@ import org.meshtastic.core.prefs.map.MapPrefs
|
|||
import org.meshtastic.core.ui.util.toPosition
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.map.model.CustomTileSource
|
||||
import org.meshtastic.proto.MeshProtos.Position
|
||||
import org.meshtastic.proto.Portnums.PortNum
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Position
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
|
|
@ -61,13 +60,13 @@ constructor(
|
|||
|
||||
val positionLogs: StateFlow<List<Position>> =
|
||||
meshLogRepository
|
||||
.getMeshPacketsFrom(destNum!!, PortNum.POSITION_APP_VALUE)
|
||||
.getMeshPacketsFrom(destNum!!, PortNum.POSITION_APP.value)
|
||||
.map { packets ->
|
||||
packets
|
||||
.mapNotNull { it.toPosition() }
|
||||
.asFlow()
|
||||
.distinctUntilChanged { old, new ->
|
||||
old.time == new.time || (old.latitudeI == new.latitudeI && old.longitudeI == new.longitudeI)
|
||||
old.time == new.time || (old.latitude_i == new.latitude_i && old.longitude_i == new.longitude_i)
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue