Skip to content

Commit

Permalink
Merge pull request #3573 from goldbattle/record_gps_tracks
Browse files Browse the repository at this point in the history
Recording and Uploading of GPX Traces
  • Loading branch information
westnordost authored May 15, 2022
2 parents 5e90700 + b8413a1 commit cad44a3
Show file tree
Hide file tree
Showing 26 changed files with 364 additions and 46 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ dependencies {
implementation("de.westnordost:osmapi-map:2.0")
implementation("de.westnordost:osmapi-changesets:2.0")
implementation("de.westnordost:osmapi-notes:2.0")
implementation("de.westnordost:osmapi-traces:2.0")
implementation("de.westnordost:osmapi-user:2.0")
implementation("com.squareup.okhttp3:okhttp:3.14.9")
implementation("se.akerfeldt:okhttp-signpost:1.1.0")
Expand Down
27 changes: 23 additions & 4 deletions app/src/main/assets/map_theme/jawg/streetcomplete.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ global:
geometry_line_color: '#44D14000'
geometry_point_color: '#88D14000'
track_color: '#44536dfe'
track_color_record: '#44fe1616'
old_track_color: '#22536dfe'

textures:
Expand Down Expand Up @@ -126,16 +127,25 @@ layers:
streetcomplete_track:
data: { source: streetcomplete_track }
current:
filter: { old: [false] }
filter: { old: [false], record: [false] }
draw:
track-lines:
color: global.track_color
width: [[14, 6px],[18, 12px]]
collide: false
join: round
order: 1000
record:
filter: { record: [true] }
draw:
track-lines:
color: global.track_color_record
width: [[14, 6px],[18, 12px]]
collide: false
join: round
order: 1000
old:
filter: { old: [true] }
filter: { old: [true], record: [false] }
draw:
track-lines:
color: global.old_track_color
Expand All @@ -146,16 +156,25 @@ layers:
streetcomplete_track2:
data: { source: streetcomplete_track2 }
current:
filter: { old: [false] }
filter: { old: [false], record: [false] }
draw:
track-lines:
color: global.track_color
width: [[14, 6px],[18, 12px]]
collide: false
join: round
order: 1000
record:
filter: { record: [true] }
draw:
track-lines:
color: global.track_color_record
width: [[14, 6px],[18, 12px]]
collide: false
join: round
order: 1000
old:
filter: { old: [true] }
filter: { old: [true], record: [false] }
draw:
track-lines:
color: global.old_track_color
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/de/westnordost/streetcomplete/Prefs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ object Prefs {
const val OSM_USER_ID = "osm.userid"
const val OSM_USER_NAME = "osm.username"
const val OSM_UNREAD_MESSAGES = "osm.unread_messages"
const val OSM_HAS_UPLOAD_TRACES_PERMISSION = "osm.upload_traces_permission"
const val USER_DAYS_ACTIVE = "days_active"
const val USER_GLOBAL_RANK = "user_global_rank"
const val USER_LAST_TIMESTAMP_ACTIVE = "last_timestamp_active"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import de.westnordost.streetcomplete.data.osm.mapdata.MapDataApi
import de.westnordost.streetcomplete.data.osm.mapdata.MapDataApiImpl
import de.westnordost.streetcomplete.data.osmnotes.NotesApi
import de.westnordost.streetcomplete.data.osmnotes.NotesApiImpl
import de.westnordost.streetcomplete.data.osmtracks.TracksApi
import de.westnordost.streetcomplete.data.osmtracks.TracksApiImpl
import de.westnordost.streetcomplete.data.user.OAuthStore
import oauth.signpost.OAuthConsumer
import org.koin.androidx.workmanager.dsl.worker
Expand All @@ -17,6 +19,7 @@ val osmApiModule = module {
factory { Cleaner(get(), get(), get()) }
factory<MapDataApi> { MapDataApiImpl(get()) }
factory<NotesApi> { NotesApiImpl(get()) }
factory<TracksApi> { TracksApiImpl(get()) }
factory { Preloader(get(named("CountryBoundariesFuture")), get(named("FeatureDictionaryFuture"))) }
factory { UserApi(get()) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,10 @@ class StreetCompleteSQLiteOpenHelper(context: Context, dbName: String) :
)
db.execSQL("DROP TABLE $oldGeometryTableName;")
}
if (oldVersion <= 5 && newVersion > 5) {
db.execSQL("ALTER TABLE ${NoteEditsTable.NAME} ADD COLUMN ${NoteEditsTable.Columns.TRACK} text DEFAULT '[]' NOT NULL")
}
}
}

