new feature: you can mark a recorded waypoint as POI see #71

master
y20k 2020-07-30 13:59:07 +02:00
parent 6290cbd709
commit 18d043cafe
No known key found for this signature in database
GPG Key ID: 824D4259F41FAFF6
18 changed files with 120 additions and 209 deletions

View File

@ -10,7 +10,7 @@ android {
defaultConfig {
applicationId 'org.y20k.trackbook'
minSdkVersion 25
targetSdkVersion 27
targetSdkVersion 29
versionCode 42
versionName '2.0.5'
resConfigs "en", "da", "de", "fr", "hr", "id", "it", "ja", "nb-rNO", "nl", "sv", "zh-rCN"

View File

@ -30,7 +30,6 @@ import androidx.preference.PreferenceManager
import com.google.android.material.bottomnavigation.BottomNavigationView
import org.osmdroid.config.Configuration
import org.y20k.trackbook.helpers.AppThemeHelper
import org.y20k.trackbook.helpers.ImportHelper
import org.y20k.trackbook.helpers.LogHelper
import org.y20k.trackbook.helpers.PreferencesHelper
@ -91,12 +90,6 @@ class MainActivity : AppCompatActivity() {
}
}
// convert old tracks (one-time import)
if (PreferencesHelper.isHouseKeepingNecessary(this)) {
ImportHelper.convertOldTracks(this)
PreferencesHelper.saveHouseKeepingNecessaryState(this)
}
// register listener for changes in shared preferences
PreferenceManager.getDefaultSharedPreferences(this as Context).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
}

View File

