diff --git a/.skills/compose-ui/strings-index.txt b/.skills/compose-ui/strings-index.txt index f7c85daad..88cd5cc9b 100644 --- a/.skills/compose-ui/strings-index.txt +++ b/.skills/compose-ui/strings-index.txt @@ -530,6 +530,7 @@ expires ### EXPORT ### export_configuration export_data_csv +export_gpx export_keys export_keys_confirmation export_tak_data_package diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml index 642059bab..b1d27208e 100644 --- a/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/core/resources/src/commonMain/composeResources/values/strings.xml @@ -554,6 +554,7 @@ Export configuration Export all packets + Export GPX Export Keys Exports public and private keys to a file. Please store somewhere securely. Export TAK Data Package diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt index 032663661..3225f3b6f 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt @@ -374,6 +374,12 @@ open class MetricsViewModel( } } + fun savePositionGpx(uri: CommonUri, data: List, trackName: String) { + safeLaunch(context = dispatchers.io, tag = "exportGpx") { + fileService.write(uri) { sink -> sink.writeUtf8(buildGpx(data, trackName)) } + } + } + fun saveLocalStatsCSV(uri: CommonUri, data: List) { exportCsv( uri = uri, @@ -544,3 +550,25 @@ open class MetricsViewModel( private const val ONE_WIRE_SENSOR_COUNT = 8 } } + +private fun buildGpx(positions: List, 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(" ") + if ((pos.altitude ?: 0) != 0) append("${pos.altitude}") + if (pos.time > 0) append("") + append("\n") + } + } + val name = trackName.replace("&", "&").replace("<", "<").replace(">", ">") + return """ + + + $name + +$trkpts + +""" +} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt index bcb1bba40..b1ef685e5 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt @@ -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)) }