private const val DB_VERSION = 5
private const val DB_VERSION = 6
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package de.westnordost.streetcomplete.data.osmnotes.edits
import de.westnordost.streetcomplete.data.edithistory.Edit
import de.westnordost.streetcomplete.data.edithistory.NoteEditKey
import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
import de.westnordost.streetcomplete.data.osmtracks.Trackpoint

/** Contains all necessary information to create/comment on an OSM note. */
data class NoteEdit(
Expand Down Expand Up @@ -31,8 +32,11 @@ data class NoteEdit(
/** whether this edit has been uploaded already */
override val isSynced: Boolean,

/** Whether the images attached still need activation. Already true if imagePaths is empty */
val imagesNeedActivation: Boolean
/** whether the images attached still need activation. Already true if imagePaths is empty */
val imagesNeedActivation: Boolean,

/** attached GPS location history */
val track: List<Trackpoint>,
) : Edit {
override val isUndoable: Boolean get() = !isSynced
override val key: NoteEditKey get() = NoteEditKey(id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package de.westnordost.streetcomplete.data.osmnotes.edits
import de.westnordost.streetcomplete.data.osm.mapdata.BoundingBox
import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
import de.westnordost.streetcomplete.data.osmnotes.Note
import de.westnordost.streetcomplete.data.osmtracks.Trackpoint
import java.lang.System.currentTimeMillis
import java.util.concurrent.CopyOnWriteArrayList

Expand All @@ -19,7 +20,8 @@ class NoteEditsController(
action: NoteEditAction,
position: LatLon,
text: String? = null,
imagePaths: List<String> = emptyList()
imagePaths: List<String> = emptyList(),
track: List<Trackpoint> = emptyList(),
) {
val edit = NoteEdit(
0,
Expand All @@ -30,7 +32,8 @@ class NoteEditsController(
imagePaths,
currentTimeMillis(),
false,
imagePaths.isNotEmpty()
imagePaths.isNotEmpty(),
track,
)
synchronized(this) { editsDB.add(edit) }
onAddedEdit(edit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditsTable.Columns.
import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditsTable.Columns.LONGITUDE
import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditsTable.Columns.NOTE_ID
import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditsTable.Columns.TEXT
import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditsTable.Columns.TRACK
import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditsTable.Columns.TYPE
import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditsTable.NAME
import kotlinx.serialization.decodeFromString
Expand Down Expand Up @@ -123,6 +124,7 @@ class NoteEditsDao(private val db: Database) {
TEXT to text,
IMAGE_PATHS to Json.encodeToString(imagePaths),
IMAGES_NEED_ACTIVATION to if (imagesNeedActivation) 1 else 0,
TRACK to Json.encodeToString(track),
TYPE to action.name
)

Expand All @@ -135,6 +137,7 @@ class NoteEditsDao(private val db: Database) {
Json.decodeFromString(getString(IMAGE_PATHS)),
getLong(CREATED_TIMESTAMP),
getInt(IS_SYNCED) == 1,
getInt(IMAGES_NEED_ACTIVATION) == 1
getInt(IMAGES_NEED_ACTIVATION) == 1,
Json.decodeFromString(getString(TRACK)),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.koin.dsl.module
val noteEditsModule = module {
factory { NoteEditsDao(get()) }

single { NoteEditsUploader(get(), get(), get(), get()) }
single { NoteEditsUploader(get(), get(), get(), get(), get()) }
single { NoteEditsController(get()) }
single<NoteEditsSource> { get<NoteEditsController>() }
single { NotesWithEditsSource(get(), get(), get()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ object NoteEditsTable {
const val TEXT = "text"
const val IMAGE_PATHS = "image_paths"
const val IMAGES_NEED_ACTIVATION = "images_need_activation"
const val TRACK = "track"
}

const val CREATE = """
Expand All @@ -27,6 +28,7 @@ object NoteEditsTable {
${Columns.TEXT} text,
${Columns.IMAGE_PATHS} text NOT NULL,
${Columns.IMAGES_NEED_ACTIVATION} int NOT NULL,
${Columns.TRACK} text NOT NULL,
${Columns.TYPE} varchar(255)
);
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import de.westnordost.streetcomplete.data.osmnotes.StreetCompleteImageUploader
import de.westnordost.streetcomplete.data.osmnotes.deleteImages
import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditAction.COMMENT
import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditAction.CREATE
import de.westnordost.streetcomplete.data.osmtracks.Trackpoint
import de.westnordost.streetcomplete.data.osmtracks.TracksApi
import de.westnordost.streetcomplete.data.upload.ConflictException
import de.westnordost.streetcomplete.data.upload.OnUploadedChangeListener
import kotlinx.coroutines.CoroutineName
Expand All @@ -21,6 +23,7 @@ class NoteEditsUploader(
private val noteEditsController: NoteEditsController,
private val noteController: NoteController,
private val notesApi: NotesApi,
private val tracksApi: TracksApi,
private val imageUploader: StreetCompleteImageUploader
) {
var uploadedChangeListener: OnUploadedChangeListener? = null
Expand Down Expand Up @@ -62,8 +65,12 @@ class NoteEditsUploader(
}

private fun uploadEdit(edit: NoteEdit) {
val text = edit.text.orEmpty() + uploadAndGetAttachedPhotosText(edit.imagePaths)
// try to upload the image and track if we have them
val imageText = uploadAndGetAttachedPhotosText(edit.imagePaths)
val trackText = uploadAndGetAttachedTrackText(edit.track, edit.text)
val text = edit.text.orEmpty() + imageText + trackText

// done, try to upload the note to OSM
try {
val note = when (edit.action) {
CREATE -> notesApi.create(edit.position, text)
Expand Down Expand Up @@ -112,6 +119,15 @@ class NoteEditsUploader(
return ""
}

private fun uploadAndGetAttachedTrackText(
trackpoints: List<Trackpoint>,
noteText: String?
): String {
if (trackpoints.isEmpty()) return ""
val track = tracksApi.create(trackpoints, noteText)
return "\n\nGPS Trace: \nhttps://www.openstreetmap.org/user/${track.userName}/traces/${track.id}"
}

companion object {
private const val TAG = "NoteEditsUploader"
private const val NOTE = "NOTE"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package de.westnordost.streetcomplete.data.osmtracks

import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
import kotlinx.serialization.Serializable

@Serializable
data class Track(
val id: Long,
val userName: String,
)

@Serializable
data class Trackpoint(
val position: LatLon,
val time: Long,
val horizontalDilutionOfPrecision: Float,
val elevation: Float,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package de.westnordost.streetcomplete.data.osmtracks

import de.westnordost.streetcomplete.data.download.ConnectionException
import de.westnordost.streetcomplete.data.user.AuthorizationException

/**
* Creates GPS / GPX trackpoint histories
*/
interface TracksApi {

/**
* Create a new GPX track history
*
* @param trackpoints history of recorded trackpoints
* @param noteText optional text appended to the track
*
* @throws AuthorizationException if this application is not authorized to write notes
* (Permission.READ_GPS_TRACES, Permission.WRITE_GPS_TRACES)
* @throws ConnectionException if a temporary network connection problem occurs
*
* @return the new track
*/
fun create(trackpoints: List<Trackpoint>, noteText: String?): Track
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package de.westnordost.streetcomplete.data.osmtracks

import de.westnordost.osmapi.OsmConnection
import de.westnordost.osmapi.common.errors.OsmApiException
import de.westnordost.osmapi.common.errors.OsmApiReadResponseException
import de.westnordost.osmapi.common.errors.OsmAuthorizationException
import de.westnordost.osmapi.common.errors.OsmConnectionException
import de.westnordost.osmapi.map.data.OsmLatLon
import de.westnordost.osmapi.traces.GpsTraceDetails
import de.westnordost.osmapi.traces.GpsTracesApi
import de.westnordost.osmapi.traces.GpsTrackpoint
import de.westnordost.streetcomplete.ApplicationConstants
import de.westnordost.streetcomplete.data.download.ConnectionException
import de.westnordost.streetcomplete.data.user.AuthorizationException
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter

class TracksApiImpl(osm: OsmConnection) : TracksApi {
private val api: GpsTracesApi = GpsTracesApi(osm)

override fun create(trackpoints: List<Trackpoint>, noteText: String?): Track = wrapExceptions {
// Filename is just the start of the track
// https://stackoverflow.com/a/49862573/7718197
val name = DateTimeFormatter
.ofPattern("yyyy_MM_dd'T'HH_mm_ss.SSSSSS'Z'")
.withZone(ZoneOffset.UTC)
.format(Instant.ofEpochSecond(trackpoints[0].time)) + ".gpx"
val visibility = GpsTraceDetails.Visibility.IDENTIFIABLE
val description = noteText ?: "Uploaded via ${ApplicationConstants.USER_AGENT}"
val tags = listOf(ApplicationConstants.NAME.lowercase())

// Generate history of trackpoints
val history = trackpoints.mapIndexed { idx, it ->
GpsTrackpoint(
OsmLatLon(it.position.latitude, it.position.longitude),
Instant.ofEpochMilli(it.time),
idx == 0,
it.horizontalDilutionOfPrecision,
it.elevation
)
}

// Finally query the API and return!
val traceId = api.create(name, visibility, description, tags, history)
val details = api.get(traceId)
Track(details.id, details.userName)
}
}

private inline fun <T> wrapExceptions(block: () -> T): T =
try {
block()
} catch (e: OsmAuthorizationException) {
throw AuthorizationException(e.message, e)
} catch (e: OsmConnectionException) {
throw ConnectionException(e.message, e)
} catch (e: OsmApiReadResponseException) {
// probably a temporary connection error
throw ConnectionException(e.message, e)
} catch (e: OsmApiException) {
// request timeout is a temporary connection error
throw if (e.errorCode == 408) ConnectionException(e.message, e) else e
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditAction
import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditsController
import de.westnordost.streetcomplete.data.osmnotes.notequests.OsmNoteQuest
import de.westnordost.streetcomplete.data.osmnotes.notequests.OsmNoteQuestController
import de.westnordost.streetcomplete.data.osmtracks.Trackpoint
import de.westnordost.streetcomplete.osm.KEYS_THAT_SHOULD_BE_REMOVED_WHEN_SHOP_IS_REPLACED
import de.westnordost.streetcomplete.osm.removeCheckDates
import de.westnordost.streetcomplete.quests.note_discussion.NoteAnswer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.collections.ArrayList

/** Controls the workflow of quests: Solving them, hiding them instead, splitting the way instead,
* undoing, etc. */
Expand Down Expand Up @@ -66,10 +66,11 @@ class QuestController(
suspend fun createNote(
text: String,
imagePaths: List<String>,
position: LatLon
position: LatLon,
track: List<Trackpoint>,
) = withContext(Dispatchers.IO) {
val fullText = "$text\n\nvia ${ApplicationConstants.USER_AGENT}"
noteEditsController.add(0, NoteEditAction.CREATE, position, fullText, imagePaths)
noteEditsController.add(0, NoteEditAction.CREATE, position, fullText, imagePaths, track)
}

/** Split a way for the given OSM Quest.
Expand Down
Loading

0 comments on commit cad44a3

Please sign in to comment.