@ -44,7 +44,7 @@ import org.y20k.trackbook.ui.MapFragmentLayoutHolder
/*
* MapFragment class
*/
class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener {
class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlay.MarkerListener {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(MapFragment::class.java)
@ -75,7 +75,7 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener {
/* Overrides onStop from Fragment */
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// initialize layout
layout = MapFragmentLayoutHolder(activity as Context, inflater, container, currentBestLocation, trackingState)
layout = MapFragmentLayoutHolder(activity as Context, this as MapOverlay.MarkerListener, inflater, container, currentBestLocation, trackingState)
// set up buttons
layout.currentLocationButton.setOnClickListener {
@ -178,6 +178,17 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener {
}
/* Overrides onMarkerTapped from MarkerListener */
override fun onMarkerTapped(latitude: Double, longitude: Double) {
super.onMarkerTapped(latitude, longitude)
if (bound) {
track = TrackHelper.toggleStarred(activity as Context, track, latitude, longitude)
layout.overlayCurrentTrack(track, trackingState)
trackerService.track = track
}
}
/* Start tracker service */
private fun startTrackerService() {
val intent = Intent(activity, TrackerService::class.java)

View File

@ -41,10 +41,12 @@ import org.y20k.trackbook.core.Track
import org.y20k.trackbook.dialogs.RenameTrackDialog
import org.y20k.trackbook.helpers.FileHelper
import org.y20k.trackbook.helpers.LogHelper
import org.y20k.trackbook.helpers.MapOverlay
import org.y20k.trackbook.helpers.TrackHelper
import org.y20k.trackbook.ui.TrackFragmentLayoutHolder
class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDialog.YesNoDialogListener {
class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDialog.YesNoDialogListener, MapOverlay.MarkerListener {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TrackFragment::class.java)
@ -71,7 +73,7 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
/* Overrides onCreateView from Fragment */
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// initialize layout
layout = TrackFragmentLayoutHolder(activity as Context, inflater, container, track)
layout = TrackFragmentLayoutHolder(activity as Context, this as MapOverlay.MarkerListener, inflater, container, track)
// set up share button
layout.shareButton.setOnClickListener {
@ -162,6 +164,19 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
}
/* Overrides onMarkerTapped from MarkerListener */
override fun onMarkerTapped(latitude: Double, longitude: Double) {
super.onMarkerTapped(latitude, longitude)
// update track display
track = TrackHelper.toggleStarred(activity as Context, track, latitude, longitude)
layout.updateTrackOverlay(track)
// save track
GlobalScope.launch {
FileHelper.saveTrackSuspended(track, true)
}
}
/* Opens up a file picker to select the save location */
private fun openSaveGpxDialog() {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
@ -182,7 +197,7 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
/* Share track as GPX via share sheet */
private fun shareGpxTrack() {
val gpxFile = Uri.parse(layout.track.gpxUriString).toFile()
val gpxShareUri = FileProvider.getUriForFile(this.activity as Context, "${activity!!.applicationContext.packageName}.provider", gpxFile)
val gpxShareUri = FileProvider.getUriForFile(this.activity as Context, "${requireActivity().applicationContext.packageName}.provider", gpxFile)
val shareIntent: Intent = Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND
data = gpxShareUri

View File

@ -37,7 +37,8 @@ data class WayPoint(@Expose val provider: String,
@Expose val time: Long,
@Expose val distanceToStartingPoint: Float = 0f,
@Expose val numberSatellites: Int = 0,
@Expose var isStopOver: Boolean = false): Parcelable {
@Expose var isStopOver: Boolean = false,
@Expose var starred: Boolean = false): Parcelable {
/* Converts WayPoint into Location */

View File

@ -1,168 +0,0 @@
/*
* ImportHelper.kt
* Implements the ImportHelper object
* A ImportHelper manages the one-time import of old .trackbook files
*
* This file is part of
* TRACKBOOK - Movement Recorder for Android
*
* Copyright (c) 2016-20 - Y20K.org
* Licensed under the MIT-License
* http://opensource.org/licenses/MIT
*
* Trackbook uses osmdroid - OpenStreetMap-Tools for Android
* https://github.com/osmdroid/osmdroid
*/
package org.y20k.trackbook.helpers
import android.content.Context
import android.location.Location
import androidx.annotation.Keep
import com.google.gson.GsonBuilder
import com.google.gson.annotations.SerializedName
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.y20k.trackbook.Keys
import org.y20k.trackbook.core.Track
import org.y20k.trackbook.core.WayPoint
import java.io.BufferedReader
import java.io.File
import java.io.InputStream
import java.io.InputStreamReader
import java.util.*
/*
* ImportHelper data class
*/
object ImportHelper {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(ImportHelper::class.java)
/* Converts older tracks of type .trackbook into the new format */
fun convertOldTracks(context: Context) {
val oldTracks: ArrayList<Track> = arrayListOf()
val trackFolder: File? = context.getExternalFilesDir(Keys.FOLDER_TRACKS)
if (trackFolder != null && trackFolder.exists() && trackFolder.isDirectory) {
trackFolder.listFiles()?.forEach { file ->
if (file.name.endsWith(".trackbook")) {
// read until last line reached
val stream: InputStream = file.inputStream()
val reader: BufferedReader = BufferedReader(InputStreamReader(stream))
val builder: StringBuilder = StringBuilder()
reader.forEachLine {
builder.append(it)
builder.append("\n") }
stream.close()
// get content of file
val fileContent: String = builder.toString()
// get LegacyTrack from JSON
val gsonBuilder = GsonBuilder()
gsonBuilder.setDateFormat("M/d/yy hh:mm a")
val oldTrack: LegacyTrack = gsonBuilder.create().fromJson(fileContent, LegacyTrack::class.java)
oldTracks.add(oldTrack.toTrack())
}
}
}
// save track using "deferred await"
if (oldTracks.isNotEmpty()) {
GlobalScope.launch {
oldTracks.forEach { oldTrack ->
// step 1: create and store filenames for json and gpx files
oldTrack.trackUriString = FileHelper.getTrackFileUri(context, oldTrack).toString()
oldTrack.gpxUriString = FileHelper.getGpxFileUri(context, oldTrack).toString()
// step 2: save track
FileHelper.saveTrackSuspended(oldTrack, saveGpxToo = true)
// step 3: save tracklist
FileHelper.addTrackAndSaveTracklistSuspended(context, oldTrack)
}
}
}
}
/*
* Inner class: Legacy version of Track - used for one-time import only
* Warning: Works only as long as targetSdkVersion < 28
*/
@Keep
private data class LegacyTrack (
@SerializedName("b") var mTrackFormatVersion: Int = 0,
@SerializedName("c") var mWayPoints: List<LegacyWayPoint>,
@SerializedName("d") var mTrackLength: Float = 0f,
@SerializedName("e") var mDuration: Long = 0,
@SerializedName("f") var mStepCount: Float = 0f,
@SerializedName("g") var mRecordingStart: Date = GregorianCalendar.getInstance().time,
@SerializedName("h") var mRecordingStop: Date = mRecordingStart,
@SerializedName("i") var mMaxAltitude: Double = 0.0,
@SerializedName("j") var mMinAltitude: Double = 0.0,
@SerializedName("k") var mPositiveElevation: Double = 0.0,
@SerializedName("l") var mNegativeElevation: Double = 0.0) {
/* Converts */
fun toTrack():Track {
val track: Track = Track()
track.trackFormatVersion = mTrackFormatVersion
mWayPoints.forEach { legacyWayPoint ->
val wayPoint: WayPoint= WayPoint(
provider = legacyWayPoint.mLocation.provider,
latitude = legacyWayPoint.mLocation.latitude,
longitude = legacyWayPoint.mLocation.longitude,
altitude = legacyWayPoint.mLocation.altitude,
accuracy = legacyWayPoint.mLocation.accuracy,
time = legacyWayPoint.mLocation.time,
distanceToStartingPoint = legacyWayPoint.mDistanceToStartingPoint,
numberSatellites = legacyWayPoint.mNumberSatellites,
isStopOver = legacyWayPoint.mIsStopOver
)
track.wayPoints.add(wayPoint)
}
track.length = mTrackLength
track.duration = mDuration
track.stepCount = mStepCount
track.recordingStart = mRecordingStart
track.recordingStop = mRecordingStop
track.maxAltitude = mMaxAltitude
track.minAltitude = mMinAltitude
track.positiveElevation = mPositiveElevation
track.negativeElevation = mNegativeElevation
track.latitude = track.wayPoints[0].latitude
track.longitude = track.wayPoints[0].longitude
track.name = DateTimeHelper.convertToReadableDate(mRecordingStart)
return track
}
}
/*
* End of inner class
*/
/*
* Inner class: Legacy version of WayPoint - used for one-time import only
* Warning: Works only as long as targetSdkVersion < 28
*/
@Keep
private data class LegacyWayPoint (
@SerializedName("a") var mLocation: Location,
@SerializedName("b") var mIsStopOver: Boolean = false,
@SerializedName("c") var mDistanceToStartingPoint: Float = 0f,
@SerializedName("d") var mNumberSatellites: Int = 0) {
}
/*
* End of inner class
*/
}

View File

@ -106,7 +106,6 @@ object LengthUnitHelper {
// speed in km/h / mph
val velocity: Double = convertMetersPerSecond((trackLength / duration), useImperialUnits)
// create readable speed string
LogHelper.e("TAG", "duration = $duration velocity = $velocity")
var bd: BigDecimal = BigDecimal.valueOf(velocity)
bd = bd.setScale(1, RoundingMode.HALF_UP)
speed = bd.toPlainString()

View File

@ -36,12 +36,18 @@ import java.util.*
/*
* MapHelper object
* MapHelper class
*/
object MapHelper {
class MapOverlay (private var markerListener: MarkerListener) {
/* Interface used to communicate back to activity/fragment */
interface MarkerListener {
fun onMarkerTapped(latitude: Double, longitude: Double) {
}
}
/* Define log tag */
private val LOG_TAG = MapHelper::class.java.simpleName
private val TAG = MapOverlay::class.java.simpleName
/* Creates icon overlay for current position (used in MapFragment) */
@ -93,16 +99,22 @@ object MapHelper {
when (trackingState) {
// CASE: Recording is active
Keys.STATE_TRACKING_ACTIVE -> {
when (wayPoint.isStopOver) {
true -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!!
false -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_red_24dp)!!
if (wayPoint.starred) {
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_star_red_24dp)!!
} else if (wayPoint.isStopOver) {
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!!
} else {
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_red_24dp)!!
}
}
// CASE: Recording is paused/stopped
else -> {
when (wayPoint.isStopOver) {
true -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!!
false -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_blue_24dp)!!
if (wayPoint.starred) {
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_star_blue_24dp)!!
} else if (wayPoint.isStopOver) {
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!!
} else {
newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_blue_24dp)!!
}
}
}
@ -121,7 +133,8 @@ object MapHelper {
/* Creates a marker overlay item */
private fun createOverlayItem(context: Context, latitude: Double, longitude: Double, accuracy: Float, provider: String, time: Long): OverlayItem {
val title: String = "${context.getString(R.string.marker_description_time)}: ${SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(time)}"
val description: String = "${context.getString(R.string.marker_description_accuracy)}: ${DecimalFormat("#0.00").format(accuracy)} (${provider})"
//val description: String = "${context.getString(R.string.marker_description_accuracy)}: ${DecimalFormat("#0.00").format(accuracy)} (${provider})"
val description: String = "${context.getString(R.string.marker_description_time)}: ${SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(time)} | ${context.getString(R.string.marker_description_accuracy)}: ${DecimalFormat("#0.00").format(accuracy)} (${provider})"
val position: GeoPoint = GeoPoint(latitude, longitude)
return OverlayItem(title, description, position)
}
@ -129,10 +142,10 @@ object MapHelper {
/* Creates an overlay */
private fun createOverlay(context: Context, overlayItems: ArrayList<OverlayItem>): ItemizedIconOverlay<OverlayItem> {
return ItemizedIconOverlay(overlayItems,
return ItemizedIconOverlay<OverlayItem>(context, overlayItems,
object : ItemizedIconOverlay.OnItemGestureListener<OverlayItem> {
override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean {
Toast.makeText(context, item.title, Toast.LENGTH_LONG).show()
markerListener.onMarkerTapped(item.point.latitude, item.point.longitude)
return true
}
override fun onItemLongPress(index: Int, item: OverlayItem): Boolean {
@ -141,8 +154,7 @@ object MapHelper {
Toast.makeText(context, item.snippet, Toast.LENGTH_LONG).show()
return true
}
}, context)
})
}
}

View File

@ -19,6 +19,8 @@ package org.y20k.trackbook.helpers
import android.content.Context
import android.location.Location
import android.widget.Toast
import org.y20k.trackbook.R
import org.y20k.trackbook.core.Track
import org.y20k.trackbook.core.TracklistElement
import org.y20k.trackbook.core.WayPoint
@ -219,5 +221,18 @@ object TrackHelper {
}
/* Toggles starred flag for given position */
fun toggleStarred(context: Context, track: Track, latitude: Double, longitude: Double): Track {
track.wayPoints.forEach { waypoint ->
if (waypoint.latitude == latitude && waypoint.longitude == longitude) {
waypoint.starred = !waypoint.starred
when (waypoint.starred) {
true -> Toast.makeText(context, R.string.toast_message_poi_added, Toast.LENGTH_LONG).show()
false -> Toast.makeText(context, R.string.toast_message_poi_removed, Toast.LENGTH_LONG).show()
}
}
}
return track
}
}

View File

@ -88,8 +88,8 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
trackElementViewHolder.trackNameView.text = tracklist.tracklistElements[position].name
trackElementViewHolder.trackDataView.text = createTrackDataString(position)
when (tracklist.tracklistElements[position].starred) {
true -> trackElementViewHolder.starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_24dp))
false -> trackElementViewHolder.starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_border_24dp))
true -> trackElementViewHolder.starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_filled_24dp))
false -> trackElementViewHolder.starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_outline_24dp))
}
trackElementViewHolder.trackElement.setOnClickListener {
tracklistListener.onTrackElementTapped(tracklist.tracklistElements[position])
@ -134,11 +134,11 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
val starButton: ImageButton = view as ImageButton
when (tracklist.tracklistElements[position].starred) {
true -> {
starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_border_24dp))
starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_outline_24dp))
tracklist.tracklistElements[position].starred = false
}
false -> {
starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_24dp))
starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_filled_24dp))
tracklist.tracklistElements[position].starred = true
}
}

