mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-06-26 06:25:24 -04:00
feat(node): add GPX export to position log screen (#5919)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1
.skills/compose-ui/strings-index.txt
generated
1
.skills/compose-ui/strings-index.txt
generated
@@ -530,6 +530,7 @@ expires
|
||||
### EXPORT ###
|
||||
export_configuration
|
||||
export_data_csv
|
||||
export_gpx
|
||||
export_keys
|
||||
export_keys_confirmation
|
||||
export_tak_data_package
|
||||
|
||||
@@ -554,6 +554,7 @@
|
||||
<!-- EXPORT -->
|
||||
<string name="export_configuration">Export configuration</string>
|
||||
<string name="export_data_csv">Export all packets</string>
|
||||
<string name="export_gpx">Export GPX</string>
|
||||
<string name="export_keys">Export Keys</string>
|
||||
<string name="export_keys_confirmation">Exports public and private keys to a file. Please store somewhere securely.</string>
|
||||
<string name="export_tak_data_package">Export TAK Data Package</string>
|
||||
|
||||
@@ -374,6 +374,12 @@ open class MetricsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun savePositionGpx(uri: CommonUri, data: List<org.meshtastic.proto.Position>, trackName: String) {
|
||||
safeLaunch(context = dispatchers.io, tag = "exportGpx") {
|
||||
fileService.write(uri) { sink -> sink.writeUtf8(buildGpx(data, trackName)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun saveLocalStatsCSV(uri: CommonUri, data: List<Telemetry>) {
|
||||
exportCsv(
|
||||
uri = uri,
|
||||
@@ -544,3 +550,25 @@ open class MetricsViewModel(
|
||||
private const val ONE_WIRE_SENSOR_COUNT = 8
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildGpx(positions: List<org.meshtastic.proto.Position>, trackName: String): String {
|
||||
val trkpts = buildString {
|
||||
for (pos in positions) {
|
||||
val lat = formatString("%.7f", (pos.latitude_i ?: 0) * GeoConstants.DEG_D)
|
||||
val lon = formatString("%.7f", (pos.longitude_i ?: 0) * GeoConstants.DEG_D)
|
||||
append(" <trkpt lat=\"$lat\" lon=\"$lon\">")
|
||||
if ((pos.altitude ?: 0) != 0) append("<ele>${pos.altitude}</ele>")
|
||||
if (pos.time > 0) append("<time>${Instant.fromEpochSeconds(pos.time.toLong())}</time>")
|
||||
append("</trkpt>\n")
|
||||
}
|
||||
}
|
||||
val name = trackName.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
return """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx version="1.1" creator="Meshtastic" xmlns="http://www.topografix.com/GPX/1/1">
|
||||
<trk>
|
||||
<name>$name</name>
|
||||
<trkseg>
|
||||
$trkpts </trkseg>
|
||||
</trk>
|
||||
</gpx>"""
|
||||
}
|
||||
|
||||
@@ -27,8 +27,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.clear
|
||||
import org.meshtastic.core.resources.export_gpx
|
||||
import org.meshtastic.core.resources.position_log
|
||||
import org.meshtastic.core.ui.icon.Delete
|
||||
import org.meshtastic.core.ui.icon.FileDownload
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.Refresh
|
||||
import org.meshtastic.core.ui.util.LocalNodeTrackMapProvider
|
||||
@@ -40,6 +42,8 @@ fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
|
||||
val positions = state.positionLogs
|
||||
|
||||
val exportPositionLauncher = rememberSaveFileLauncher { uri -> viewModel.savePositionCSV(uri, positions) }
|
||||
val nodeName = state.node?.user?.long_name ?: ""
|
||||
val exportGpxLauncher = rememberSaveFileLauncher { uri -> viewModel.savePositionGpx(uri, positions, nodeName) }
|
||||
|
||||
val trackMap = LocalNodeTrackMapProvider.current
|
||||
val destNum = state.node?.num ?: 0
|
||||
@@ -48,12 +52,18 @@ fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
|
||||
onNavigateUp = onNavigateUp,
|
||||
telemetryType = null,
|
||||
titleRes = Res.string.position_log,
|
||||
nodeName = state.node?.user?.long_name ?: "",
|
||||
nodeName = nodeName,
|
||||
data = positions,
|
||||
timeProvider = { it.time.toDouble() },
|
||||
onExportCsv = { exportPositionLauncher("position.csv", "text/csv") },
|
||||
extraActions = {
|
||||
if (positions.isNotEmpty()) {
|
||||
IconButton(onClick = { exportGpxLauncher("track.gpx", "application/gpx+xml") }) {
|
||||
Icon(
|
||||
imageVector = MeshtasticIcons.FileDownload,
|
||||
contentDescription = stringResource(Res.string.export_gpx),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { viewModel.clearPosition() }) {
|
||||
Icon(imageVector = MeshtasticIcons.Delete, contentDescription = stringResource(Res.string.clear))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user