Skip to content

Commit

Permalink
Merge pull request #1957 from kylecorry31/path-group-export
Browse files Browse the repository at this point in the history
Path group export
  • Loading branch information
kylecorry31 authored Sep 9, 2023
2 parents 906cc51 + 3ef9b27 commit 5ef7016
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.kylecorry.trail_sense.navigation.paths.domain

data class FullPath(val path: Path, val points: List<PathPoint>, val parent: PathGroup? = null)
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,24 @@ import com.kylecorry.andromeda.gpx.GPXWaypoint

class PathGPXConverter {

fun toGPX(name: String?, path: List<PathPoint>): GPXData {
val waypoints = path.map {
fun toGPX(paths: List<FullPath>): GPXData {
val tracks = paths.map { toGPX(it) }
return GPXData(emptyList(), tracks.flatMap { it.tracks }, emptyList())
}

fun toGPX(path: FullPath): GPXData {
val waypoints = path.points.map {
GPXWaypoint(it.coordinate, elevation = it.elevation, time = it.time)
}
val pathId = path.firstOrNull()?.pathId ?: 0
val pathId = path.points.firstOrNull()?.pathId ?: 0

val trackSegment = GPXTrackSegment(waypoints)
val track = GPXTrack(name, id = pathId, segments = listOf(trackSegment))
val track = GPXTrack(
path.path.name,
id = pathId,
segments = listOf(trackSegment),
group = path.parent?.name
)
return GPXData(emptyList(), listOf(track), emptyList())
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
package com.kylecorry.trail_sense.navigation.paths.domain

import androidx.annotation.ColorInt
import com.kylecorry.trail_sense.shared.colors.AppColor

data class PathStyle(
val line: LineStyle,
val point: PathPointColoringStyle,
@ColorInt val color: Int,
val visible: Boolean
)
){
companion object {
fun default(): PathStyle {
return PathStyle(
LineStyle.Dotted,
PathPointColoringStyle.None,
AppColor.Gray.color,
true
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.kylecorry.trail_sense.navigation.paths.infrastructure.persistence
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
import com.kylecorry.andromeda.core.coroutines.onIO
import com.kylecorry.trail_sense.navigation.paths.domain.PathPoint
import com.kylecorry.trail_sense.navigation.paths.domain.WaypointEntity
import com.kylecorry.trail_sense.shared.database.AppDatabase
Expand Down Expand Up @@ -71,12 +72,12 @@ class WaypointRepo private constructor(context: Context) : IWaypointRepo {
}
}

override suspend fun getAllInPaths(pathIds: List<Long>): List<PathPoint> {
override suspend fun getAllInPaths(pathIds: List<Long>): List<PathPoint> = onIO {
val points = mutableListOf<WaypointEntity>()
for (pathId in pathIds) {
points.addAll(waypointDao.getAllInPathSync(pathId))
}
return points.map { it.toPathPoint() }
points.map { it.toPathPoint() }
}

override fun getAllInPathLive(pathId: Long): LiveData<List<PathPoint>> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.kylecorry.trail_sense.navigation.paths.ui

enum class PathGroupAction {
Export,
Delete,
Rename,
Open,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ class PathGroupListItemMapper(
PathGroupAction.Rename
)
},
ListMenuItem(context.getString(R.string.export)) {
actionHandler(
value,
PathGroupAction.Export
)
},
ListMenuItem(context.getString(R.string.delete)) {
actionHandler(
value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,15 @@ class PathsFragment : BoundFragment<FragmentPathsBinding>() {
binding.pathsTitle.rightButton.setOnClickListener {
val defaultSort = prefs.navigation.pathSort
Pickers.menu(
it, listOf(
getString(R.string.sort_by, getSortString(defaultSort))
it,
listOf(
getString(R.string.sort_by, getSortString(defaultSort)),
getString(R.string.export)
)
) { selected ->
when (selected) {
0 -> changeSort()
1 -> exportCurrentGroup()
}
true
}
Expand Down Expand Up @@ -226,6 +229,7 @@ class PathsFragment : BoundFragment<FragmentPathsBinding>() {
PathGroupAction.Rename -> renameGroup(group)
PathGroupAction.Open -> manager.open(group.id)
PathGroupAction.Move -> movePath(group)
PathGroupAction.Export -> exportPath(group)
}
}

Expand Down Expand Up @@ -296,11 +300,15 @@ class PathsFragment : BoundFragment<FragmentPathsBinding>() {
command.execute(manager.root?.id)
}

private fun exportPath(path: Path) {
private fun exportPath(path: IPath?) {
val command = ExportPathCommand(requireContext(), this, gpxService, pathService)
command.execute(path)
}

private fun exportCurrentGroup() {
exportPath(manager.root)
}

private fun deletePath(path: Path) {
val command = DeletePathCommand(requireContext(), this, pathService)
command.execute(path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@ import android.content.Context
import androidx.lifecycle.LifecycleOwner
import com.kylecorry.andromeda.alerts.Alerts
import com.kylecorry.andromeda.core.coroutines.BackgroundMinimumState
import com.kylecorry.andromeda.core.coroutines.onIO
import com.kylecorry.andromeda.core.coroutines.onMain
import com.kylecorry.andromeda.fragments.inBackground
import com.kylecorry.andromeda.gpx.GPXData
import com.kylecorry.andromeda.pickers.CoroutinePickers
import com.kylecorry.trail_sense.R
import com.kylecorry.trail_sense.navigation.paths.domain.FullPath
import com.kylecorry.trail_sense.navigation.paths.domain.IPath
import com.kylecorry.trail_sense.navigation.paths.domain.IPathService
import com.kylecorry.trail_sense.navigation.paths.domain.Path
import com.kylecorry.trail_sense.navigation.paths.domain.PathGPXConverter
import com.kylecorry.trail_sense.navigation.paths.domain.PathGroup
import com.kylecorry.trail_sense.navigation.paths.infrastructure.PathGroupLoader
import com.kylecorry.trail_sense.navigation.paths.infrastructure.persistence.PathService
import com.kylecorry.trail_sense.navigation.paths.ui.PathNameFactory
import com.kylecorry.trail_sense.shared.io.IOService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
Expand All @@ -22,27 +30,70 @@ class ExportPathCommand(
private val lifecycleOwner: LifecycleOwner,
private val gpxService: IOService<GPXData>,
private val pathService: IPathService = PathService.getInstance(context)
) : IPathCommand {
) {

override fun execute(path: Path) {
private val pathNameFactory = PathNameFactory(context)

fun execute(path: IPath?) {
lifecycleOwner.inBackground(BackgroundMinimumState.Created) {
val waypoints = pathService.getWaypoints(path.id)
val gpx = PathGPXConverter().toGPX(path.name, waypoints)
// Load the paths and groups (without waypoints)
val all = getPaths(path)
val paths = all.filterIsInstance<Path>()
val groups = all.filterIsInstance<PathGroup>().associateBy { it.id }

if (paths.isEmpty()){
return@inBackground
}

val chosenIds = if (path is Path) {
listOf(path.id)
} else {
val selection = CoroutinePickers.items(
context,
context.getString(R.string.export),
paths.map { pathNameFactory.getName(it) },
List(paths.size) { it }
) ?: return@inBackground

if (selection.isEmpty()){
return@inBackground
}

selection.map { paths[it].id }
}

// Load the waypoints and associate them with the paths
val waypoints = pathService.getWaypoints(chosenIds)
val pathsToExport = paths
.filter { chosenIds.contains(it.id) }
.map {
val parent = it.parentId?.let { id -> groups[id] }
FullPath(it, waypoints[it.id] ?: emptyList(), parent)
}

// Export to a GPX file
val gpx = PathGPXConverter().toGPX(pathsToExport)
val exportFile = "trail-sense-${Instant.now().epochSecond}.gpx"
val success = gpxService.export(gpx, exportFile)
withContext(Dispatchers.Main) {

// Notify the user
onMain {
if (success) {
Alerts.toast(
context,
context.getString(R.string.path_exported)
)
Alerts.toast(context, context.getString(R.string.path_exported))
} else {
Alerts.toast(
context,
context.getString(R.string.export_path_error)
)
Alerts.toast(context, context.getString(R.string.export_path_error))
}
}
}
}

private suspend fun getPaths(path: IPath?): List<IPath> = onIO {
if (path is Path) {
listOf(path)
} else {
listOfNotNull(path) + pathService.loader().getChildren(path?.id)
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import com.kylecorry.andromeda.core.coroutines.onIO
import com.kylecorry.andromeda.core.filterIndices
import com.kylecorry.andromeda.fragments.inBackground
import com.kylecorry.andromeda.gpx.GPXData
import com.kylecorry.andromeda.gpx.GPXWaypoint
import com.kylecorry.andromeda.pickers.CoroutinePickers
import com.kylecorry.trail_sense.R
import com.kylecorry.trail_sense.navigation.paths.domain.FullPath
import com.kylecorry.trail_sense.navigation.paths.domain.IPathService
import com.kylecorry.trail_sense.navigation.paths.domain.Path
import com.kylecorry.trail_sense.navigation.paths.domain.PathGroup
import com.kylecorry.trail_sense.navigation.paths.domain.PathMetadata
import com.kylecorry.trail_sense.navigation.paths.domain.PathPoint
import com.kylecorry.trail_sense.navigation.paths.domain.PathSimplificationQuality
Expand All @@ -29,10 +32,12 @@ class ImportPathsCommand(
private val prefs: IPathPreferences = UserPreferences(context).navigation
) {

private val style = prefs.defaultPathStyle

fun execute(parentId: Long?) {
lifecycleOwner.inBackground(BackgroundMinimumState.Created) {
val gpx = gpxService.import() ?: return@inBackground
val paths = mutableListOf<Pair<String?, List<PathPoint>>>()
val paths = mutableListOf<FullPath>()

// Get the tracks and routes from the GPX
paths.addAll(getTracks(gpx))
Expand All @@ -43,7 +48,7 @@ class ImportPathsCommand(
context,
context.getString(R.string.import_btn),
paths.map {
it.first ?: context.getString(android.R.string.untitled)
it.path.name ?: context.getString(android.R.string.untitled)
},
paths.indices.toList()
) ?: return@inBackground
Expand All @@ -66,44 +71,60 @@ class ImportPathsCommand(
}
}

private suspend fun getRoutes(gpx: GPXData): List<Pair<String?, List<PathPoint>>> = onDefault {
val paths = mutableListOf<Pair<String?, List<PathPoint>>>()
private suspend fun getRoutes(gpx: GPXData): List<FullPath> = onDefault {
// Groups are a Trail Sense concept, so routes don't have groups
val paths = mutableListOf<FullPath>()
for (route in gpx.routes) {
paths.add(route.name to route.points.map {
PathPoint(
0, 0, it.coordinate, it.elevation, it.time
)
})
val path = Path(0, route.name, style, PathMetadata.empty)
paths.add(FullPath(path, route.points.toPathPoints()))
}
paths
}

private suspend fun getTracks(gpx: GPXData): List<Pair<String?, List<PathPoint>>> = onDefault {
val paths = mutableListOf<Pair<String?, List<PathPoint>>>()
private suspend fun getTracks(gpx: GPXData): List<FullPath> = onDefault {
val paths = mutableListOf<FullPath>()
for (track in gpx.tracks) {
for ((points) in track.segments) {
paths.add(track.name to points.map {
PathPoint(
0, 0, it.coordinate, it.elevation, it.time
)
})
val path = Path(0, track.name, style, PathMetadata.empty)
val parent = track.group?.let {
PathGroup(0, it)
}
paths.add(FullPath(path, points.toPathPoints(), parent))
}
}
paths
}

private fun List<GPXWaypoint>.toPathPoints(): List<PathPoint> {
return map {
PathPoint(0, 0, it.coordinate, it.elevation, it.time)
}
}

private suspend fun importPaths(
paths: List<Pair<String?, List<PathPoint>>>, parentId: Long?
paths: List<FullPath>, parentId: Long?
) = onIO {
val shouldSimplify = prefs.simplifyPathOnImport
val style = prefs.defaultPathStyle
for ((name, waypoints) in paths) {

val groupNames = paths.mapNotNull { it.parent?.name }.distinct()
val groupIdMap = mutableMapOf<String, Long>()
for (groupName in groupNames) {
val id = pathService.addGroup(PathGroup(0, groupName, parentId))
groupIdMap[groupName] = id
}

for (path in paths) {
val parent = if (path.parent != null) {
groupIdMap[path.parent.name]
} else {
parentId
}
// Create the path
val pathToCreate = Path(0, name, style, PathMetadata.empty, parentId = parentId)
val pathToCreate = path.path.copy(parentId = parent)
val pathId = pathService.addPath(pathToCreate)

// Add the waypoints to the path
pathService.addWaypointsToPath(waypoints, pathId)
pathService.addWaypointsToPath(path.points, pathId)

// Simplify the path
if (shouldSimplify) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.kylecorry.trail_sense.shared.grouping.persistence

import com.kylecorry.andromeda.core.coroutines.onIO
import com.kylecorry.trail_sense.shared.grouping.Groupable

class GroupLoader<T : Groupable>(
Expand All @@ -15,9 +16,9 @@ class GroupLoader<T : Groupable>(
return groupLoader.invoke(id)
}

private suspend fun loadChildren(id: Long?, maxDepth: Int?): List<T> {
private suspend fun loadChildren(id: Long?, maxDepth: Int?): List<T> = onIO {
if (maxDepth != null && maxDepth <= 0) {
return emptyList()
return@onIO emptyList()
}

val children = childLoader.invoke(id)
Expand All @@ -27,6 +28,6 @@ class GroupLoader<T : Groupable>(
maxDepth - 1
}
val subchildren = children.filter { it.isGroup }.flatMap { loadChildren(it.id, newDepth) }
return children + subchildren
children + subchildren
}
}

0 comments on commit 5ef7016

Please sign in to comment.