View File

@ -44,14 +44,14 @@ import org.y20k.trackbook.R
import org.y20k.trackbook.core.Track
import org.y20k.trackbook.helpers.AppThemeHelper
import org.y20k.trackbook.helpers.LogHelper
import org.y20k.trackbook.helpers.MapHelper
import org.y20k.trackbook.helpers.MapOverlay
import org.y20k.trackbook.helpers.PreferencesHelper
/*
* MapFragmentLayoutHolder class
*/
data class MapFragmentLayoutHolder(var context: Context, var inflater: LayoutInflater, var container: ViewGroup?, val startLocation: Location, val trackingState: Int) {
data class MapFragmentLayoutHolder(private var context: Context, private var markerListener: MapOverlay.MarkerListener, private var inflater: LayoutInflater, private var container: ViewGroup?, private val startLocation: Location, private val trackingState: Int) {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(MapFragmentLayoutHolder::class.java)
@ -109,7 +109,7 @@ data class MapFragmentLayoutHolder(var context: Context, var inflater: LayoutInf
mapView.overlays.add(compassOverlay)
// add my location overlay
currentPositionOverlay = MapHelper.createMyLocationOverlay(context, startLocation, trackingState)
currentPositionOverlay = MapOverlay(markerListener).createMyLocationOverlay(context, startLocation, trackingState)
mapView.overlays.add(currentPositionOverlay)
centerMap(startLocation)
@ -157,7 +157,7 @@ data class MapFragmentLayoutHolder(var context: Context, var inflater: LayoutInf
/* Mark current position on map */
fun markCurrentPosition(location: Location, trackingState: Int = Keys.STATE_TRACKING_NOT) {
mapView.overlays.remove(currentPositionOverlay)
currentPositionOverlay = MapHelper.createMyLocationOverlay(context, location, trackingState)
currentPositionOverlay = MapOverlay(markerListener).createMyLocationOverlay(context, location, trackingState)
mapView.overlays.add(currentPositionOverlay)
}
@ -168,7 +168,7 @@ data class MapFragmentLayoutHolder(var context: Context, var inflater: LayoutInf
mapView.overlays.remove(currentTrackOverlay)
}
if (track.wayPoints.isNotEmpty()) {
currentTrackOverlay = MapHelper.createTrackOverlay(context, track, trackingState)
currentTrackOverlay = MapOverlay(markerListener).createTrackOverlay(context, track, trackingState)
mapView.overlays.add(currentTrackOverlay)
}
}

View File

@ -50,7 +50,7 @@ import kotlin.math.roundToInt
/*
* TrackFragmentLayoutHolder class
*/
data class TrackFragmentLayoutHolder(var context: Context, var inflater: LayoutInflater, var container: ViewGroup?, var track: Track) {
data class TrackFragmentLayoutHolder(private var context: Context, private var markerListener: MapOverlay.MarkerListener, private var inflater: LayoutInflater, private var container: ViewGroup?, var track: Track) {
/* Define log tag */
private val TAG: String = LogHelper.makeLogTag(TrackFragmentLayoutHolder::class.java)
@ -140,7 +140,7 @@ data class TrackFragmentLayoutHolder(var context: Context, var inflater: LayoutI
mapView.overlays.add(compassOverlay)
// create map overlay
trackOverlay = MapHelper.createTrackOverlay(context, track, Keys.STATE_TRACKING_NOT)
trackOverlay = MapOverlay(markerListener).createTrackOverlay(context, track, Keys.STATE_TRACKING_NOT)
if (track.wayPoints.isNotEmpty()) {
mapView.overlays.add(trackOverlay)
}
@ -161,6 +161,19 @@ data class TrackFragmentLayoutHolder(var context: Context, var inflater: LayoutI
}
/* Updates map overlay */
fun updateTrackOverlay(newTrack: Track) {
track = newTrack
if (trackOverlay != null) {
mapView.overlays.remove(trackOverlay)
}
if (track.wayPoints.isNotEmpty()) {
trackOverlay = MapOverlay(markerListener).createTrackOverlay(context, track, Keys.STATE_TRACKING_NOT)
mapView.overlays.add(trackOverlay)
}
}
/* Saves zoom level and center of this map */
fun saveViewStateToTrack() {
val center: IGeoPoint = mapView.mapCenter

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/trackbook_blue"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/trackbook_red"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
</vector>

View File

@ -57,7 +57,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_star_border_24dp" />
app:srcCompat="@drawable/ic_star_outline_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -43,6 +43,8 @@
<string name="toast_message_copied_to_clipboard">Copied to clipboard.</string>
<string name="toast_message_elevation_info">Hint: The accuracy of elevation data depends on your device. The uphill and downhill elevation of the whole route is measured.</string>
<string name="toast_message_install_file_helper">Unable to save. Please install a file manager first.</string>
<string name="toast_message_poi_added">Point of interest marker added.</string>
<string name="toast_message_poi_removed">Point of interest marker removed.</string>
<string name="toast_message_save_gpx">Saving recording as GPX.</string>
<!-- Map Markers -->
<string name="marker_description_time">Time</string>