checkpoint
This commit is contained in:
parent
47531768d1
commit
2568af3bb1
49 changed files with 825 additions and 1512 deletions
|
@ -4,12 +4,12 @@ apply plugin: 'kotlin-parcelize'
|
|||
apply plugin: 'androidx.navigation.safeargs.kotlin'
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
compileSdkVersion 33
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'net.voussoir.trackbook'
|
||||
applicationId 'net.voussoir.trkpt'
|
||||
minSdkVersion 25
|
||||
targetSdkVersion 31
|
||||
targetSdk 32
|
||||
versionCode 50
|
||||
versionName '2.1.2'
|
||||
resConfigs "en", "da", "de", "fr", "hr", "id", "it", "ja", "nb-rNO", "nl", "pl", "pt-rBR", "ru", "sv", "tr", "zh-rCN"
|
||||
|
@ -51,25 +51,25 @@ android {
|
|||
|
||||
dependencies {
|
||||
// Kotlin
|
||||
def coroutinesVersion = "1.5.2"
|
||||
def coroutinesVersion = '1.6.4'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
|
||||
// AndroidX
|
||||
def navigationVersion = "2.3.5"
|
||||
implementation "androidx.activity:activity-ktx:1.4.0"
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
def navigationVersion = '2.5.3'
|
||||
implementation 'androidx.activity:activity-ktx:1.6.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:2.5.3"
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.6.0-alpha03'
|
||||
implementation 'com.google.android.material:material:1.9.0-alpha02'
|
||||
|
||||
// Gson
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
|
||||
// OpenStreetMap
|
||||
implementation 'org.osmdroid:osmdroid-android:6.1.11'
|
||||
implementation 'org.osmdroid:osmdroid-android:6.1.14'
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<application
|
||||
android:name=".Trackbook"
|
||||
android:name="org.y20k.trackbook.Trackbook"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
|
|
|
@ -26,7 +26,7 @@ import java.util.*
|
|||
object Keys {
|
||||
|
||||
// application name
|
||||
const val APPLICATION_NAME: String = "Trackbook"
|
||||
const val APPLICATION_NAME: String = "trkpt"
|
||||
|
||||
// version numbers
|
||||
const val CURRENT_TRACK_FORMAT_VERSION: Int = 4
|
||||
|
@ -39,9 +39,10 @@ object Keys {
|
|||
|
||||
// args
|
||||
const val ARG_TRACK_TITLE: String = "ArgTrackTitle"
|
||||
const val ARG_TRACK_FILE_URI: String = "ArgTrackFileUri"
|
||||
const val ARG_GPX_FILE_URI: String = "ArgGpxFileUri"
|
||||
const val ARG_TRACK_ID: String = "ArgTrackId"
|
||||
const val ARG_TRACK_ID: String = "ArgTrackID"
|
||||
const val ARG_TRACK_DEVICE_ID: String = "ArgTrackDeviceID"
|
||||
const val ARG_TRACK_START_TIME: String = "ArgTrackStartTime"
|
||||
const val ARG_TRACK_STOP_TIME: String = "ArgTrackStopTime"
|
||||
|
||||
// preferences
|
||||
const val PREF_ONE_TIME_HOUSEKEEPING_NECESSARY = "ONE_TIME_HOUSEKEEPING_NECESSARY_VERSIONCODE_38" // increment to current app version code to trigger housekeeping that runs only once
|
||||
|
@ -57,15 +58,12 @@ object Keys {
|
|||
const val PREF_USE_IMPERIAL_UNITS: String = "prefUseImperialUnits"
|
||||
const val PREF_GPS_ONLY: String = "prefGpsOnly"
|
||||
const val PREF_OMIT_RESTS: String = "prefOmitRests"
|
||||
const val PREF_AUTO_EXPORT_INTERVAL: String = "prefAutoExportInterval"
|
||||
const val PREF_ALTITUDE_SMOOTHING_VALUE: String = "prefAltitudeSmoothingValue"
|
||||
const val PREF_LOCATION_ACCURACY_THRESHOLD: String = "prefLocationAccuracyThreshold"
|
||||
const val PREF_LOCATION_AGE_THRESHOLD: String = "prefLocationAgeThreshold"
|
||||
const val PREF_COMMIT_INTERVAL: String = "prefCommitInterval"
|
||||
const val PREF_DEVICE_ID: String = "prefDeviceID"
|
||||
|
||||
// states
|
||||
const val STATE_TRACKING_NOT_STARTED: Int = 0
|
||||
const val STATE_TRACKING_STOPPED: Int = 0
|
||||
const val STATE_TRACKING_ACTIVE: Int = 1
|
||||
const val STATE_TRACKING_PAUSED: Int = 2
|
||||
const val STATE_THEME_FOLLOW_SYSTEM: String = "stateFollowSystem"
|
||||
const val STATE_THEME_LIGHT_MODE: String = "stateLightMode"
|
||||
const val STATE_THEME_DARK_MODE: String = "stateDarkMode"
|
||||
|
@ -105,9 +103,9 @@ object Keys {
|
|||
const val ONE_HOUR_IN_MILLISECONDS: Long = 60 * ONE_MINUTE_IN_MILLISECONDS
|
||||
const val EMPTY_STRING_RESOURCE: Int = 0
|
||||
const val REQUEST_CURRENT_LOCATION_INTERVAL: Long = 1 * ONE_SECOND_IN_MILLISECONDS
|
||||
const val ADD_WAYPOINT_TO_TRACK_INTERVAL: Long = 1 * ONE_SECOND_IN_MILLISECONDS
|
||||
const val TRACKING_INTERVAL: Long = 1 * ONE_SECOND_IN_MILLISECONDS
|
||||
const val SAVE_TEMP_TRACK_INTERVAL: Long = 30 * ONE_SECOND_IN_MILLISECONDS
|
||||
const val SIGNIFICANT_TIME_DIFFERENCE: Long = 2 * ONE_MINUTE_IN_MILLISECONDS
|
||||
const val SIGNIFICANT_TIME_DIFFERENCE: Long = 1 * ONE_MINUTE_IN_MILLISECONDS
|
||||
const val STOP_OVER_THRESHOLD: Long = 5 * ONE_MINUTE_IN_MILLISECONDS
|
||||
const val IMPLAUSIBLE_TRACK_START_SPEED: Double = 250.0 // 250 km/h
|
||||
const val DEFAULT_LATITUDE: Double = 71.172500 // latitude Nordkapp, Norway
|
||||
|
@ -115,14 +113,12 @@ object Keys {
|
|||
const val DEFAULT_ACCURACY: Float = 300f // in meters
|
||||
const val DEFAULT_ALTITUDE: Double = 0.0
|
||||
const val DEFAULT_TIME: Long = 0L
|
||||
const val DEFAULT_AUTO_EXPORT_INTERVAL: Int = 24
|
||||
const val COMMIT_INTERVAL: Int = 30
|
||||
const val DEFAULT_ALTITUDE_SMOOTHING_VALUE: Int = 13
|
||||
const val DEFAULT_THRESHOLD_LOCATION_ACCURACY: Int = 30 // 30 meters
|
||||
const val DEFAULT_THRESHOLD_LOCATION_AGE: Long = 60_000_000_000L // one minute in nanoseconds
|
||||
const val DEFAULT_THRESHOLD_DISTANCE: Float = 15f // 15 meters
|
||||
const val DEFAULT_ZOOM_LEVEL: Double = 16.0
|
||||
const val MIN_NUMBER_OF_WAYPOINTS_FOR_ELEVATION_CALCULATION: Int = 5
|
||||
const val MAX_NUMBER_OF_WAYPOINTS_FOR_ELEVATION_CALCULATION: Int = 20
|
||||
const val ALTITUDE_MEASUREMENT_ERROR_THRESHOLD = 10 // altitude changes of 10 meter or more (per 15 seconds) are being discarded
|
||||
|
||||
// notification
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
package org.y20k.trackbook
|
||||
|
||||
import YesNoDialog
|
||||
import android.Manifest
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
|
@ -28,14 +27,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.helpers.*
|
||||
import org.y20k.trackbook.ui.MapFragmentLayoutHolder
|
||||
|
@ -43,34 +35,34 @@ import org.y20k.trackbook.ui.MapFragmentLayoutHolder
|
|||
/*
|
||||
* MapFragment class
|
||||
*/
|
||||
class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelper.MarkerListener {
|
||||
|
||||
class MapFragment : Fragment(), MapOverlayHelper.MarkerListener
|
||||
{
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(MapFragment::class.java)
|
||||
|
||||
/* Main class variables */
|
||||
private var bound: Boolean = false
|
||||
private val handler: Handler = Handler(Looper.getMainLooper())
|
||||
private var trackingState: Int = Keys.STATE_TRACKING_NOT_STARTED
|
||||
private var trackingState: Int = Keys.STATE_TRACKING_STOPPED
|
||||
private var gpsProviderActive: Boolean = false
|
||||
private var networkProviderActive: Boolean = false
|
||||
private var track: Track = Track()
|
||||
private lateinit var track: Track
|
||||
private lateinit var currentBestLocation: Location
|
||||
private lateinit var layout: MapFragmentLayoutHolder
|
||||
private lateinit var trackerService: TrackerService
|
||||
|
||||
|
||||
/* Overrides onCreate from Fragment */
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// TODO make only MapFragment's status bar transparent - see: https://gist.github.com/Dvik/a3de88d39da9d1d6d175025a56c5e797#file-viewextension-kt and https://proandroiddev.com/android-full-screen-ui-with-transparent-status-bar-ef52f3adde63
|
||||
// TODO make only MapFragment's status bar transparent - see:
|
||||
// https://gist.github.com/Dvik/a3de88d39da9d1d6d175025a56c5e797#file-viewextension-kt and
|
||||
// https://proandroiddev.com/android-full-screen-ui-with-transparent-status-bar-ef52f3adde63
|
||||
// get current best location
|
||||
currentBestLocation = LocationHelper.getLastKnownLocation(activity as Context)
|
||||
// get saved tracking state
|
||||
trackingState = PreferencesHelper.loadTrackingState()
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onStop from Fragment */
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
// initialize layout
|
||||
|
@ -84,30 +76,10 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
|||
layout.mainButton.setOnClickListener {
|
||||
handleTrackingManagementMenu()
|
||||
}
|
||||
layout.saveButton.setOnClickListener {
|
||||
saveTrack()
|
||||
}
|
||||
layout.clearButton.setOnClickListener {
|
||||
if (track.wayPoints.isEmpty())
|
||||
{
|
||||
// This might seem useless since it's already empty, but there are other UI updates
|
||||
// that occur as part of cleartrack so we may as well take advantage of those.
|
||||
trackerService.clearTrack()
|
||||
}
|
||||
else {
|
||||
YesNoDialog(this as YesNoDialog.YesNoDialogListener).show(
|
||||
context=activity as Context,
|
||||
type = Keys.DIALOG_DELETE_CURRENT_RECORDING,
|
||||
message = R.string.dialog_delete_current_recording_message,
|
||||
yesButton = R.string.dialog_delete_current_recording_button_discard
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return layout.rootView
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onStart from Fragment */
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
@ -119,7 +91,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
|||
activity?.bindService(Intent(activity, TrackerService::class.java), connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onResume from Fragment */
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
@ -129,7 +100,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
|||
// }
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onPause from Fragment */
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
@ -137,16 +107,19 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
|||
if (bound && trackingState != Keys.STATE_TRACKING_ACTIVE) {
|
||||
trackerService.removeGpsLocationListener()
|
||||
trackerService.removeNetworkLocationListener()
|
||||
trackerService.trackbook.database.commit()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onStop from Fragment */
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
// unbind from TrackerService
|
||||
activity?.unbindService(connection)
|
||||
handleServiceUnbind()
|
||||
if (bound)
|
||||
{
|
||||
activity?.unbindService(connection)
|
||||
handleServiceUnbind()
|
||||
}
|
||||
}
|
||||
|
||||
/* Register the permission launcher for requesting location */
|
||||
|
@ -171,14 +144,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
|||
trackerService.startTracking()
|
||||
}
|
||||
|
||||
/* Register the permission launcher for resuming the tracking service */
|
||||
private val resumeTrackingPermissionLauncher = registerForActivityResult(RequestPermission()) { isGranted: Boolean ->
|
||||
logPermissionRequestResult(isGranted)
|
||||
// start service via intent so that it keeps running after unbind
|
||||
startTrackerService()
|
||||
trackerService.resumeTracking()
|
||||
}
|
||||
|
||||
/* Logs the request result of the Activity Recognition permission launcher */
|
||||
private fun logPermissionRequestResult(isGranted: Boolean) {
|
||||
if (isGranted) {
|
||||
|
@ -188,68 +153,24 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
|||
}
|
||||
}
|
||||
|
||||
/* Overrides onYesNoDialog from YesNoDialogListener */
|
||||
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
|
||||
super.onYesNoDialog(type, dialogResult, payload, payloadString)
|
||||
when (type) {
|
||||
Keys.DIALOG_RESUME_EMPTY_RECORDING -> {
|
||||
when (dialogResult) {
|
||||
// user tapped resume
|
||||
true -> {
|
||||
trackerService.resumeTracking()
|
||||
}
|
||||
}
|
||||
}
|
||||
Keys.DIALOG_DELETE_CURRENT_RECORDING -> {
|
||||
when (dialogResult) {
|
||||
true -> {
|
||||
trackerService.clearTrack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onMarkerTapped from MarkerListener */
|
||||
override fun onMarkerTapped(latitude: Double, longitude: Double) {
|
||||
super.onMarkerTapped(latitude, longitude)
|
||||
if (bound) {
|
||||
TrackHelper.toggle_waypoint_starred(activity as Context, track, latitude, longitude)
|
||||
layout.overlayCurrentTrack(track, trackingState)
|
||||
trackerService.track = track
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Start recording waypoints */
|
||||
private fun startTracking() {
|
||||
// request activity recognition permission on Android Q+ if denied
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(activity as Context, Manifest.permission.ACTIVITY_RECOGNITION) == PackageManager.PERMISSION_DENIED) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(activity as Context, Manifest.permission.ACTIVITY_RECOGNITION) == PackageManager.PERMISSION_DENIED)
|
||||
{
|
||||
startTrackingPermissionLauncher.launch(Manifest.permission.ACTIVITY_RECOGNITION)
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
// start service via intent so that it keeps running after unbind
|
||||
startTrackerService()
|
||||
trackerService.startTracking()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Resume recording waypoints */
|
||||
private fun resumeTracking() {
|
||||
// request activity recognition permission on Android Q+ if denied
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(activity as Context, Manifest.permission.ACTIVITY_RECOGNITION) == PackageManager.PERMISSION_DENIED) {
|
||||
resumeTrackingPermissionLauncher.launch(Manifest.permission.ACTIVITY_RECOGNITION)
|
||||
} else {
|
||||
// start service via intent so that it keeps running after unbind
|
||||
startTrackerService()
|
||||
trackerService.resumeTracking()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Start tracker service */
|
||||
private fun startTrackerService() {
|
||||
private fun startTrackerService()
|
||||
{
|
||||
val intent = Intent(activity, TrackerService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// ... start service in foreground to prevent it being killed on Oreo
|
||||
|
@ -259,9 +180,9 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/* Handles state when service is being unbound */
|
||||
private fun handleServiceUnbind() {
|
||||
private fun handleServiceUnbind()
|
||||
{
|
||||
bound = false
|
||||
// unregister listener for changes in shared preferences
|
||||
PreferencesHelper.unregisterPreferenceChangeListener(sharedPreferenceChangeListener)
|
||||
|
@ -269,61 +190,25 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
|||
handler.removeCallbacks(periodicLocationRequestRunnable)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Starts / pauses tracking and toggles the recording sub menu_bottom_navigation */
|
||||
private fun handleTrackingManagementMenu() {
|
||||
when (trackingState) {
|
||||
Keys.STATE_TRACKING_PAUSED -> resumeTracking()
|
||||
Keys.STATE_TRACKING_ACTIVE -> trackerService.pauseTracking()
|
||||
Keys.STATE_TRACKING_NOT_STARTED -> startTracking()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Saves track - shows dialog, if recording is still empty */
|
||||
private fun saveTrack()
|
||||
private fun handleTrackingManagementMenu()
|
||||
{
|
||||
if (track.wayPoints.isEmpty())
|
||||
{
|
||||
YesNoDialog(this as YesNoDialog.YesNoDialogListener).show(
|
||||
context = activity as Context,
|
||||
type = Keys.DIALOG_RESUME_EMPTY_RECORDING,
|
||||
message = R.string.dialog_error_empty_recording_message,
|
||||
yesButton = R.string.dialog_error_empty_recording_button_resume
|
||||
)
|
||||
}
|
||||
else
|
||||
{
|
||||
CoroutineScope(IO).launch {
|
||||
trackerService.saveTrackAndClear(activity as Context)
|
||||
withContext(Main) {
|
||||
// step 4: open track in TrackFragement
|
||||
openTrack(track)
|
||||
}
|
||||
}
|
||||
when (trackingState) {
|
||||
Keys.STATE_TRACKING_ACTIVE -> trackerService.pauseTracking()
|
||||
Keys.STATE_TRACKING_STOPPED -> startTracking()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Opens a track in TrackFragment */
|
||||
private fun openTrack(track: Track) {
|
||||
val bundle: Bundle = Bundle()
|
||||
bundle.putString(Keys.ARG_TRACK_TITLE, track.name)
|
||||
bundle.putString(Keys.ARG_TRACK_FILE_URI, track.get_json_file(activity as Context).toUri().toString())
|
||||
bundle.putString(Keys.ARG_GPX_FILE_URI, track.get_gpx_file(activity as Context).toUri().toString())
|
||||
bundle.putLong(Keys.ARG_TRACK_ID, track.id)
|
||||
findNavController().navigate(R.id.fragment_track, bundle)
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Defines the listener for changes in shared preferences
|
||||
*/
|
||||
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
when (key) {
|
||||
Keys.PREF_TRACKING_STATE -> {
|
||||
if (activity != null) {
|
||||
when (key)
|
||||
{
|
||||
Keys.PREF_TRACKING_STATE ->
|
||||
{
|
||||
if (activity != null)
|
||||
{
|
||||
trackingState = PreferencesHelper.loadTrackingState()
|
||||
layout.updateMainButton(trackingState)
|
||||
}
|
||||
|
@ -334,7 +219,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
|||
* End of declaration
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* Defines callbacks for service binding, passed to bindService()
|
||||
*/
|
||||
|
@ -362,7 +246,6 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
|||
* End of declaration
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* Runnable: Periodically requests location
|
||||
*/
|
||||
|
@ -377,9 +260,11 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
|||
// update location and track
|
||||
layout.markCurrentPosition(currentBestLocation, trackingState)
|
||||
layout.overlayCurrentTrack(track, trackingState)
|
||||
layout.updateLiveStatics(distance = track.distance, duration = track.duration, trackingState = trackingState)
|
||||
// center map, if it had not been dragged/zoomed before
|
||||
if (!layout.userInteraction) { layout.centerMap(currentBestLocation, true)}
|
||||
if (!layout.userInteraction)
|
||||
{
|
||||
layout.centerMap(currentBestLocation, true)
|
||||
}
|
||||
// show error snackbar if necessary
|
||||
layout.toggleLocationErrorBar(gpsProviderActive, networkProviderActive)
|
||||
// use the handler to start runnable again after specified delay
|
||||
|
@ -389,5 +274,4 @@ class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlayHelpe
|
|||
/*
|
||||
* End of declaration
|
||||
*/
|
||||
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import android.content.Intent
|
|||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.preference.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -33,13 +34,13 @@ import kotlinx.coroutines.Deferred
|
|||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import org.y20k.trackbook.core.Tracklist
|
||||
import org.y20k.trackbook.core.load_tracklist
|
||||
import org.y20k.trackbook.helpers.AppThemeHelper
|
||||
import org.y20k.trackbook.helpers.FileHelper
|
||||
import org.y20k.trackbook.helpers.LengthUnitHelper
|
||||
import org.y20k.trackbook.helpers.LogHelper
|
||||
|
||||
import org.y20k.trackbook.helpers.PreferencesHelper
|
||||
import org.y20k.trackbook.helpers.random_int
|
||||
import kotlin.random.Random
|
||||
|
||||
/*
|
||||
* SettingsFragment class
|
||||
|
@ -64,6 +65,10 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
|||
val context = preferenceManager.context
|
||||
val screen = preferenceManager.createPreferenceScreen(context)
|
||||
|
||||
val preferenceCategoryGeneral: PreferenceCategory = PreferenceCategory(activity as Context)
|
||||
preferenceCategoryGeneral.title = getString(R.string.pref_general_title)
|
||||
screen.addPreference(preferenceCategoryGeneral)
|
||||
|
||||
// set up "Restrict to GPS" preference
|
||||
val preferenceGpsOnly: SwitchPreferenceCompat = SwitchPreferenceCompat(activity as Context)
|
||||
preferenceGpsOnly.isSingleLineTitle = false
|
||||
|
@ -73,6 +78,8 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
|||
preferenceGpsOnly.summaryOn = getString(R.string.pref_gps_only_summary_gps_only)
|
||||
preferenceGpsOnly.summaryOff = getString(R.string.pref_gps_only_summary_gps_and_network)
|
||||
preferenceGpsOnly.setDefaultValue(false)
|
||||
preferenceCategoryGeneral.contains(preferenceGpsOnly)
|
||||
screen.addPreference(preferenceGpsOnly)
|
||||
|
||||
// set up "Use Imperial Measurements" preference
|
||||
val preferenceImperialMeasurementUnits: SwitchPreferenceCompat = SwitchPreferenceCompat(activity as Context)
|
||||
|
@ -83,6 +90,8 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
|||
preferenceImperialMeasurementUnits.summaryOn = getString(R.string.pref_imperial_measurement_units_summary_imperial)
|
||||
preferenceImperialMeasurementUnits.summaryOff = getString(R.string.pref_imperial_measurement_units_summary_metric)
|
||||
preferenceImperialMeasurementUnits.setDefaultValue(LengthUnitHelper.useImperialUnits())
|
||||
preferenceCategoryGeneral.contains(preferenceImperialMeasurementUnits)
|
||||
screen.addPreference(preferenceImperialMeasurementUnits)
|
||||
|
||||
// set up "App Theme" preference
|
||||
val preferenceThemeSelection: ListPreference = ListPreference(activity as Context)
|
||||
|
@ -101,16 +110,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
|||
return@setOnPreferenceChangeListener false
|
||||
}
|
||||
}
|
||||
|
||||
// set up "Delete Non-Starred" preference
|
||||
val preferenceDeleteNonStarred: Preference = Preference(activity as Context)
|
||||
preferenceDeleteNonStarred.title = getString(R.string.pref_delete_non_starred_title)
|
||||
preferenceDeleteNonStarred.setIcon(R.drawable.ic_delete_24dp)
|
||||
preferenceDeleteNonStarred.summary = getString(R.string.pref_delete_non_starred_summary)
|
||||
preferenceDeleteNonStarred.setOnPreferenceClickListener{
|
||||
YesNoDialog(this as YesNoDialog.YesNoDialogListener).show(context = activity as Context, type = Keys.DIALOG_DELETE_NON_STARRED, message = R.string.dialog_yes_no_message_delete_non_starred, yesButton = R.string.dialog_yes_no_positive_button_delete_non_starred)
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
screen.addPreference(preferenceThemeSelection)
|
||||
|
||||
// set up "Recording Accuracy" preference
|
||||
val DEFAULT_OMIT_RESTS = true
|
||||
|
@ -122,46 +122,27 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
|||
preferenceOmitRests.summaryOn = getString(R.string.pref_omit_rests_on)
|
||||
preferenceOmitRests.summaryOff = getString(R.string.pref_omit_rests_off)
|
||||
preferenceOmitRests.setDefaultValue(DEFAULT_OMIT_RESTS)
|
||||
preferenceCategoryGeneral.contains(preferenceOmitRests)
|
||||
screen.addPreference(preferenceOmitRests)
|
||||
|
||||
val preferenceAutoExportInterval: SeekBarPreference = SeekBarPreference(activity as Context)
|
||||
preferenceAutoExportInterval.title = getString(R.string.pref_auto_export_interval_title)
|
||||
preferenceAutoExportInterval.setIcon(R.drawable.ic_bar_chart_24)
|
||||
preferenceAutoExportInterval.key = Keys.PREF_AUTO_EXPORT_INTERVAL
|
||||
preferenceAutoExportInterval.summary = getString(R.string.pref_auto_export_interval_summary)
|
||||
preferenceAutoExportInterval.showSeekBarValue = true
|
||||
preferenceAutoExportInterval.min = 1
|
||||
preferenceAutoExportInterval.max = 24
|
||||
preferenceAutoExportInterval.setDefaultValue(Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE)
|
||||
val preferenceDeviceID: EditTextPreference = EditTextPreference(activity as Context)
|
||||
preferenceDeviceID.title = getString(R.string.pref_device_id)
|
||||
preferenceDeviceID.setIcon(R.drawable.ic_smartphone_24dp)
|
||||
preferenceDeviceID.key = Keys.PREF_DEVICE_ID
|
||||
preferenceDeviceID.summary = getString(R.string.pref_device_id_summary) + "\n" + PreferencesHelper.load_device_id()
|
||||
preferenceDeviceID.setDefaultValue(random_int().toString())
|
||||
preferenceCategoryGeneral.contains(preferenceDeviceID)
|
||||
screen.addPreference(preferenceDeviceID)
|
||||
|
||||
// set up "Altitude Smoothing" preference
|
||||
// val preferenceAltitudeSmoothingValue: SeekBarPreference = SeekBarPreference(activity as Context)
|
||||
// preferenceAltitudeSmoothingValue.title = getString(R.string.pref_altitude_smoothing_value_title)
|
||||
// preferenceAltitudeSmoothingValue.setIcon(R.drawable.ic_bar_chart_24)
|
||||
// preferenceAltitudeSmoothingValue.key = Keys.PREF_ALTITUDE_SMOOTHING_VALUE
|
||||
// preferenceAltitudeSmoothingValue.summary = getString(R.string.pref_altitude_smoothing_value_summary)
|
||||
// preferenceAltitudeSmoothingValue.showSeekBarValue = true
|
||||
// preferenceAltitudeSmoothingValue.min = Keys.MIN_NUMBER_OF_WAYPOINTS_FOR_ELEVATION_CALCULATION
|
||||
// preferenceAltitudeSmoothingValue.max = Keys.MAX_NUMBER_OF_WAYPOINTS_FOR_ELEVATION_CALCULATION
|
||||
// preferenceAltitudeSmoothingValue.setDefaultValue(Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE)
|
||||
|
||||
|
||||
// set up "Reset" preference
|
||||
val preferenceResetAdvanced: Preference = Preference(activity as Context)
|
||||
preferenceResetAdvanced.title = getString(R.string.pref_reset_advanced_title)
|
||||
preferenceResetAdvanced.setIcon(R.drawable.ic_undo_24dp)
|
||||
preferenceResetAdvanced.summary = getString(R.string.pref_reset_advanced_summary)
|
||||
preferenceResetAdvanced.setOnPreferenceClickListener{
|
||||
preferenceOmitRests.isChecked = DEFAULT_OMIT_RESTS
|
||||
// preferenceAltitudeSmoothingValue.value = Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
val preferenceCategoryAbout: PreferenceCategory = PreferenceCategory(context)
|
||||
preferenceCategoryAbout.title = getString(R.string.pref_about_title)
|
||||
screen.addPreference(preferenceCategoryAbout)
|
||||
|
||||
// set up "App Version" preference
|
||||
val preferenceAppVersion: Preference = Preference(context)
|
||||
preferenceAppVersion.title = getString(R.string.pref_app_version_title)
|
||||
preferenceAppVersion.setIcon(R.drawable.ic_info_24dp)
|
||||
preferenceAppVersion.summary = "${getString(R.string.pref_app_version_summary)} ${BuildConfig.VERSION_NAME} (${getString(
|
||||
R.string.app_version_name)})"
|
||||
preferenceAppVersion.summary = "${getString(R.string.pref_app_version_summary)} ${BuildConfig.VERSION_NAME}"
|
||||
preferenceAppVersion.setOnPreferenceClickListener {
|
||||
// copy to clipboard
|
||||
val clip: ClipData = ClipData.newPlainText("simple text", preferenceAppVersion.summary)
|
||||
|
@ -170,40 +151,10 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
|||
Toast.makeText(activity as Context, R.string.toast_message_copied_to_clipboard, Toast.LENGTH_LONG).show()
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
// set preference categories
|
||||
val preferenceCategoryGeneral: PreferenceCategory = PreferenceCategory(activity as Context)
|
||||
preferenceCategoryGeneral.title = getString(R.string.pref_general_title)
|
||||
preferenceCategoryGeneral.contains(preferenceImperialMeasurementUnits)
|
||||
preferenceCategoryGeneral.contains(preferenceGpsOnly)
|
||||
val preferenceCategoryMaintenance: PreferenceCategory = PreferenceCategory(activity as Context)
|
||||
preferenceCategoryMaintenance.title = getString(R.string.pref_maintenance_title)
|
||||
preferenceCategoryMaintenance.contains(preferenceDeleteNonStarred)
|
||||
|
||||
val preferenceCategoryAdvanced: PreferenceCategory = PreferenceCategory(activity as Context)
|
||||
preferenceCategoryAdvanced.title = getString(R.string.pref_advanced_title)
|
||||
preferenceCategoryAdvanced.contains(preferenceOmitRests)
|
||||
// preferenceCategoryAdvanced.contains(preferenceAltitudeSmoothingValue)
|
||||
preferenceCategoryAdvanced.contains(preferenceResetAdvanced)
|
||||
|
||||
val preferenceCategoryAbout: PreferenceCategory = PreferenceCategory(context)
|
||||
preferenceCategoryAbout.title = getString(R.string.pref_about_title)
|
||||
preferenceCategoryAbout.contains(preferenceAppVersion)
|
||||
screen.addPreference(preferenceAppVersion)
|
||||
|
||||
// setup preference screen
|
||||
screen.addPreference(preferenceCategoryGeneral)
|
||||
screen.addPreference(preferenceGpsOnly)
|
||||
screen.addPreference(preferenceImperialMeasurementUnits)
|
||||
screen.addPreference(preferenceThemeSelection)
|
||||
screen.addPreference(preferenceCategoryMaintenance)
|
||||
screen.addPreference(preferenceDeleteNonStarred)
|
||||
screen.addPreference(preferenceCategoryAdvanced)
|
||||
screen.addPreference(preferenceOmitRests)
|
||||
// screen.addPreference(preferenceAltitudeSmoothingValue)
|
||||
screen.addPreference(preferenceAutoExportInterval)
|
||||
screen.addPreference(preferenceResetAdvanced)
|
||||
screen.addPreference(preferenceCategoryAbout)
|
||||
screen.addPreference(preferenceAppVersion)
|
||||
preferenceScreen = screen
|
||||
}
|
||||
|
||||
|
@ -212,25 +163,11 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
|||
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) {
|
||||
when (type) {
|
||||
Keys.DIALOG_DELETE_NON_STARRED -> {
|
||||
when (dialogResult) {
|
||||
// user tapped delete
|
||||
true -> {
|
||||
deleteNonStarred(activity as Context)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
super.onYesNoDialog(type, dialogResult, payload, payloadString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Removes track and track files for given position - used by TracklistFragment */
|
||||
private fun deleteNonStarred(context: Context) {
|
||||
val tracklist: Tracklist = load_tracklist(context)
|
||||
tracklist.delete_non_starred(context)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -31,25 +31,17 @@ import android.view.ViewGroup
|
|||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.y20k.trackbook.core.Database
|
||||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.core.track_from_file
|
||||
import org.y20k.trackbook.dialogs.RenameTrackDialog
|
||||
import org.y20k.trackbook.helpers.DateTimeHelper
|
||||
import org.y20k.trackbook.helpers.FileHelper
|
||||
import org.y20k.trackbook.helpers.LogHelper
|
||||
import org.y20k.trackbook.helpers.MapOverlayHelper
|
||||
import org.y20k.trackbook.helpers.TrackHelper
|
||||
import org.y20k.trackbook.helpers.iso8601_format
|
||||
import org.y20k.trackbook.ui.TrackFragmentLayoutHolder
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDialog.YesNoDialogListener, MapOverlayHelper.MarkerListener {
|
||||
|
||||
|
@ -59,39 +51,24 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
|||
|
||||
/* Main class variables */
|
||||
private lateinit var layout: TrackFragmentLayoutHolder
|
||||
private lateinit var trackFileUriString: String
|
||||
|
||||
|
||||
/* Overrides onCreate from Fragment */
|
||||
override fun onCreate(savedInstanceState: Bundle?)
|
||||
{
|
||||
super.onCreate(savedInstanceState)
|
||||
trackFileUriString = arguments?.getString(Keys.ARG_TRACK_FILE_URI, String()) ?: String()
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onCreateView from Fragment */
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
// initialize layout
|
||||
val track: Track
|
||||
if (this::trackFileUriString.isInitialized && trackFileUriString.isNotBlank())
|
||||
{
|
||||
track = track_from_file(activity as Context, Uri.parse(trackFileUriString).toFile())
|
||||
} else {
|
||||
track = Track()
|
||||
}
|
||||
val database: Database = (requireActivity().applicationContext as Trackbook).database
|
||||
val track: Track = Track(
|
||||
database=database,
|
||||
device_id= this.requireArguments().getString(Keys.ARG_TRACK_DEVICE_ID, ""),
|
||||
start_time= iso8601_format.parse(this.requireArguments().getString(Keys.ARG_TRACK_START_TIME)!!),
|
||||
stop_time=iso8601_format.parse(this.requireArguments().getString(Keys.ARG_TRACK_STOP_TIME)!!),
|
||||
)
|
||||
track.load_trkpts()
|
||||
layout = TrackFragmentLayoutHolder(activity as Context, this as MapOverlayHelper.MarkerListener, inflater, container, track)
|
||||
|
||||
// set up share button
|
||||
layout.shareButton.setOnClickListener {
|
||||
openSaveGpxDialog()
|
||||
}
|
||||
// layout.shareButton.setOnLongClickListener {
|
||||
// val v = (activity as Context).getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
// v.vibrate(50)
|
||||
// shareGpxTrack()
|
||||
// return@setOnLongClickListener true
|
||||
// }
|
||||
// set up delete button
|
||||
layout.deleteButton.setOnClickListener {
|
||||
val dialogMessage: String = "${getString(R.string.dialog_yes_no_message_delete_recording)}\n\n- ${layout.trackNameView.text}"
|
||||
|
@ -110,14 +87,12 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
|||
return layout.rootView
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onResume from Fragment */
|
||||
override fun onResume()
|
||||
{
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onPause from Fragment */
|
||||
override fun onPause()
|
||||
{
|
||||
|
@ -131,39 +106,26 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
|||
private val requestSaveGpxLauncher = registerForActivityResult(StartActivityForResult(), this::requestSaveGpxResult)
|
||||
|
||||
|
||||
/* Pass the activity result */
|
||||
private fun requestSaveGpxResult(result: ActivityResult)
|
||||
{
|
||||
// save GPX file to result file location
|
||||
if (result.resultCode == Activity.RESULT_OK && result.data != null)
|
||||
if (result.resultCode != Activity.RESULT_OK || result.data == null)
|
||||
{
|
||||
val sourceUri: Uri = layout.track.get_gpx_file(activity as Context).toUri()
|
||||
Toast.makeText(activity as Context, sourceUri.toString(), Toast.LENGTH_LONG).show()
|
||||
val targetUri: Uri? = result.data?.data
|
||||
if (targetUri != null)
|
||||
{
|
||||
// copy file async (= fire & forget - no return value needed)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
FileHelper.saveCopyOfFileSuspended(activity as Context, originalFileUri = sourceUri, targetFileUri = targetUri)
|
||||
}
|
||||
Toast.makeText(activity as Context, targetUri.toString(), Toast.LENGTH_LONG).show()
|
||||
// Toast.makeText(activity as Context, R.string.toast_message_save_gpx, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val targetUri: Uri? = result.data?.data
|
||||
if (targetUri == null)
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
val outputsuccess: Uri? = layout.track.export_gpx(activity as Context, targetUri)
|
||||
if (outputsuccess == null)
|
||||
{
|
||||
Toast.makeText(activity as Context, "failed to export for some reason", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onRenameTrackDialog from RenameTrackDialog */
|
||||
override fun onRenameTrackDialog(textInput: String)
|
||||
{
|
||||
// rename track async (= fire & forget - no return value needed)
|
||||
CoroutineScope(Dispatchers.IO).launch { FileHelper.renameTrackSuspended(activity as Context, layout.track, textInput) }
|
||||
// update name in layout
|
||||
layout.track.name = textInput
|
||||
layout.trackNameView.text = textInput
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onYesNoDialog from YesNoDialogListener */
|
||||
override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String)
|
||||
{
|
||||
|
@ -175,15 +137,18 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
|||
// user tapped remove track
|
||||
true -> {
|
||||
// switch to TracklistFragment and remove track there
|
||||
val bundle: Bundle = bundleOf(Keys.ARG_TRACK_ID to layout.track.id)
|
||||
findNavController().navigate(R.id.tracklist_fragment, bundle)
|
||||
// val bundle: Bundle = bundleOf(Keys.ARG_TRACK_ID to layout.track.id)
|
||||
// findNavController().navigate(R.id.tracklist_fragment, bundle)
|
||||
}
|
||||
else ->
|
||||
{
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onMarkerTapped from MarkerListener */
|
||||
override fun onMarkerTapped(latitude: Double, longitude: Double)
|
||||
{
|
||||
|
@ -192,12 +157,11 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
|||
layout.updateTrackOverlay()
|
||||
}
|
||||
|
||||
|
||||
/* Opens up a file picker to select the save location */
|
||||
private fun openSaveGpxDialog()
|
||||
{
|
||||
val context = this.activity as Context
|
||||
val export_name: String = DateTimeHelper.convertToSortableDateString(layout.track.recordingStart) + Keys.GPX_FILE_EXTENSION
|
||||
val export_name: String = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(layout.track.start_time) + Keys.GPX_FILE_EXTENSION
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = Keys.MIME_TYPE_GPX
|
||||
|
@ -213,42 +177,5 @@ class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDi
|
|||
LogHelper.e(TAG, "Unable to save GPX.")
|
||||
Toast.makeText(activity as Context, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
// val context = this.activity as Context
|
||||
// val export_name: String = DateTimeHelper.convertToSortableDateString(layout.track.recordingStart) + Keys.GPX_FILE_EXTENSION
|
||||
// val sourceUri: Uri = layout.track.get_gpx_file(activity as Context).toUri()
|
||||
// // val targetUri: Uri = "file:///storage/emulated/0/Syncthing/GPX".toUri()
|
||||
// val targetUri: Uri = File(File("/storage/emulated/0/Syncthing/GPX"), export_name).toUri()
|
||||
// Toast.makeText(activity as Context, targetUri.toString(), Toast.LENGTH_LONG).show()
|
||||
// CoroutineScope(Dispatchers.IO).launch {
|
||||
// FileHelper.saveCopyOfFileSuspended(activity as Context, originalFileUri = sourceUri, targetFileUri = targetUri)
|
||||
// }
|
||||
// Toast.makeText(activity as Context, R.string.toast_message_save_gpx, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
|
||||
/* Share track as GPX via share sheet */
|
||||
private fun shareGpxTrack()
|
||||
{
|
||||
val gpxFile = layout.track.get_gpx_file(this.activity as Context)
|
||||
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
|
||||
type = Keys.MIME_TYPE_GPX
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
putExtra(Intent.EXTRA_STREAM, gpxShareUri)
|
||||
}, null)
|
||||
|
||||
// show share sheet - if file helper is available
|
||||
val packageManager: PackageManager? = activity?.packageManager
|
||||
if (packageManager != null && shareIntent.resolveActivity(packageManager) != null)
|
||||
{
|
||||
startActivity(shareIntent)
|
||||
}
|
||||
else
|
||||
{
|
||||
Toast.makeText(activity, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,40 +18,100 @@
|
|||
|
||||
package org.y20k.trackbook
|
||||
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageManager
|
||||
import android.database.Cursor
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import org.y20k.trackbook.core.Database
|
||||
import org.y20k.trackbook.core.Homepoint
|
||||
import org.y20k.trackbook.core.Trkpt
|
||||
import org.y20k.trackbook.helpers.AppThemeHelper
|
||||
import org.y20k.trackbook.helpers.LogHelper
|
||||
import org.y20k.trackbook.helpers.PreferencesHelper
|
||||
import org.y20k.trackbook.helpers.PreferencesHelper.initPreferences
|
||||
|
||||
import org.y20k.trackbook.helpers.iso8601
|
||||
import org.y20k.trackbook.helpers.iso8601_format
|
||||
import java.io.File
|
||||
|
||||
/*
|
||||
* Trackbook.class
|
||||
*/
|
||||
class Trackbook: Application() {
|
||||
class Trackbook(): Application() {
|
||||
val database: Database = Database()
|
||||
val homepoints: ArrayList<Homepoint> = ArrayList()
|
||||
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(Trackbook::class.java)
|
||||
|
||||
|
||||
/* Implements onCreate */
|
||||
override fun onCreate() {
|
||||
override fun onCreate()
|
||||
{
|
||||
super.onCreate()
|
||||
LogHelper.v(TAG, "Trackbook application started.")
|
||||
LogHelper.v("VOUSSOIR", "Trackbook application started.")
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
// initialize single sharedPreferences object when app is launched
|
||||
initPreferences()
|
||||
// set Dark / Light theme state
|
||||
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection())
|
||||
|
||||
ContextCompat.checkSelfPermission(applicationContext, android.Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
ContextCompat.checkSelfPermission(applicationContext, android.Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
Log.i("VOUSSOIR", "Device ID = ${PreferencesHelper.load_device_id()}")
|
||||
if (ContextCompat.checkSelfPermission(applicationContext, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
|
||||
{
|
||||
this.database.connect(File("/storage/emulated/0/Syncthing/GPX/trkpt_${PreferencesHelper.load_device_id()}.db"))
|
||||
}
|
||||
}
|
||||
|
||||
fun load_homepoints()
|
||||
{
|
||||
this.homepoints.clear()
|
||||
homepoint_generator().forEach { homepoint -> this.homepoints.add(homepoint) }
|
||||
}
|
||||
|
||||
/* Implements onTerminate */
|
||||
override fun onTerminate() {
|
||||
fun homepoint_generator() = iterator<Homepoint>
|
||||
{
|
||||
if (! database.ready)
|
||||
{
|
||||
return@iterator
|
||||
}
|
||||
val cursor: Cursor = database.connection.query(
|
||||
"homepoints",
|
||||
arrayOf("lat", "lon", "radius", "name"),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
val COLUMN_LAT = cursor.getColumnIndex("lat")
|
||||
val COLUMN_LON = cursor.getColumnIndex("lon")
|
||||
val COLUMN_RADIUS = cursor.getColumnIndex("radius")
|
||||
val COLUMN_NAME = cursor.getColumnIndex("name")
|
||||
try
|
||||
{
|
||||
while (cursor.moveToNext())
|
||||
{
|
||||
val homepoint = Homepoint(
|
||||
latitude=cursor.getDouble(COLUMN_LAT),
|
||||
longitude=cursor.getDouble(COLUMN_LON),
|
||||
radius=cursor.getDouble(COLUMN_RADIUS),
|
||||
name=cursor.getString(COLUMN_NAME),
|
||||
)
|
||||
yield(homepoint)
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTerminate()
|
||||
{
|
||||
super.onTerminate()
|
||||
LogHelper.v(TAG, "Trackbook application terminated.")
|
||||
LogHelper.v("VOUSSOIR", "Trackbook application terminated.")
|
||||
database.close()
|
||||
}
|
||||
|
||||
}
|
|
@ -31,17 +31,17 @@ import android.location.Location
|
|||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.Manifest
|
||||
import android.content.ContentValues
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.Runnable
|
||||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.core.load_temp_track
|
||||
import org.y20k.trackbook.core.Database
|
||||
import org.y20k.trackbook.core.Trkpt
|
||||
import org.y20k.trackbook.helpers.*
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
/*
|
||||
* TrackerService class
|
||||
|
@ -52,24 +52,25 @@ class TrackerService: Service(), SensorEventListener
|
|||
private val TAG: String = LogHelper.makeLogTag(TrackerService::class.java)
|
||||
|
||||
/* Main class variables */
|
||||
var trackingState: Int = Keys.STATE_TRACKING_NOT_STARTED
|
||||
var trackingState: Int = Keys.STATE_TRACKING_STOPPED
|
||||
var gpsProviderActive: Boolean = false
|
||||
var networkProviderActive: Boolean = false
|
||||
var useImperial: Boolean = false
|
||||
var gpsOnly: Boolean = false
|
||||
var omitRests: Boolean = true
|
||||
var autoExportInterval: Int = Keys.DEFAULT_AUTO_EXPORT_INTERVAL
|
||||
var device_id: String = random_int().toString()
|
||||
var recording_started: Date = GregorianCalendar.getInstance().time
|
||||
var commitInterval: Int = Keys.COMMIT_INTERVAL
|
||||
var currentBestLocation: Location = LocationHelper.getDefaultLocation()
|
||||
var lastTempSave: Date = Keys.DEFAULT_DATE
|
||||
var lastAutoExport: Date = Keys.DEFAULT_DATE
|
||||
var lastCommit: Date = Keys.DEFAULT_DATE
|
||||
var stepCountOffset: Float = 0f
|
||||
var track: Track = Track()
|
||||
lateinit var track: Track
|
||||
var gpsLocationListenerRegistered: Boolean = false
|
||||
var networkLocationListenerRegistered: Boolean = false
|
||||
var bound: Boolean = false
|
||||
private val binder = LocalBinder()
|
||||
private val handler: Handler = Handler(Looper.getMainLooper())
|
||||
private var altitudeValues: SimpleMovingAverageQueue = SimpleMovingAverageQueue(Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE)
|
||||
lateinit var trackbook: Trackbook
|
||||
private lateinit var locationManager: LocationManager
|
||||
private lateinit var sensorManager: SensorManager
|
||||
private lateinit var notificationManager: NotificationManager
|
||||
|
@ -111,7 +112,8 @@ class TrackerService: Service(), SensorEventListener
|
|||
}
|
||||
|
||||
/* Adds a Network location listener to location manager */
|
||||
private fun addNetworkLocationListener() {
|
||||
private fun addNetworkLocationListener()
|
||||
{
|
||||
if (gpsOnly)
|
||||
{
|
||||
LogHelper.v(TAG, "Skipping Network listener. User prefers GPS-only.")
|
||||
|
@ -150,12 +152,11 @@ class TrackerService: Service(), SensorEventListener
|
|||
|
||||
fun clearTrack()
|
||||
{
|
||||
track = Track()
|
||||
stepCountOffset = 0f
|
||||
FileHelper.delete_temp_file(this as Context)
|
||||
trackingState = Keys.STATE_TRACKING_NOT_STARTED
|
||||
trackingState = Keys.STATE_TRACKING_STOPPED
|
||||
PreferencesHelper.saveTrackingState(trackingState)
|
||||
stopForeground(true)
|
||||
track.delete()
|
||||
track = Track(trackbook.database, device_id, start_time=GregorianCalendar.getInstance().time, stop_time=Date(GregorianCalendar.getInstance().time.time + 86400))
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
notificationManager.cancel(Keys.TRACKER_SERVICE_NOTIFICATION_ID) // this call was not necessary prior to Android 12
|
||||
}
|
||||
|
||||
|
@ -205,9 +206,7 @@ class TrackerService: Service(), SensorEventListener
|
|||
private fun displayNotification(): Notification {
|
||||
val notification: Notification = notificationHelper.createNotification(
|
||||
trackingState,
|
||||
track.distance,
|
||||
track.duration,
|
||||
useImperial
|
||||
iso8601(GregorianCalendar.getInstance().time)
|
||||
)
|
||||
notificationManager.notify(Keys.TRACKER_SERVICE_NOTIFICATION_ID, notification)
|
||||
return notification
|
||||
|
@ -233,11 +232,14 @@ class TrackerService: Service(), SensorEventListener
|
|||
override fun onCreate()
|
||||
{
|
||||
super.onCreate()
|
||||
trackbook = (applicationContext as Trackbook)
|
||||
trackbook.load_homepoints()
|
||||
gpsOnly = PreferencesHelper.loadGpsOnly()
|
||||
device_id = PreferencesHelper.load_device_id()
|
||||
track = Track(trackbook.database, device_id, start_time=GregorianCalendar.getInstance().time, stop_time=Date(GregorianCalendar.getInstance().time.time + 86400))
|
||||
useImperial = PreferencesHelper.loadUseImperialUnits()
|
||||
omitRests = PreferencesHelper.loadOmitRests()
|
||||
autoExportInterval = PreferencesHelper.loadAutoExportInterval()
|
||||
|
||||
commitInterval = PreferencesHelper.loadCommitInterval()
|
||||
locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
@ -248,8 +250,6 @@ class TrackerService: Service(), SensorEventListener
|
|||
networkLocationListener = createLocationListener()
|
||||
trackingState = PreferencesHelper.loadTrackingState()
|
||||
currentBestLocation = LocationHelper.getLastKnownLocation(this)
|
||||
track = load_temp_track(this)
|
||||
// altitudeValues.capacity = PreferencesHelper.loadAltitudeSmoothingValue()
|
||||
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
|
||||
}
|
||||
|
||||
|
@ -282,13 +282,11 @@ class TrackerService: Service(), SensorEventListener
|
|||
if (sensorEvent != null) {
|
||||
if (stepCountOffset == 0f) {
|
||||
// store steps previously recorded by the system
|
||||
stepCountOffset = (sensorEvent.values[0] - 1) - track.stepCount // subtract any steps recorded during this session in case the app was killed
|
||||
stepCountOffset = (sensorEvent.values[0] - 1) - 0 // subtract any steps recorded during this session in case the app was killed
|
||||
}
|
||||
// calculate step count - subtract steps previously recorded
|
||||
steps = sensorEvent.values[0] - stepCountOffset
|
||||
}
|
||||
// update step count in track
|
||||
track.stepCount = steps
|
||||
}
|
||||
|
||||
/* Overrides onStartCommand from Service */
|
||||
|
@ -300,7 +298,7 @@ class TrackerService: Service(), SensorEventListener
|
|||
if (trackingState == Keys.STATE_TRACKING_ACTIVE)
|
||||
{
|
||||
LogHelper.w(TAG, "Trackbook has been killed by the operating system. Trying to resume recording.")
|
||||
resumeTracking()
|
||||
startTracking()
|
||||
}
|
||||
}
|
||||
else if (intent.action == Keys.ACTION_STOP)
|
||||
|
@ -311,10 +309,6 @@ class TrackerService: Service(), SensorEventListener
|
|||
{
|
||||
startTracking()
|
||||
}
|
||||
else if (intent.action == Keys.ACTION_RESUME)
|
||||
{
|
||||
resumeTracking()
|
||||
}
|
||||
|
||||
// START_STICKY is used for services that are explicitly started and stopped as needed
|
||||
return START_STICKY
|
||||
|
@ -358,42 +352,11 @@ class TrackerService: Service(), SensorEventListener
|
|||
}
|
||||
}
|
||||
|
||||
/* Resume tracking after stop/pause */
|
||||
fun resumeTracking()
|
||||
{
|
||||
// load temp track - returns an empty track if there is no temp file.
|
||||
track = load_temp_track(this)
|
||||
if (track.wayPoints.isNotEmpty()) {
|
||||
track.wayPoints.last().isStopOver = true
|
||||
}
|
||||
track.resumed = true
|
||||
track.recordingPaused += (GregorianCalendar.getInstance().time.time - track.recordingStop.time)
|
||||
startTracking(newTrack = false)
|
||||
}
|
||||
|
||||
fun saveTrackAndClear(context: Context)
|
||||
{
|
||||
this.pauseTracking()
|
||||
track.save_all_files(context)
|
||||
this.clearTrack()
|
||||
}
|
||||
|
||||
fun saveTrackAndStartNew(context: Context)
|
||||
{
|
||||
if (track.wayPoints.isNotEmpty())
|
||||
{
|
||||
track.save_all_files(context)
|
||||
}
|
||||
track = Track()
|
||||
FileHelper.delete_temp_file(this as Context)
|
||||
}
|
||||
|
||||
private fun startStepCounter()
|
||||
{
|
||||
val stepCounterAvailable = sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_UI)
|
||||
if (!stepCounterAvailable) {
|
||||
LogHelper.w(TAG, "Pedometer sensor not available.")
|
||||
track.stepCount = -1f
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -401,12 +364,11 @@ class TrackerService: Service(), SensorEventListener
|
|||
{
|
||||
addGpsLocationListener()
|
||||
addNetworkLocationListener()
|
||||
// set up new track
|
||||
if (newTrack) {
|
||||
track = Track()
|
||||
stepCountOffset = 0f
|
||||
}
|
||||
trackingState = Keys.STATE_TRACKING_ACTIVE
|
||||
if (newTrack)
|
||||
{
|
||||
this.recording_started = GregorianCalendar.getInstance().time
|
||||
}
|
||||
PreferencesHelper.saveTrackingState(trackingState)
|
||||
startStepCounter()
|
||||
handler.postDelayed(periodicTrackUpdate, 0)
|
||||
|
@ -415,19 +377,16 @@ class TrackerService: Service(), SensorEventListener
|
|||
|
||||
fun pauseTracking()
|
||||
{
|
||||
track.recordingStop = GregorianCalendar.getInstance().time
|
||||
CoroutineScope(IO).launch { track.save_temp_suspended(this@TrackerService) }
|
||||
trackbook.database.commit()
|
||||
|
||||
trackingState = Keys.STATE_TRACKING_PAUSED
|
||||
trackingState = Keys.STATE_TRACKING_STOPPED
|
||||
PreferencesHelper.saveTrackingState(trackingState)
|
||||
|
||||
altitudeValues.reset()
|
||||
|
||||
sensorManager.unregisterListener(this)
|
||||
handler.removeCallbacks(periodicTrackUpdate)
|
||||
|
||||
displayNotification()
|
||||
stopForeground(false)
|
||||
stopForeground(STOP_FOREGROUND_DETACH)
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -435,24 +394,25 @@ class TrackerService: Service(), SensorEventListener
|
|||
*/
|
||||
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
when (key) {
|
||||
// preference "Restrict to GPS"
|
||||
Keys.PREF_GPS_ONLY -> {
|
||||
Keys.PREF_GPS_ONLY ->
|
||||
{
|
||||
gpsOnly = PreferencesHelper.loadGpsOnly()
|
||||
when (gpsOnly) {
|
||||
true -> removeNetworkLocationListener()
|
||||
false -> addNetworkLocationListener()
|
||||
}
|
||||
}
|
||||
// preference "Use Imperial Measurements"
|
||||
Keys.PREF_USE_IMPERIAL_UNITS -> {
|
||||
Keys.PREF_USE_IMPERIAL_UNITS ->
|
||||
{
|
||||
useImperial = PreferencesHelper.loadUseImperialUnits()
|
||||
}
|
||||
// preference "Recording Accuracy"
|
||||
Keys.PREF_OMIT_RESTS -> {
|
||||
Keys.PREF_OMIT_RESTS ->
|
||||
{
|
||||
omitRests = PreferencesHelper.loadOmitRests()
|
||||
}
|
||||
Keys.PREF_AUTO_EXPORT_INTERVAL -> {
|
||||
autoExportInterval = PreferencesHelper.loadAutoExportInterval()
|
||||
Keys.PREF_DEVICE_ID ->
|
||||
{
|
||||
device_id = PreferencesHelper.load_device_id()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -470,84 +430,96 @@ class TrackerService: Service(), SensorEventListener
|
|||
* End of inner class
|
||||
*/
|
||||
|
||||
fun should_keep_point(location: Location): Boolean
|
||||
{
|
||||
if(! trackbook.database.ready)
|
||||
{
|
||||
Log.i("VOUSSOIR", "Omitting due to database not ready.")
|
||||
return false
|
||||
}
|
||||
if (location.latitude == 0.0 || location.longitude == 0.0)
|
||||
{
|
||||
Log.i("VOUSSOIR", "Omitting due to 0,0 location.")
|
||||
return false
|
||||
}
|
||||
if (! LocationHelper.isRecentEnough(location))
|
||||
{
|
||||
Log.i("VOUSSOIR", "Omitting due to not recent enough.")
|
||||
return false
|
||||
}
|
||||
if (! LocationHelper.isAccurateEnough(location, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY))
|
||||
{
|
||||
Log.i("VOUSSOIR", "Omitting due to not accurate enough.")
|
||||
return false
|
||||
}
|
||||
for (homepoint in trackbook.homepoints)
|
||||
{
|
||||
if (LocationHelper.calculateDistance(homepoint.location, location) < homepoint.radius)
|
||||
{
|
||||
Log.i("VOUSSOIR", "Omitting due to homepoint ${homepoint}.")
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (track.trkpts.isEmpty())
|
||||
{
|
||||
return true
|
||||
}
|
||||
if (! LocationHelper.isDifferentEnough(track.trkpts.last().toLocation(), location, omitRests))
|
||||
{
|
||||
Log.i("VOUSSOIR", "Omitting due to too close to previous.")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
/*
|
||||
* Runnable: Periodically track updates (if recording active)
|
||||
*/
|
||||
private val periodicTrackUpdate: Runnable = object : Runnable
|
||||
{
|
||||
override fun run() {
|
||||
// add waypoint to track - step count is continuously updated in onSensorChanged
|
||||
val success = track.add_waypoint(currentBestLocation, omitRests, track.resumed)
|
||||
val now: Date = GregorianCalendar.getInstance().time
|
||||
if (success) {
|
||||
track.resumed = false
|
||||
|
||||
// store previous smoothed altitude
|
||||
val previousAltitude: Double = altitudeValues.getAverage()
|
||||
// put current altitude into queue
|
||||
val currentBestLocationAltitude: Double = currentBestLocation.altitude
|
||||
if (currentBestLocationAltitude != Keys.DEFAULT_ALTITUDE) altitudeValues.add(currentBestLocationAltitude)
|
||||
// TODO remove
|
||||
// uncomment to use test altitude values - useful if testing with an emulator
|
||||
//altitudeValues.add(getTestAltitude()) // TODO remove
|
||||
// TODO remove
|
||||
|
||||
// only start calculating elevation differences, if enough data has been added to queue
|
||||
if (altitudeValues.prepared) {
|
||||
// get current smoothed altitude
|
||||
val currentAltitude: Double = altitudeValues.getAverage()
|
||||
// calculate and store elevation differences
|
||||
track = LocationHelper.calculateElevationDifferences(currentAltitude, previousAltitude, track)
|
||||
// TODO remove
|
||||
LogHelper.d(TAG, "Elevation Calculation || prev = $previousAltitude | curr = $currentAltitude | pos = ${track.positiveElevation} | neg = ${track.negativeElevation}")
|
||||
// TODO remove
|
||||
val nowstr: String = iso8601(now)
|
||||
val trkpt: Trkpt = Trkpt(location=currentBestLocation)
|
||||
Log.i("VOUSSOIR", "Processing point ${currentBestLocation.latitude}, ${currentBestLocation.longitude} ${nowstr}.")
|
||||
if (should_keep_point((currentBestLocation)))
|
||||
{
|
||||
val values = ContentValues().apply {
|
||||
put("device_id", device_id)
|
||||
put("lat", trkpt.latitude)
|
||||
put("lon", trkpt.longitude)
|
||||
put("time", nowstr)
|
||||
put("accuracy", trkpt.accuracy)
|
||||
put("sat", trkpt.numberSatellites)
|
||||
put("ele", trkpt.altitude)
|
||||
put("star", 0)
|
||||
}
|
||||
if (! trackbook.database.connection.inTransaction())
|
||||
{
|
||||
trackbook.database.connection.beginTransaction()
|
||||
}
|
||||
trackbook.database.connection.insert("trkpt", null, values)
|
||||
track.trkpts.add(trkpt)
|
||||
if (track.trkpts.size > track.dequelimit)
|
||||
{
|
||||
track.trkpts.removeFirst()
|
||||
}
|
||||
|
||||
// save a temp track
|
||||
if (now.time - lastTempSave.time > Keys.SAVE_TEMP_TRACK_INTERVAL) {
|
||||
lastTempSave = now
|
||||
CoroutineScope(IO).launch { track.save_temp_suspended(this@TrackerService) }
|
||||
if (now.time - lastCommit.time > Keys.SAVE_TEMP_TRACK_INTERVAL)
|
||||
{
|
||||
if (trackbook.database.connection.inTransaction())
|
||||
{
|
||||
trackbook.database.commit()
|
||||
}
|
||||
lastCommit = now
|
||||
}
|
||||
|
||||
}
|
||||
if (now.time - track.recordingStart.time > (autoExportInterval * Keys.ONE_HOUR_IN_MILLISECONDS)) {
|
||||
saveTrackAndStartNew(this@TrackerService)
|
||||
}
|
||||
// update notification
|
||||
displayNotification()
|
||||
// re-run this in set interval
|
||||
handler.postDelayed(this, Keys.ADD_WAYPOINT_TO_TRACK_INTERVAL)
|
||||
handler.postDelayed(this, Keys.TRACKING_INTERVAL)
|
||||
}
|
||||
}
|
||||
/*
|
||||
* End of declaration
|
||||
*/
|
||||
|
||||
/* Simple queue that evicts older elements and holds an average */
|
||||
/* Credit: CircularQueue https://stackoverflow.com/a/51923797 */
|
||||
class SimpleMovingAverageQueue(var capacity: Int) : LinkedList<Double>()
|
||||
{
|
||||
var prepared: Boolean = false
|
||||
private var sum: Double = 0.0
|
||||
override fun add(element: Double): Boolean
|
||||
{
|
||||
prepared = this.size + 1 >= Keys.MIN_NUMBER_OF_WAYPOINTS_FOR_ELEVATION_CALCULATION
|
||||
if (this.size >= capacity) {
|
||||
sum -= this.first
|
||||
removeFirst()
|
||||
}
|
||||
sum += element
|
||||
return super.add(element)
|
||||
}
|
||||
fun getAverage(): Double
|
||||
{
|
||||
return sum / this.size
|
||||
}
|
||||
fun reset()
|
||||
{
|
||||
this.clear()
|
||||
prepared = false
|
||||
sum = 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ class TrackingToggleTileService: TileService() {
|
|||
|
||||
/* Main class variables */
|
||||
private var bound: Boolean = false
|
||||
private var trackingState: Int = Keys.STATE_TRACKING_NOT_STARTED
|
||||
private var trackingState: Int = Keys.STATE_TRACKING_STOPPED
|
||||
private lateinit var trackerService: TrackerService
|
||||
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ import kotlinx.coroutines.Dispatchers.Main
|
|||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.helpers.LogHelper
|
||||
import org.y20k.trackbook.helpers.UiHelper
|
||||
import org.y20k.trackbook.helpers.iso8601_format
|
||||
import org.y20k.trackbook.tracklist.TracklistAdapter
|
||||
|
||||
|
||||
|
@ -48,21 +49,18 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
|
|||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(TracklistFragment::class.java)
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private lateinit var tracklistAdapter: TracklistAdapter
|
||||
private lateinit var trackElementList: RecyclerView
|
||||
private lateinit var tracklistOnboarding: ConstraintLayout
|
||||
|
||||
|
||||
/* Overrides onCreateView from Fragment */
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// create tracklist adapter
|
||||
tracklistAdapter = TracklistAdapter(this)
|
||||
tracklistAdapter = TracklistAdapter(this, (requireActivity().applicationContext as Trackbook).database)
|
||||
}
|
||||
|
||||
|
||||
/* Overrides onCreateView from Fragment */
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
// find views
|
||||
|
@ -97,9 +95,9 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
|
|||
override fun onTrackElementTapped(track: Track) {
|
||||
val bundle: Bundle = bundleOf(
|
||||
Keys.ARG_TRACK_TITLE to track.name,
|
||||
Keys.ARG_TRACK_FILE_URI to track.get_json_file(activity as Context).toUri().toString(),
|
||||
Keys.ARG_GPX_FILE_URI to track.get_gpx_file(activity as Context).toUri().toString(),
|
||||
Keys.ARG_TRACK_ID to track.id
|
||||
Keys.ARG_TRACK_DEVICE_ID to track.device_id,
|
||||
Keys.ARG_TRACK_START_TIME to iso8601_format.format(track.start_time),
|
||||
Keys.ARG_TRACK_STOP_TIME to iso8601_format.format(track.stop_time),
|
||||
)
|
||||
findNavController().navigate(R.id.fragment_track, bundle)
|
||||
}
|
||||
|
@ -130,7 +128,6 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// toggle onboarding layout
|
||||
private fun toggleOnboardingLayout() {
|
||||
when (tracklistAdapter.isEmpty()) {
|
||||
|
@ -147,13 +144,11 @@ class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener,
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Inner class: custom LinearLayoutManager that overrides onLayoutCompleted
|
||||
*/
|
||||
inner class CustomLinearLayoutManager(context: Context): LinearLayoutManager(context, VERTICAL, false) {
|
||||
|
||||
inner class CustomLinearLayoutManager(context: Context): LinearLayoutManager(context, VERTICAL, false)
|
||||
{
|
||||
override fun supportsPredictiveItemAnimations(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
|
52
app/src/main/java/org/y20k/trackbook/core/Database.kt
Normal file
52
app/src/main/java/org/y20k/trackbook/core/Database.kt
Normal file
|
@ -0,0 +1,52 @@
|
|||
package org.y20k.trackbook.core
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteDatabase.openOrCreateDatabase
|
||||
import android.util.Log
|
||||
import java.io.File
|
||||
|
||||
class Database()
|
||||
{
|
||||
var ready: Boolean = false
|
||||
lateinit var file: File
|
||||
lateinit var connection: SQLiteDatabase
|
||||
fun close()
|
||||
{
|
||||
this.connection.close()
|
||||
this.ready = false
|
||||
}
|
||||
|
||||
fun connect(file: File)
|
||||
{
|
||||
this.file = file
|
||||
this.connection = openOrCreateDatabase(file, null)
|
||||
this.initialize_tables()
|
||||
this.ready = true
|
||||
}
|
||||
|
||||
fun commit()
|
||||
{
|
||||
if (! this.ready)
|
||||
{
|
||||
return
|
||||
}
|
||||
if (! this.connection.inTransaction())
|
||||
{
|
||||
return
|
||||
}
|
||||
Log.i("VOUSSOIR", "Committing.")
|
||||
this.connection.setTransactionSuccessful()
|
||||
this.connection.endTransaction()
|
||||
}
|
||||
|
||||
private fun initialize_tables()
|
||||
{
|
||||
this.connection.beginTransaction()
|
||||
this.connection.execSQL("CREATE TABLE IF NOT EXISTS meta(name TEXT PRIMARY KEY, value TEXT)")
|
||||
this.connection.execSQL("CREATE TABLE IF NOT EXISTS trkpt(lat REAL NOT NULL, lon REAL NOT NULL, time TEXT NOT NULL, accuracy REAL, device_id INTEGER NOT NULL, ele INTEGER, sat INTEGER, star INTEGER, PRIMARY KEY(lat, lon, time, device_id))")
|
||||
this.connection.execSQL("CREATE TABLE IF NOT EXISTS homepoints(lat REAL NOT NULL, lon REAL NOT NULL, radius REAL NOT NULL, name TEXT, PRIMARY KEY(lat, lon))")
|
||||
this.connection.setTransactionSuccessful()
|
||||
this.connection.endTransaction()
|
||||
}
|
||||
}
|
20
app/src/main/java/org/y20k/trackbook/core/Homepoint.kt
Normal file
20
app/src/main/java/org/y20k/trackbook/core/Homepoint.kt
Normal file
|
@ -0,0 +1,20 @@
|
|||
package org.y20k.trackbook.core
|
||||
|
||||
import android.location.Location
|
||||
import java.util.*
|
||||
|
||||
class Homepoint(val latitude: Double, val longitude: Double, val radius: Double, val name: String)
|
||||
{
|
||||
val location: Location = this.to_location()
|
||||
|
||||
private fun to_location(): Location
|
||||
{
|
||||
val location: Location = Location("homepoint")
|
||||
location.latitude = latitude
|
||||
location.longitude = longitude
|
||||
location.altitude = 0.0
|
||||
location.accuracy = radius.toFloat()
|
||||
location.time = GregorianCalendar.getInstance().time.time
|
||||
return location
|
||||
}
|
||||
}
|
|
@ -16,260 +16,78 @@
|
|||
|
||||
package org.y20k.trackbook.core
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.database.Cursor
|
||||
import android.location.Location
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.google.gson.annotations.Expose
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.helpers.DateTimeHelper
|
||||
import org.y20k.trackbook.helpers.FileHelper
|
||||
import org.y20k.trackbook.helpers.LocationHelper
|
||||
import org.y20k.trackbook.helpers.iso8601
|
||||
import org.y20k.trackbook.helpers.iso8601_format
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.random.Random
|
||||
|
||||
/*
|
||||
* Track data class
|
||||
*/
|
||||
@Keep
|
||||
@Parcelize
|
||||
data class Track (
|
||||
@Expose val id: Long = make_random_id(),
|
||||
@Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION,
|
||||
@Expose val wayPoints: MutableList<WayPoint> = mutableListOf<WayPoint>(),
|
||||
@Expose var distance: Float = 0f,
|
||||
@Expose var duration: Long = 0L,
|
||||
@Expose var recordingPaused: Long = 0L,
|
||||
@Expose var stepCount: Float = 0f,
|
||||
@Expose var recordingStart: Date = GregorianCalendar.getInstance().time,
|
||||
@Expose var dateString: String = DateTimeHelper.convertToReadableDate(recordingStart),
|
||||
@Expose var recordingStop: Date = recordingStart,
|
||||
// The resumed flag will be true for the first point that is received after unpausing a
|
||||
// recording, so that the distance travelled while paused is not added to the track.distance.
|
||||
@Expose var resumed: Boolean = false,
|
||||
@Expose var maxAltitude: Double = 0.0,
|
||||
@Expose var minAltitude: Double = 0.0,
|
||||
@Expose var positiveElevation: Double = 0.0,
|
||||
@Expose var negativeElevation: Double = 0.0,
|
||||
@Expose var latitude: Double = Keys.DEFAULT_LATITUDE,
|
||||
@Expose var longitude: Double = Keys.DEFAULT_LONGITUDE,
|
||||
@Expose var zoomLevel: Double = Keys.DEFAULT_ZOOM_LEVEL,
|
||||
@Expose var name: String = DateTimeHelper.convertToReadableDate(recordingStart),
|
||||
@Expose var starred: Boolean = false,
|
||||
): Parcelable
|
||||
val database: Database,
|
||||
val device_id: String,
|
||||
val start_time: Date,
|
||||
val stop_time: Date,
|
||||
var name: String = "",
|
||||
var dequelimit: Int = 7200,
|
||||
var view_latitude: Double = Keys.DEFAULT_LATITUDE,
|
||||
var view_longitude: Double = Keys.DEFAULT_LONGITUDE,
|
||||
var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION,
|
||||
val trkpts: ArrayDeque<Trkpt> = ArrayDeque<Trkpt>(dequelimit),
|
||||
var zoomLevel: Double = Keys.DEFAULT_ZOOM_LEVEL,
|
||||
)
|
||||
{
|
||||
fun add_waypoint(location: Location, omitRests: Boolean, resumed: Boolean): Boolean
|
||||
fun delete()
|
||||
{
|
||||
// Step 1: Get previous location
|
||||
val previousLocation: Location?
|
||||
var numberOfWayPoints: Int = this.wayPoints.size
|
||||
|
||||
// CASE: First location
|
||||
if (numberOfWayPoints == 0)
|
||||
{
|
||||
previousLocation = null
|
||||
}
|
||||
// CASE: Second location - check if first location was plausible & remove implausible location
|
||||
else if (numberOfWayPoints == 1 && !LocationHelper.isFirstLocationPlausible(location, this))
|
||||
{
|
||||
previousLocation = null
|
||||
numberOfWayPoints = 0
|
||||
this.wayPoints.removeAt(0)
|
||||
}
|
||||
// CASE: Third location or second location (if first was plausible)
|
||||
else
|
||||
{
|
||||
previousLocation = this.wayPoints[numberOfWayPoints - 1].toLocation()
|
||||
}
|
||||
|
||||
// Step 2: Update duration
|
||||
val now: Date = GregorianCalendar.getInstance().time
|
||||
val difference: Long = now.time - this.recordingStop.time
|
||||
this.duration += difference
|
||||
this.recordingStop = now
|
||||
|
||||
// Step 3: Add waypoint, if recent and accurate and different enough
|
||||
val shouldBeAdded: Boolean = (
|
||||
LocationHelper.isRecentEnough(location) &&
|
||||
LocationHelper.isAccurateEnough(location, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY) &&
|
||||
LocationHelper.isDifferentEnough(previousLocation, location, omitRests)
|
||||
)
|
||||
if (! shouldBeAdded)
|
||||
{
|
||||
return false
|
||||
}
|
||||
// Step 3.1: Update distance (do not update if resumed -> we do not want to add values calculated during a recording pause)
|
||||
if (!resumed)
|
||||
{
|
||||
this.distance = this.distance + LocationHelper.calculateDistance(previousLocation, location)
|
||||
}
|
||||
// Step 3.2: Update altitude values
|
||||
val altitude: Double = location.altitude
|
||||
if (altitude != 0.0)
|
||||
{
|
||||
if (numberOfWayPoints == 0)
|
||||
{
|
||||
this.maxAltitude = altitude
|
||||
this.minAltitude = altitude
|
||||
}
|
||||
else
|
||||
{
|
||||
if (altitude > this.maxAltitude) this.maxAltitude = altitude
|
||||
if (altitude < this.minAltitude) this.minAltitude = altitude
|
||||
}
|
||||
}
|
||||
// Step 3.3: Toggle stop over status, if necessary
|
||||
if (this.wayPoints.size < 0)
|
||||
{
|
||||
this.wayPoints[this.wayPoints.size - 1].isStopOver = LocationHelper.isStopOver(previousLocation, location)
|
||||
}
|
||||
|
||||
// Step 3.4: Add current location as point to center on for later display
|
||||
this.latitude = location.latitude
|
||||
this.longitude = location.longitude
|
||||
|
||||
// Step 3.5: Add location as new waypoint
|
||||
this.wayPoints.add(WayPoint(location = location, distanceToStartingPoint = this.distance))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun delete(context: Context)
|
||||
{
|
||||
Log.i("VOUSSOIR", "Deleting track ${this.id}.")
|
||||
val json_file: File = this.get_json_file(context)
|
||||
if (json_file.isFile)
|
||||
{
|
||||
json_file.delete()
|
||||
}
|
||||
val gpx_file: File = this.get_gpx_file(context)
|
||||
if (gpx_file.isFile)
|
||||
{
|
||||
gpx_file.delete()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun delete_suspended(context: Context)
|
||||
{
|
||||
return suspendCoroutine { cont ->
|
||||
cont.resume(this.delete(context))
|
||||
cont.resume(this.delete())
|
||||
}
|
||||
}
|
||||
|
||||
fun get_gpx_file(context: Context): File
|
||||
{
|
||||
val basename: String = this.id.toString() + Keys.GPX_FILE_EXTENSION
|
||||
return File(context.getExternalFilesDir(Keys.FOLDER_GPX), basename)
|
||||
}
|
||||
|
||||
fun get_export_gpx_file(context: Context): File
|
||||
{
|
||||
val basename: String = DateTimeHelper.convertToSortableDateString(this.recordingStart) + Keys.GPX_FILE_EXTENSION
|
||||
val basename: String = DateTimeHelper.convertToSortableDateString(this.start_time) + " " + DateTimeHelper.convertToSortableDateString(this.start_time) + Keys.GPX_FILE_EXTENSION
|
||||
return File(File("/storage/emulated/0/Syncthing/GPX"), basename)
|
||||
}
|
||||
|
||||
fun get_json_file(context: Context): File
|
||||
fun export_gpx(context: Context, fileuri: Uri): Uri?
|
||||
{
|
||||
val basename: String = this.id.toString() + Keys.TRACKBOOK_FILE_EXTENSION
|
||||
return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), basename)
|
||||
}
|
||||
|
||||
fun save_all_files(context: Context)
|
||||
{
|
||||
this.save_json(context)
|
||||
this.save_gpx(context)
|
||||
this.save_export_gpx(context)
|
||||
}
|
||||
|
||||
suspend fun save_all_files_suspended(context: Context)
|
||||
{
|
||||
return suspendCoroutine { cont ->
|
||||
cont.resume(this.save_all_files(context))
|
||||
if (! database.ready)
|
||||
{
|
||||
Log.i("VOUSSOIR", "Failed to export due to database not ready.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun save_gpx(context: Context)
|
||||
{
|
||||
val gpx: String = this.to_gpx()
|
||||
FileHelper.write_text_file_noblank(gpx, this.get_gpx_file(context))
|
||||
Log.i("VOUSSOIR", "Saved ${this.id}.gpx")
|
||||
}
|
||||
|
||||
suspend fun save_gpx_suspended(context: Context)
|
||||
{
|
||||
return suspendCoroutine { cont ->
|
||||
cont.resume(this.save_gpx(context))
|
||||
Log.i("VOUSSOIR", "Let's export to " + fileuri.toString())
|
||||
val writer = context.contentResolver.openOutputStream(fileuri)
|
||||
if (writer == null)
|
||||
{
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun save_export_gpx(context: Context)
|
||||
{
|
||||
val gpx: String = this.to_gpx()
|
||||
val outputfile: File = this.get_export_gpx_file(context)
|
||||
FileHelper.write_text_file_noblank(gpx, outputfile)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(context, outputfile.toString(), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun save_export_gpx_suspended(context: Context)
|
||||
{
|
||||
return suspendCoroutine { cont ->
|
||||
cont.resume(this.save_export_gpx(context))
|
||||
}
|
||||
}
|
||||
|
||||
fun save_json(context: Context)
|
||||
{
|
||||
val json: String = this.to_json()
|
||||
FileHelper.write_text_file_noblank(json, this.get_json_file(context))
|
||||
Log.i("VOUSSOIR", "Saved ${this.id}.json")
|
||||
}
|
||||
|
||||
suspend fun save_json_suspended(context: Context)
|
||||
{
|
||||
return suspendCoroutine { cont ->
|
||||
cont.resume(this.save_json(context))
|
||||
}
|
||||
}
|
||||
|
||||
fun save_temp(context: Context)
|
||||
{
|
||||
val json: String = this.to_json()
|
||||
FileHelper.write_text_file_noblank(json, FileHelper.get_temp_file(context))
|
||||
}
|
||||
|
||||
suspend fun save_temp_suspended(context: Context)
|
||||
{
|
||||
return suspendCoroutine { cont ->
|
||||
cont.resume(this.save_temp(context))
|
||||
}
|
||||
}
|
||||
|
||||
fun to_gpx(): String {
|
||||
val gpxString = StringBuilder("")
|
||||
|
||||
// Header
|
||||
gpxString.appendLine("""
|
||||
val write = {x: String -> writer.write(x.encodeToByteArray()); writer.write("\n".encodeToByteArray())}
|
||||
|
||||
write("""
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<gpx
|
||||
version="1.1" creator="Trackbook App (Android)"
|
||||
|
@ -278,62 +96,140 @@ data class Track (
|
|||
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
|
||||
>
|
||||
""".trimIndent())
|
||||
gpxString.appendLine("\t<metadata>")
|
||||
gpxString.appendLine("\t\t<name>Trackbook Recording: ${this.name}</name>")
|
||||
gpxString.appendLine("\t</metadata>")
|
||||
|
||||
// POIs
|
||||
val poiList: List<WayPoint> = this.wayPoints.filter { it.starred }
|
||||
poiList.forEach { poi ->
|
||||
gpxString.appendLine("\t<wpt lat=\"${poi.latitude}\" lon=\"${poi.longitude}\">")
|
||||
gpxString.appendLine("\t\t<name>Point of interest</name>")
|
||||
gpxString.appendLine("\t\t<ele>${poi.altitude}</ele>")
|
||||
gpxString.appendLine("\t</wpt>")
|
||||
}
|
||||
write("\t<metadata>")
|
||||
write("\t\t<name>Trackbook Recording: ${this.name}</name>")
|
||||
write("\t\t<device>${this.device_id}</device>")
|
||||
write("\t</metadata>")
|
||||
|
||||
// TRK
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
|
||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
gpxString.appendLine("\t<trk>")
|
||||
gpxString.appendLine("\t\t<name>${this.name}</name>")
|
||||
gpxString.appendLine("\t\t<trkseg>")
|
||||
this.wayPoints.forEach { wayPoint ->
|
||||
gpxString.appendLine("\t\t\t<trkpt lat=\"${wayPoint.latitude}\" lon=\"${wayPoint.longitude}\">")
|
||||
gpxString.appendLine("\t\t\t\t<ele>${wayPoint.altitude}</ele>")
|
||||
gpxString.appendLine("\t\t\t\t<time>${dateFormat.format(Date(wayPoint.time))}</time>")
|
||||
gpxString.appendLine("\t\t\t\t<sat>${wayPoint.numberSatellites}</sat>")
|
||||
gpxString.appendLine("\t\t\t</trkpt>")
|
||||
write("\t<trk>")
|
||||
write("\t\t<name>${this.name}</name>")
|
||||
write("\t\t<trkseg>")
|
||||
|
||||
trkpt_generator().forEach { trkpt ->
|
||||
write("\t\t\t<trkpt lat=\"${trkpt.latitude}\" lon=\"${trkpt.longitude}\">")
|
||||
write("\t\t\t\t<ele>${trkpt.altitude}</ele>")
|
||||
write("\t\t\t\t<time>${iso8601_format.format(trkpt.time)}</time>")
|
||||
write("\t\t\t\t<sat>${trkpt.numberSatellites}</sat>")
|
||||
write("\t\t\t</trkpt>")
|
||||
}
|
||||
gpxString.appendLine("\t\t</trkseg>")
|
||||
gpxString.appendLine("\t</trk>")
|
||||
gpxString.appendLine("</gpx>")
|
||||
|
||||
return gpxString.toString()
|
||||
write("\t\t</trkseg>")
|
||||
write("\t</trk>")
|
||||
write("</gpx>")
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(context, fileuri.toString(), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return fileuri
|
||||
}
|
||||
|
||||
fun to_json(): String
|
||||
fun load_trkpts()
|
||||
{
|
||||
return FileHelper.getCustomGson().toJson(this)
|
||||
this.trkpts.clear()
|
||||
trkpt_generator().forEach { trkpt -> this.trkpts.add(trkpt) }
|
||||
if (this.trkpts.size > 0)
|
||||
{
|
||||
this.view_latitude = this.trkpts.first().latitude
|
||||
this.view_longitude = this.trkpts.first().longitude
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun load_temp_track(context: Context): Track
|
||||
{
|
||||
return track_from_file(context, FileHelper.get_temp_file(context))
|
||||
}
|
||||
|
||||
fun track_from_file(context: Context, file: File): Track
|
||||
{
|
||||
// get JSON from text file
|
||||
val json: String = FileHelper.readTextFile(context, file)
|
||||
if (json.isEmpty())
|
||||
fun statistics(): TrackStatistics
|
||||
{
|
||||
return Track()
|
||||
var first: Trkpt? = null
|
||||
var last: Trkpt? = null
|
||||
var previous: Trkpt? = null
|
||||
val stats = TrackStatistics()
|
||||
for (trkpt in trkpt_generator())
|
||||
{
|
||||
if (previous == null)
|
||||
{
|
||||
first = trkpt
|
||||
previous = trkpt
|
||||
stats.max_altitude = trkpt.altitude
|
||||
stats.min_altitude = trkpt.altitude
|
||||
continue
|
||||
}
|
||||
stats.distance += LocationHelper.calculateDistance(previous.toLocation(), trkpt.toLocation())
|
||||
val ascentdiff = trkpt.altitude - previous.altitude
|
||||
if (ascentdiff > 0)
|
||||
{
|
||||
stats.total_ascent += ascentdiff
|
||||
}
|
||||
else
|
||||
{
|
||||
stats.total_descent += ascentdiff
|
||||
}
|
||||
if (trkpt.altitude > stats.max_altitude)
|
||||
{
|
||||
stats.max_altitude = trkpt.altitude
|
||||
}
|
||||
if (trkpt.altitude < stats.min_altitude)
|
||||
{
|
||||
stats.min_altitude = trkpt.altitude
|
||||
}
|
||||
last = trkpt
|
||||
}
|
||||
if (first == null || last == null)
|
||||
{
|
||||
return stats
|
||||
}
|
||||
stats.duration = last.time.time - first.time.time
|
||||
stats.velocity = stats.distance / stats.duration
|
||||
return stats
|
||||
}
|
||||
|
||||
fun trkpt_generator() = iterator<Trkpt>
|
||||
{
|
||||
val cursor: Cursor = database.connection.query(
|
||||
"trkpt",
|
||||
arrayOf("lat", "lon", "time", "ele", "accuracy", "sat"),
|
||||
"device_id = ? AND time > ? AND time < ?",
|
||||
arrayOf(device_id, iso8601(start_time), iso8601(stop_time)),
|
||||
null,
|
||||
null,
|
||||
"time ASC",
|
||||
null,
|
||||
)
|
||||
val COLUMN_LAT = cursor.getColumnIndex("lat")
|
||||
val COLUMN_LON = cursor.getColumnIndex("lon")
|
||||
val COLUMN_ELE = cursor.getColumnIndex("ele")
|
||||
val COLUMN_SAT = cursor.getColumnIndex("sat")
|
||||
val COLUMN_ACCURACY = cursor.getColumnIndex("accuracy")
|
||||
val COLUMN_TIME = cursor.getColumnIndex("time")
|
||||
try
|
||||
{
|
||||
while (cursor.moveToNext())
|
||||
{
|
||||
val trkpt: Trkpt = Trkpt(
|
||||
provider="",
|
||||
latitude=cursor.getDouble(COLUMN_LAT),
|
||||
longitude=cursor.getDouble(COLUMN_LON),
|
||||
altitude=cursor.getDouble(COLUMN_ELE),
|
||||
accuracy=cursor.getFloat(COLUMN_ACCURACY),
|
||||
time=iso8601_format.parse(cursor.getString(COLUMN_TIME)),
|
||||
distanceToStartingPoint=0F,
|
||||
numberSatellites=cursor.getInt(COLUMN_SAT),
|
||||
)
|
||||
yield(trkpt)
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return FileHelper.getCustomGson().fromJson(json, Track::class.java)
|
||||
}
|
||||
|
||||
fun make_random_id(): Long
|
||||
{
|
||||
return (Random.nextBits(31).toLong() shl 32) + Random.nextBits(32)
|
||||
}
|
||||
data class TrackStatistics(
|
||||
var distance: Double = 0.0,
|
||||
var duration: Long = 0,
|
||||
var velocity: Double = 0.0,
|
||||
var total_ascent: Double = 0.0,
|
||||
var total_descent: Double = 0.0,
|
||||
var max_altitude: Double = 0.0,
|
||||
var min_altitude: Double = 0.0,
|
||||
)
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
/*
|
||||
* Tracklist.kt
|
||||
* Implements the Tracklist data class
|
||||
* A Tracklist stores a list of Tracks
|
||||
*
|
||||
* This file is part of
|
||||
* TRACKBOOK - Movement Recorder for Android
|
||||
*
|
||||
* Copyright (c) 2016-22 - 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.core
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.Expose
|
||||
import java.io.File
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.y20k.trackbook.Keys
|
||||
|
||||
/*
|
||||
* Tracklist data class
|
||||
*/
|
||||
@Keep
|
||||
@Parcelize
|
||||
data class Tracklist (
|
||||
@Expose val tracklistFormatVersion: Int = Keys.CURRENT_TRACKLIST_FORMAT_VERSION,
|
||||
@Expose val tracks: MutableList<Track> = mutableListOf<Track>()
|
||||
): Parcelable
|
||||
{
|
||||
fun delete_non_starred(context: Context)
|
||||
{
|
||||
val to_delete: List<Track> = this.tracks.filter{! it.starred}
|
||||
to_delete.forEach { track ->
|
||||
if (!track.starred)
|
||||
{
|
||||
track.delete(context)
|
||||
}
|
||||
}
|
||||
this.tracks.removeIf{! it.starred}
|
||||
}
|
||||
suspend fun delete_non_starred_suspended(context: Context)
|
||||
{
|
||||
return suspendCoroutine { cont ->
|
||||
cont.resume(this.delete_non_starred(context))
|
||||
}
|
||||
}
|
||||
|
||||
fun get_total_distance(): Double
|
||||
{
|
||||
return this.tracks.sumOf {it.distance.toDouble()}
|
||||
}
|
||||
|
||||
fun get_total_duration(): Long
|
||||
{
|
||||
return this.tracks.sumOf {it.duration}
|
||||
}
|
||||
|
||||
fun deepCopy(): Tracklist
|
||||
{
|
||||
return Tracklist(tracklistFormatVersion, mutableListOf<Track>().apply { addAll(tracks) })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun load_tracklist(context: Context): Tracklist {
|
||||
Log.i("VOUSSOIR", "Loading tracklist.")
|
||||
val folder = context.getExternalFilesDir("tracks")
|
||||
val tracklist: Tracklist = Tracklist()
|
||||
if (folder == null)
|
||||
{
|
||||
return tracklist
|
||||
}
|
||||
folder.walk().filter{ f: File -> f.isFile }.forEach{ json_file ->
|
||||
val track = track_from_file(context, json_file)
|
||||
tracklist.tracks.add(track)
|
||||
}
|
||||
tracklist.tracks.sortByDescending {it.recordingStart}
|
||||
return tracklist
|
||||
}
|
||||
|
||||
suspend fun load_tracklist_suspended(context: Context): Tracklist
|
||||
{
|
||||
return suspendCoroutine {cont -> cont.resume(load_tracklist(context))}
|
||||
}
|
|
@ -23,19 +23,19 @@ import androidx.annotation.Keep
|
|||
import com.google.gson.annotations.Expose
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.y20k.trackbook.helpers.LocationHelper
|
||||
|
||||
import java.util.*
|
||||
|
||||
/*
|
||||
* WayPoint data class
|
||||
*/
|
||||
@Keep
|
||||
@Parcelize
|
||||
data class WayPoint(@Expose val provider: String,
|
||||
data class Trkpt(@Expose val provider: String,
|
||||
@Expose val latitude: Double,
|
||||
@Expose val longitude: Double,
|
||||
@Expose val altitude: Double,
|
||||
@Expose val accuracy: Float,
|
||||
@Expose val time: Long,
|
||||
@Expose val time: Date,
|
||||
@Expose val distanceToStartingPoint: Float = 0f,
|
||||
@Expose val numberSatellites: Int = 0,
|
||||
@Expose var isStopOver: Boolean = false,
|
||||
|
@ -43,28 +43,16 @@ data class WayPoint(@Expose val provider: String,
|
|||
|
||||
/* Constructor using just Location */
|
||||
constructor(location: Location) : this (
|
||||
provider=location.provider,
|
||||
provider=location.provider.toString(),
|
||||
latitude=location.latitude,
|
||||
longitude=location.longitude,
|
||||
altitude=location. altitude,
|
||||
altitude=location.altitude,
|
||||
accuracy=location.accuracy,
|
||||
time=location.time,
|
||||
time=Date(location.time),
|
||||
distanceToStartingPoint=0F,
|
||||
numberSatellites=LocationHelper.getNumberOfSatellites(location),
|
||||
)
|
||||
|
||||
/* Constructor using Location plus distanceToStartingPoint and numberSatellites */
|
||||
constructor(location: Location, distanceToStartingPoint: Float) : this (
|
||||
provider=location.provider,
|
||||
latitude=location.latitude,
|
||||
longitude=location.longitude,
|
||||
altitude=location. altitude,
|
||||
accuracy=location.accuracy,
|
||||
time=location.time,
|
||||
distanceToStartingPoint=distanceToStartingPoint,
|
||||
numberSatellites=LocationHelper.getNumberOfSatellites(location),
|
||||
)
|
||||
|
||||
/* Converts WayPoint into Location */
|
||||
fun toLocation(): Location {
|
||||
val location: Location = Location(provider)
|
||||
|
@ -72,7 +60,7 @@ data class WayPoint(@Expose val provider: String,
|
|||
location.longitude = longitude
|
||||
location.altitude = altitude
|
||||
location.accuracy = accuracy
|
||||
location.time = time
|
||||
location.time = this.time.time
|
||||
return location
|
||||
}
|
||||
|
|
@ -38,31 +38,6 @@ object FileHelper {
|
|||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(FileHelper::class.java)
|
||||
|
||||
fun delete_temp_file(context: Context)
|
||||
{
|
||||
val temp: File = get_temp_file(context)
|
||||
if (temp.isFile())
|
||||
{
|
||||
temp.delete()
|
||||
}
|
||||
}
|
||||
fun get_temp_file(context: Context): File
|
||||
{
|
||||
return File(context.getExternalFilesDir(Keys.FOLDER_TEMP), Keys.TEMP_FILE)
|
||||
}
|
||||
|
||||
/* Suspend function: Wrapper for renameTrack */
|
||||
suspend fun renameTrackSuspended(context: Context, track: Track, newName: String) {
|
||||
return suspendCoroutine { cont ->
|
||||
track.name = newName
|
||||
track.save_json(context)
|
||||
track.save_gpx(context)
|
||||
cont.resume(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Suspend function: Wrapper for copyFile */
|
||||
suspend fun saveCopyOfFileSuspended(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) {
|
||||
return suspendCoroutine { cont ->
|
||||
|
|
|
@ -96,15 +96,12 @@ object LengthUnitHelper {
|
|||
|
||||
|
||||
/* Converts for the given unit System distance and duration values to a readable velocity string */
|
||||
fun convertToVelocityString(trackDuration: Long, trackRecordingPause: Long, trackLength: Float, useImperialUnits: Boolean = false) : String {
|
||||
fun convertToVelocityString(velocity: Double, useImperialUnits: Boolean = false) : String {
|
||||
var speed: String = "0"
|
||||
|
||||
// duration minus pause in seconds
|
||||
val duration: Long = (trackDuration - trackRecordingPause) / 1000L
|
||||
|
||||
if (duration > 0L) {
|
||||
if (velocity > 0.0) {
|
||||
// speed in km/h / mph
|
||||
val velocity: Double = convertMetersPerSecond((trackLength / duration), useImperialUnits)
|
||||
val velocity: Double = convertMetersPerSecond(velocity, useImperialUnits)
|
||||
// create readable speed string
|
||||
var bd: BigDecimal = BigDecimal.valueOf(velocity)
|
||||
bd = bd.setScale(1, RoundingMode.HALF_UP)
|
||||
|
@ -119,7 +116,7 @@ object LengthUnitHelper {
|
|||
|
||||
|
||||
/* Coverts meters per second to either km/h or mph */
|
||||
fun convertMetersPerSecond(metersPerSecond: Float, useImperial: Boolean = false): Double {
|
||||
fun convertMetersPerSecond(metersPerSecond: Double, useImperial: Boolean = false): Double {
|
||||
if (useImperial) {
|
||||
// mph
|
||||
return metersPerSecond * 2.2369362920544
|
||||
|
|
|
@ -51,14 +51,6 @@ object LocationHelper {
|
|||
return defaultLocation
|
||||
}
|
||||
|
||||
|
||||
/* Checks if a location is older than one minute */
|
||||
fun isOldLocation(location: Location): Boolean {
|
||||
// check how many milliseconds the given location is old
|
||||
return GregorianCalendar.getInstance().time.time - location.time > Keys.SIGNIFICANT_TIME_DIFFERENCE
|
||||
}
|
||||
|
||||
|
||||
/* Tries to return the last location that the system has stored */
|
||||
fun getLastKnownLocation(context: Context): Location {
|
||||
// get last location that Trackbook has stored
|
||||
|
@ -117,7 +109,6 @@ object LocationHelper {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/* Checks if GPS location provider is available and enabled */
|
||||
fun isGpsEnabled(locationManager: LocationManager): Boolean {
|
||||
if (locationManager.allProviders.contains(LocationManager.GPS_PROVIDER)) {
|
||||
|
@ -156,27 +147,6 @@ object LocationHelper {
|
|||
return isAccurate
|
||||
}
|
||||
|
||||
|
||||
/* Checks if the first location of track is plausible */
|
||||
fun isFirstLocationPlausible(secondLocation: Location, track: Track): Boolean {
|
||||
// speed in km/h
|
||||
val speed: Double = calculateSpeed(firstLocation = track.wayPoints[0].toLocation(), secondLocation = secondLocation, firstTimestamp = track.recordingStart.time, secondTimestamp = GregorianCalendar.getInstance().time.time)
|
||||
// plausible = speed under 250 km/h
|
||||
return speed < Keys.IMPLAUSIBLE_TRACK_START_SPEED
|
||||
}
|
||||
|
||||
|
||||
/* Calculates speed */
|
||||
private fun calculateSpeed(firstLocation: Location, secondLocation: Location, firstTimestamp: Long, secondTimestamp: Long, useImperial: Boolean = false): Double {
|
||||
// time difference in seconds
|
||||
val timeDifference: Long = (secondTimestamp - firstTimestamp) / 1000L
|
||||
// distance in meters
|
||||
val distance: Float = calculateDistance(firstLocation, secondLocation)
|
||||
// speed in either km/h (default) or mph
|
||||
return LengthUnitHelper.convertMetersPerSecond(distance / timeDifference, useImperial)
|
||||
}
|
||||
|
||||
|
||||
/* Checks if given location is different enough compared to previous location */
|
||||
fun isDifferentEnough(previousLocation: Location?, location: Location, omitRests: Boolean): Boolean {
|
||||
// check if previous location is (not) available
|
||||
|
@ -216,51 +186,6 @@ object LocationHelper {
|
|||
return distance
|
||||
}
|
||||
|
||||
|
||||
/* Calculate elevation differences */
|
||||
fun calculateElevationDifferencesOld(previousLocation: Location?, location: Location, track: Track): Pair<Double, Double> {
|
||||
// store current values
|
||||
var positiveElevation: Double = track.positiveElevation
|
||||
var negativeElevation: Double = track.negativeElevation
|
||||
if (previousLocation != null) {
|
||||
// factor is bigger than 1 if the time stamp difference is larger than the movement recording interval
|
||||
val timeDifferenceFactor: Long = (location.time - previousLocation.time) / Keys.ADD_WAYPOINT_TO_TRACK_INTERVAL
|
||||
// get elevation difference and sum it up
|
||||
val altitudeDifference: Double = location.altitude - previousLocation.altitude
|
||||
if (altitudeDifference > 0 && altitudeDifference < Keys.ALTITUDE_MEASUREMENT_ERROR_THRESHOLD * timeDifferenceFactor && location.altitude != Keys.DEFAULT_ALTITUDE) {
|
||||
positiveElevation = track.positiveElevation + altitudeDifference // upwards movement
|
||||
}
|
||||
if (altitudeDifference < 0 && altitudeDifference > -Keys.ALTITUDE_MEASUREMENT_ERROR_THRESHOLD * timeDifferenceFactor && location.altitude != Keys.DEFAULT_ALTITUDE) {
|
||||
negativeElevation = track.negativeElevation + altitudeDifference // downwards movement
|
||||
}
|
||||
}
|
||||
return Pair(positiveElevation, negativeElevation)
|
||||
}
|
||||
|
||||
|
||||
/* Calculate elevation differences */
|
||||
fun calculateElevationDifferences(currentAltitude: Double, previousAltitude: Double, track: Track): Track {
|
||||
if (currentAltitude != Keys.DEFAULT_ALTITUDE && previousAltitude != Keys.DEFAULT_ALTITUDE) {
|
||||
val altitudeDifference: Double = currentAltitude - previousAltitude
|
||||
if (altitudeDifference > 0) {
|
||||
track.positiveElevation += altitudeDifference // upwards movement
|
||||
}
|
||||
if (altitudeDifference < 0) {
|
||||
track.negativeElevation += altitudeDifference // downwards movement
|
||||
}
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
|
||||
/* Checks if given location is a stop over */
|
||||
fun isStopOver(previousLocation: Location?, location: Location): Boolean {
|
||||
if (previousLocation == null) return false
|
||||
// check how many milliseconds the given locations are apart
|
||||
return location.time - previousLocation.time > Keys.STOP_OVER_THRESHOLD
|
||||
}
|
||||
|
||||
|
||||
/* Get number of satellites from Location extras */
|
||||
fun getNumberOfSatellites(location: Location): Int {
|
||||
val numberOfSatellites: Int
|
||||
|
|
|
@ -36,7 +36,7 @@ import org.osmdroid.views.overlay.simplefastpoint.SimplePointTheme
|
|||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.core.WayPoint
|
||||
import org.y20k.trackbook.core.Trkpt
|
||||
import java.text.DecimalFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
@ -58,10 +58,10 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
|
|||
|
||||
|
||||
/* Creates icon overlay for current position (used in MapFragment) */
|
||||
fun createMyLocationOverlay(context: Context, location: Location, trackingState: Int): ItemizedIconOverlay<OverlayItem> {
|
||||
|
||||
fun createMyLocationOverlay(context: Context, location: Location, trackingState: Int): ItemizedIconOverlay<OverlayItem>
|
||||
{
|
||||
val overlayItems: ArrayList<OverlayItem> = ArrayList<OverlayItem>()
|
||||
val locationIsOld:Boolean = LocationHelper.isOldLocation(location)
|
||||
val locationIsOld: Boolean = !(LocationHelper.isRecentEnough(location))
|
||||
|
||||
// create marker
|
||||
val newMarker: Drawable
|
||||
|
@ -83,7 +83,24 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
|
|||
}
|
||||
|
||||
// add marker to list of overlay items
|
||||
val overlayItem: OverlayItem = createOverlayItem(context, location.latitude, location.longitude, location.accuracy, location.provider, location.time)
|
||||
val overlayItem: OverlayItem = createOverlayItem(context, location.latitude, location.longitude, location.accuracy, location.provider.toString(), location.time)
|
||||
overlayItem.setMarker(newMarker)
|
||||
overlayItems.add(overlayItem)
|
||||
|
||||
// create and return overlay for current position
|
||||
return createOverlay(context, overlayItems, enableStarring = false)
|
||||
}
|
||||
|
||||
/* Creates icon overlay for current position (used in MapFragment) */
|
||||
fun createHomepointOverlay(context: Context, location: Location): ItemizedIconOverlay<OverlayItem>
|
||||
{
|
||||
val overlayItems: ArrayList<OverlayItem> = ArrayList<OverlayItem>()
|
||||
|
||||
// create marker
|
||||
val newMarker: Drawable = ContextCompat.getDrawable(context, R.drawable.ic_homepoint_24dp)!!
|
||||
|
||||
// add marker to list of overlay items
|
||||
val overlayItem: OverlayItem = createOverlayItem(context, location.latitude, location.longitude, location.accuracy, location.provider.toString(), location.time)
|
||||
overlayItem.setMarker(newMarker)
|
||||
overlayItems.add(overlayItem)
|
||||
|
||||
|
@ -93,13 +110,14 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
|
|||
|
||||
|
||||
/* Creates icon overlay for track */
|
||||
fun createTrackOverlay(context: Context, track: Track, trackingState: Int): SimpleFastPointOverlay {
|
||||
fun createTrackOverlay(context: Context, track: Track, trackingState: Int): SimpleFastPointOverlay
|
||||
{
|
||||
// get marker color
|
||||
val color = if (trackingState == Keys.STATE_TRACKING_ACTIVE) context.getColor(R.color.default_red)
|
||||
else context.getColor(R.color.default_blue)
|
||||
// gather points for overlay
|
||||
val points: MutableList<IGeoPoint> = mutableListOf()
|
||||
track.wayPoints.forEach { wayPoint ->
|
||||
track.trkpts.forEach { wayPoint ->
|
||||
val label: String = "${context.getString(R.string.marker_description_time)}: ${SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(wayPoint.time)} | ${context.getString(R.string.marker_description_accuracy)}: ${DecimalFormat("#0.00").format(wayPoint.accuracy)} (${wayPoint.provider})"
|
||||
// only add normal points
|
||||
if (!wayPoint.starred && !wayPoint.isStopOver) {
|
||||
|
@ -133,45 +151,60 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
|
|||
return overlay
|
||||
}
|
||||
|
||||
|
||||
/* Creates overlay containing start, stop, stopover and starred markers for track */
|
||||
fun createSpecialMakersTrackOverlay(context: Context, track: Track, trackingState: Int, displayStartEndMarker: Boolean = false): ItemizedIconOverlay<OverlayItem> {
|
||||
fun createSpecialMakersTrackOverlay(context: Context, track: Track, trackingState: Int, displayStartEndMarker: Boolean = false): ItemizedIconOverlay<OverlayItem>
|
||||
{
|
||||
val overlayItems: ArrayList<OverlayItem> = ArrayList<OverlayItem>()
|
||||
val trackingActive: Boolean = trackingState == Keys.STATE_TRACKING_ACTIVE
|
||||
val maxIndex: Int = track.wayPoints.size - 1
|
||||
val maxIndex: Int = track.trkpts.size - 1
|
||||
|
||||
track.wayPoints.forEachIndexed { index: Int, wayPoint: WayPoint ->
|
||||
track.trkpts.forEachIndexed { index: Int, trkpt: Trkpt ->
|
||||
var overlayItem: OverlayItem? = null
|
||||
if (!trackingActive && index == 0 && displayStartEndMarker && wayPoint.starred) {
|
||||
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time)
|
||||
if (!trackingActive && index == 0 && displayStartEndMarker && trkpt.starred)
|
||||
{
|
||||
overlayItem = createOverlayItem(context, trkpt.latitude, trkpt.longitude, trkpt.accuracy, trkpt.provider, trkpt.time.time)
|
||||
overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_start_starred_blue_48dp)!!)
|
||||
} else if (!trackingActive && index == 0 && displayStartEndMarker && !wayPoint.starred) {
|
||||
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time)
|
||||
}
|
||||
else if (!trackingActive && index == 0 && displayStartEndMarker && !trkpt.starred)
|
||||
{
|
||||
overlayItem = createOverlayItem(context, trkpt.latitude, trkpt.longitude, trkpt.accuracy, trkpt.provider, trkpt.time.time)
|
||||
overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_start_blue_48dp)!!)
|
||||
} else if (!trackingActive && index == maxIndex && displayStartEndMarker && wayPoint.starred) {
|
||||
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time)
|
||||
}
|
||||
else if (!trackingActive && index == maxIndex && displayStartEndMarker && trkpt.starred)
|
||||
{
|
||||
overlayItem = createOverlayItem(context, trkpt.latitude, trkpt.longitude, trkpt.accuracy, trkpt.provider, trkpt.time.time)
|
||||
overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_end_starred_blue_48dp)!!)
|
||||
} else if (!trackingActive && index == maxIndex && displayStartEndMarker && !wayPoint.starred) {
|
||||
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time)
|
||||
}
|
||||
else if (!trackingActive && index == maxIndex && displayStartEndMarker && !trkpt.starred)
|
||||
{
|
||||
overlayItem = createOverlayItem(context, trkpt.latitude, trkpt.longitude, trkpt.accuracy, trkpt.provider, trkpt.time.time)
|
||||
overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_end_blue_48dp)!!)
|
||||
} else if (!trackingActive && wayPoint.starred) {
|
||||
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time)
|
||||
}
|
||||
else if (!trackingActive && trkpt.starred)
|
||||
{
|
||||
overlayItem = createOverlayItem(context, trkpt.latitude, trkpt.longitude, trkpt.accuracy, trkpt.provider, trkpt.time.time)
|
||||
overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_star_blue_24dp)!!)
|
||||
} else if (trackingActive && wayPoint.starred) {
|
||||
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time)
|
||||
}
|
||||
else if (trackingActive && trkpt.starred)
|
||||
{
|
||||
overlayItem = createOverlayItem(context, trkpt.latitude, trkpt.longitude, trkpt.accuracy, trkpt.provider, trkpt.time.time)
|
||||
overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_star_red_24dp)!!)
|
||||
} else if (wayPoint.isStopOver) {
|
||||
overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time)
|
||||
}
|
||||
else if (trkpt.isStopOver)
|
||||
{
|
||||
overlayItem = createOverlayItem(context, trkpt.latitude, trkpt.longitude, trkpt.accuracy, trkpt.provider, trkpt.time.time)
|
||||
overlayItem.setMarker(ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!!)
|
||||
}
|
||||
// add overlay item, if it was created
|
||||
if (overlayItem != null) overlayItems.add(overlayItem)
|
||||
if (overlayItem != null)
|
||||
{
|
||||
overlayItems.add(overlayItem)
|
||||
}
|
||||
}
|
||||
// create and return overlay for current position
|
||||
return createOverlay(context, overlayItems, enableStarring = true)
|
||||
}
|
||||
|
||||
|
||||
/* 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)}"
|
||||
|
@ -183,7 +216,6 @@ class MapOverlayHelper (private var markerListener: MarkerListener) {
|
|||
return item
|
||||
}
|
||||
|
||||
|
||||
/* Creates an overlay */
|
||||
private fun createOverlay(context: Context, overlayItems: ArrayList<OverlayItem>, enableStarring: Boolean): ItemizedIconOverlay<OverlayItem> {
|
||||
return ItemizedIconOverlay<OverlayItem>(context, overlayItems,
|
||||
|
|
|
@ -45,7 +45,7 @@ class NotificationHelper(private val trackerService: TrackerService) {
|
|||
|
||||
|
||||
/* Creates notification */
|
||||
fun createNotification(trackingState: Int, trackLength: Float, duration: Long, useImperial: Boolean): Notification {
|
||||
fun createNotification(trackingState: Int, timestamp: String): Notification {
|
||||
|
||||
// create notification channel if necessary
|
||||
if (shouldCreateNotificationChannel()) {
|
||||
|
@ -56,7 +56,7 @@ class NotificationHelper(private val trackerService: TrackerService) {
|
|||
val builder = NotificationCompat.Builder(trackerService, Keys.NOTIFICATION_CHANNEL_RECORDING)
|
||||
builder.setContentIntent(showActionPendingIntent)
|
||||
builder.setSmallIcon(R.drawable.ic_notification_icon_small_24dp)
|
||||
builder.setContentText(getContentString(trackerService, duration, trackLength, useImperial))
|
||||
builder.setContentText(timestamp)
|
||||
|
||||
// add icon and actions for stop, resume and show
|
||||
when (trackingState) {
|
||||
|
@ -77,22 +77,13 @@ class NotificationHelper(private val trackerService: TrackerService) {
|
|||
|
||||
}
|
||||
|
||||
|
||||
/* Build context text for notification builder */
|
||||
private fun getContentString(context: Context, duration: Long, trackLength: Float, useImperial: Boolean): String {
|
||||
return "${LengthUnitHelper.convertDistanceToString(trackLength, useImperial)} • ${DateTimeHelper.convertToReadableTime(context, duration)}"
|
||||
}
|
||||
|
||||
|
||||
/* Checks if notification channel should be created */
|
||||
private fun shouldCreateNotificationChannel() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !nowPlayingChannelExists()
|
||||
|
||||
|
||||
/* Checks if notification channel exists */
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun nowPlayingChannelExists() = notificationManager.getNotificationChannel(Keys.NOTIFICATION_CHANNEL_RECORDING) != null
|
||||
|
||||
|
||||
/* Create a notification channel */
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel() {
|
||||
|
@ -103,20 +94,24 @@ class NotificationHelper(private val trackerService: TrackerService) {
|
|||
notificationManager.createNotificationChannel(notificationChannel)
|
||||
}
|
||||
|
||||
|
||||
/* Notification pending intents */
|
||||
private val stopActionPendingIntent = PendingIntent.getService(
|
||||
trackerService,14,
|
||||
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_STOP),PendingIntent.FLAG_IMMUTABLE)
|
||||
trackerService,
|
||||
14,
|
||||
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_STOP),
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
private val resumeActionPendingIntent = PendingIntent.getService(
|
||||
trackerService, 16,
|
||||
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_RESUME),PendingIntent.FLAG_IMMUTABLE)
|
||||
trackerService,
|
||||
16,
|
||||
Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_RESUME),
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
private val showActionPendingIntent: PendingIntent? = TaskStackBuilder.create(trackerService).run {
|
||||
addNextIntentWithParentStack(Intent(trackerService, MainActivity::class.java))
|
||||
getPendingIntent(10, PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
|
||||
/* Notification actions */
|
||||
private val stopAction = NotificationCompat.Action(
|
||||
R.drawable.ic_notification_action_stop_24dp,
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||
import android.content.SharedPreferences
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.y20k.trackbook.Keys
|
||||
|
@ -43,6 +44,13 @@ object PreferencesHelper {
|
|||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
}
|
||||
|
||||
fun load_device_id(): String
|
||||
{
|
||||
val v = sharedPreferences.getString(Keys.PREF_DEVICE_ID, random_int().toString()).toString();
|
||||
Log.i("VOUSSOIR", "Loaded device_id ${v}.")
|
||||
return v
|
||||
}
|
||||
|
||||
fun loadZoomLevel(): Double {
|
||||
return sharedPreferences.getDouble(Keys.PREF_MAP_ZOOM_LEVEL, Keys.DEFAULT_ZOOM_LEVEL)
|
||||
}
|
||||
|
@ -52,7 +60,7 @@ object PreferencesHelper {
|
|||
}
|
||||
|
||||
fun loadTrackingState(): Int {
|
||||
return sharedPreferences.getInt(Keys.PREF_TRACKING_STATE, Keys.STATE_TRACKING_NOT_STARTED)
|
||||
return sharedPreferences.getInt(Keys.PREF_TRACKING_STATE, Keys.STATE_TRACKING_STOPPED)
|
||||
}
|
||||
|
||||
fun saveTrackingState(trackingState: Int) {
|
||||
|
@ -71,14 +79,10 @@ object PreferencesHelper {
|
|||
return sharedPreferences.getBoolean(Keys.PREF_OMIT_RESTS, true)
|
||||
}
|
||||
|
||||
fun loadAutoExportInterval(): Int {
|
||||
return sharedPreferences.getInt(Keys.PREF_AUTO_EXPORT_INTERVAL, Keys.DEFAULT_AUTO_EXPORT_INTERVAL)
|
||||
fun loadCommitInterval(): Int {
|
||||
return sharedPreferences.getInt(Keys.PREF_COMMIT_INTERVAL, Keys.COMMIT_INTERVAL)
|
||||
}
|
||||
|
||||
// fun loadAltitudeSmoothingValue(): Int {
|
||||
// return sharedPreferences.getInt(Keys.PREF_ALTITUDE_SMOOTHING_VALUE, Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE)
|
||||
// }
|
||||
|
||||
/* Loads the state of a map */
|
||||
fun loadCurrentBestLocation(): Location {
|
||||
val provider: String = sharedPreferences.getString(Keys.PREF_CURRENT_BEST_LOCATION_PROVIDER, LocationManager.NETWORK_PROVIDER) ?: LocationManager.NETWORK_PROVIDER
|
||||
|
|
|
@ -18,7 +18,6 @@ package org.y20k.trackbook.helpers
|
|||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import java.util.*
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.core.Track
|
||||
|
||||
|
@ -35,7 +34,7 @@ object TrackHelper {
|
|||
/* Toggles starred flag for given position */
|
||||
fun toggle_waypoint_starred(context: Context, track: Track, latitude: Double, longitude: Double)
|
||||
{
|
||||
track.wayPoints.forEach { waypoint ->
|
||||
track.trkpts.forEach { waypoint ->
|
||||
if (waypoint.latitude == latitude && waypoint.longitude == longitude) {
|
||||
waypoint.starred = !waypoint.starred
|
||||
when (waypoint.starred) {
|
||||
|
|
22
app/src/main/java/org/y20k/trackbook/helpers/functions.kt
Normal file
22
app/src/main/java/org/y20k/trackbook/helpers/functions.kt
Normal file
|
@ -0,0 +1,22 @@
|
|||
package org.y20k.trackbook.helpers
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.random.Random
|
||||
|
||||
val iso8601_format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US)
|
||||
|
||||
fun iso8601(datetime: Date): String
|
||||
{
|
||||
return iso8601_format.format(datetime)
|
||||
}
|
||||
|
||||
fun random_long(): Long
|
||||
{
|
||||
return (Random.nextBits(31).toLong() shl 32) + Random.nextBits(32)
|
||||
}
|
||||
|
||||
fun random_int(): Int
|
||||
{
|
||||
return Random.nextBits(31)
|
||||
}
|
|
@ -19,6 +19,7 @@ package org.y20k.trackbook.tracklist
|
|||
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -26,49 +27,72 @@ import android.view.ViewGroup
|
|||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.core.Database
|
||||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.core.Tracklist
|
||||
import org.y20k.trackbook.core.load_tracklist
|
||||
import org.y20k.trackbook.helpers.*
|
||||
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
/*
|
||||
* TracklistAdapter class
|
||||
*/
|
||||
class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(TracklistAdapter::class.java)
|
||||
|
||||
|
||||
class TracklistAdapter(val fragment: Fragment, val database: Database) : RecyclerView.Adapter<RecyclerView.ViewHolder>()
|
||||
{
|
||||
/* Main class variables */
|
||||
private val context: Context = fragment.activity as Context
|
||||
private lateinit var tracklistListener: TracklistAdapterListener
|
||||
private var useImperial: Boolean = PreferencesHelper.loadUseImperialUnits()
|
||||
private var tracklist: Tracklist = Tracklist()
|
||||
|
||||
val tracks: ArrayList<Track> = ArrayList<Track>()
|
||||
|
||||
/* Listener Interface */
|
||||
interface TracklistAdapterListener {
|
||||
interface TracklistAdapterListener
|
||||
{
|
||||
fun onTrackElementTapped(track: Track) { }
|
||||
}
|
||||
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView)
|
||||
{
|
||||
tracklistListener = fragment as TracklistAdapterListener
|
||||
tracklist = load_tracklist(context)
|
||||
tracks.clear()
|
||||
if (! database.ready)
|
||||
{
|
||||
return
|
||||
}
|
||||
val cursor: Cursor = database.connection.query(
|
||||
"trkpt",
|
||||
arrayOf("distinct strftime('%Y-%m-%d', time)", "device_id"),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"time DESC",
|
||||
null,
|
||||
)
|
||||
try
|
||||
{
|
||||
val df: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US)
|
||||
while (cursor.moveToNext())
|
||||
{
|
||||
val start_time: Date? = df.parse(cursor.getString(0) + "T00:00:00.000")
|
||||
val stop_time: Date? = df.parse(cursor.getString(0) + "T23:59:59.999")
|
||||
Log.i("VOUSSOIR", "TracklistAdapter prep track ${cursor.getString(0)}")
|
||||
if (start_time != null && stop_time != null)
|
||||
{
|
||||
tracks.add(Track(database=database, device_id=cursor.getString(1), start_time=start_time, stop_time=stop_time))
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -81,14 +105,16 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
|
|||
|
||||
|
||||
/* Overrides getItemViewType */
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
override fun getItemViewType(position: Int): Int
|
||||
{
|
||||
return Keys.VIEW_TYPE_TRACK
|
||||
}
|
||||
|
||||
|
||||
/* Overrides getItemCount from RecyclerView.Adapter */
|
||||
override fun getItemCount(): Int {
|
||||
return tracklist.tracks.size
|
||||
override fun getItemCount(): Int
|
||||
{
|
||||
return tracks.size
|
||||
}
|
||||
|
||||
|
||||
|
@ -97,17 +123,10 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
|
|||
{
|
||||
val positionInTracklist: Int = position
|
||||
val elementTrackViewHolder: ElementTrackViewHolder = holder as ElementTrackViewHolder
|
||||
elementTrackViewHolder.trackNameView.text = tracklist.tracks[positionInTracklist].name
|
||||
elementTrackViewHolder.trackNameView.text = getTrackName(positionInTracklist)
|
||||
elementTrackViewHolder.trackDataView.text = createTrackDataString(positionInTracklist)
|
||||
when (tracklist.tracks[positionInTracklist].starred) {
|
||||
true -> elementTrackViewHolder.starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_filled_24dp))
|
||||
false -> elementTrackViewHolder.starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_outline_24dp))
|
||||
}
|
||||
elementTrackViewHolder.trackElement.setOnClickListener {
|
||||
tracklistListener.onTrackElementTapped(tracklist.tracks[positionInTracklist])
|
||||
}
|
||||
elementTrackViewHolder.starButton.setOnClickListener {
|
||||
toggleStarred(it, positionInTracklist)
|
||||
tracklistListener.onTrackElementTapped(tracks[positionInTracklist])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,16 +134,16 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
|
|||
/* Get track name for given position */
|
||||
fun getTrackName(positionInRecyclerView: Int): String
|
||||
{
|
||||
return tracklist.tracks[positionInRecyclerView].name
|
||||
return SimpleDateFormat("yyyy-MM-dd", Locale.US).format(tracks[positionInRecyclerView].start_time)
|
||||
}
|
||||
|
||||
fun delete_track_at_position(context: Context, index: Int)
|
||||
{
|
||||
val track = tracklist.tracks[index]
|
||||
track.delete(context)
|
||||
tracklist.tracks.remove(track)
|
||||
notifyItemRemoved(index)
|
||||
notifyItemRangeChanged(index, this.itemCount);
|
||||
// val track = tracklist.tracks[index]
|
||||
// track.delete()
|
||||
// tracklist.tracks.remove(track)
|
||||
// notifyItemRemoved(index)
|
||||
// notifyItemRangeChanged(index, this.itemCount);
|
||||
}
|
||||
|
||||
suspend fun delete_track_at_position_suspended(context: Context, position: Int)
|
||||
|
@ -136,81 +155,40 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
|
|||
|
||||
fun delete_track_by_id(context: Context, trackId: Long)
|
||||
{
|
||||
val index: Int = tracklist.tracks.indexOfFirst {it.id == trackId}
|
||||
if (index == -1) {
|
||||
return
|
||||
}
|
||||
tracklist.tracks[index].delete(context)
|
||||
tracklist.tracks.removeAt(index)
|
||||
notifyItemRemoved(index)
|
||||
notifyItemRangeChanged(index, this.itemCount);
|
||||
// val index: Int = tracklist.tracks.indexOfFirst {it.id == trackId}
|
||||
// if (index == -1) {
|
||||
// return
|
||||
// }
|
||||
// tracklist.tracks[index].delete()
|
||||
// tracklist.tracks.removeAt(index)
|
||||
// notifyItemRemoved(index)
|
||||
// notifyItemRangeChanged(index, this.itemCount);
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
return tracklist.tracks.size == 0
|
||||
}
|
||||
|
||||
/* Toggles the starred state of tracklist element - and saves tracklist */
|
||||
private fun toggleStarred(view: View, position: Int) {
|
||||
val starButton: ImageButton = view as ImageButton
|
||||
if (tracklist.tracks[position].starred)
|
||||
{
|
||||
starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_outline_24dp))
|
||||
tracklist.tracks[position].starred = false
|
||||
}
|
||||
else
|
||||
{
|
||||
starButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_star_filled_24dp))
|
||||
tracklist.tracks[position].starred = true
|
||||
}
|
||||
tracklist.tracks[position].save_json(context)
|
||||
fun isEmpty(): Boolean
|
||||
{
|
||||
return tracks.size == 0
|
||||
}
|
||||
|
||||
/* Creates the track data string */
|
||||
private fun createTrackDataString(position: Int): String {
|
||||
val track: Track = tracklist.tracks[position]
|
||||
val track_duration_string = DateTimeHelper.convertToReadableTime(context, track.duration)
|
||||
val trackDataString: String
|
||||
when (track.name == track.dateString) {
|
||||
// CASE: no individual name set - exclude date
|
||||
true -> trackDataString = "${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)} • ${track_duration_string}"
|
||||
// CASE: no individual name set - include date
|
||||
false -> trackDataString = "${track.dateString} • ${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)} • ${track_duration_string}"
|
||||
}
|
||||
return trackDataString
|
||||
private fun createTrackDataString(position: Int): String
|
||||
{
|
||||
val track: Track = tracks[position]
|
||||
return "device: " + track.device_id
|
||||
// val track_duration_string = DateTimeHelper.convertToReadableTime(context, track.duration)
|
||||
// val trackDataString: String
|
||||
// if (track.name == track.dateString)
|
||||
// {
|
||||
// trackDataString = "${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)} • ${track_duration_string}"
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// trackDataString = "${track.dateString} • ${LengthUnitHelper.convertDistanceToString(track.distance, useImperial)} • ${track_duration_string}"
|
||||
// }
|
||||
// return trackDataString
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Inner class: DiffUtil.Callback that determines changes in data - improves list performance
|
||||
*/
|
||||
private inner class DiffCallback(val oldList: Tracklist, val newList: Tracklist): DiffUtil.Callback() {
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList.tracks[oldItemPosition]
|
||||
val newItem = newList.tracks[newItemPosition]
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun getOldListSize(): Int {
|
||||
return oldList.tracks.size
|
||||
}
|
||||
|
||||
override fun getNewListSize(): Int {
|
||||
return newList.tracks.size
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList.tracks[oldItemPosition]
|
||||
val newItem = newList.tracks[newItemPosition]
|
||||
return (oldItem.id == newItem.id) && (oldItem.distance == newItem.distance)
|
||||
}
|
||||
}
|
||||
/*
|
||||
* End of inner class
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* Inner class: ViewHolder for a track element
|
||||
*/
|
||||
|
@ -218,8 +196,6 @@ class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter<Re
|
|||
val trackElement: ConstraintLayout = elementTrackLayout.findViewById(R.id.track_element)
|
||||
val trackNameView: TextView = elementTrackLayout.findViewById(R.id.track_name)
|
||||
val trackDataView: TextView = elementTrackLayout.findViewById(R.id.track_data)
|
||||
val starButton: ImageButton = elementTrackLayout.findViewById(R.id.star_button)
|
||||
|
||||
}
|
||||
/*
|
||||
* End of inner class
|
||||
|
|
|
@ -49,6 +49,7 @@ import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider
|
|||
import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay
|
||||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.Trackbook
|
||||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.helpers.*
|
||||
|
||||
|
@ -66,17 +67,11 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
|
|||
var userInteraction: Boolean = false
|
||||
val currentLocationButton: FloatingActionButton
|
||||
val mainButton: ExtendedFloatingActionButton
|
||||
val saveButton: FloatingActionButton
|
||||
val clearButton: FloatingActionButton
|
||||
private val additionalButtons: Group
|
||||
private val mapView: MapView
|
||||
private val homepoint_overlays: ArrayList<ItemizedIconOverlay<OverlayItem>> = ArrayList()
|
||||
private var currentPositionOverlay: ItemizedIconOverlay<OverlayItem>
|
||||
private var currentTrackOverlay: SimpleFastPointOverlay?
|
||||
private var currentTrackSpecialMarkerOverlay: ItemizedIconOverlay<OverlayItem>?
|
||||
private val liveStatisticsDistanceView: MaterialTextView
|
||||
private val liveStatisticsDistanceOutlineView: MaterialTextView
|
||||
private val liveStatisticsDurationView: MaterialTextView
|
||||
private val liveStatisticsDurationOutlineView: MaterialTextView
|
||||
private val useImperial: Boolean = PreferencesHelper.loadUseImperialUnits()
|
||||
private var locationErrorBar: Snackbar
|
||||
private var controller: IMapController
|
||||
|
@ -90,13 +85,6 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
|
|||
mapView = rootView.findViewById(R.id.map)
|
||||
currentLocationButton = rootView.findViewById(R.id.location_button)
|
||||
mainButton = rootView.findViewById(R.id.main_button)
|
||||
additionalButtons = rootView.findViewById(R.id.additional_buttons)
|
||||
saveButton = rootView.findViewById(R.id.button_save)
|
||||
clearButton = rootView.findViewById(R.id.button_clear)
|
||||
liveStatisticsDistanceView = rootView.findViewById(R.id.live_statistics_distance)
|
||||
liveStatisticsDistanceOutlineView = rootView.findViewById(R.id.live_statistics_distance_outline)
|
||||
liveStatisticsDurationView = rootView.findViewById(R.id.live_statistics_duration)
|
||||
liveStatisticsDurationOutlineView = rootView.findViewById(R.id.live_statistics_duration_outline)
|
||||
locationErrorBar = Snackbar.make(mapView, String(), Snackbar.LENGTH_INDEFINITE)
|
||||
|
||||
// basic map setup
|
||||
|
@ -124,11 +112,8 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
|
|||
compassOverlay.setCompassCenter((screen_width / densityScalingFactor) - 36f, 36f)
|
||||
mapView.overlays.add(compassOverlay)
|
||||
|
||||
// position the live statistics
|
||||
(liveStatisticsDistanceView.layoutParams as ConstraintLayout.LayoutParams).apply {
|
||||
// topMargin = (12 * densityScalingFactor).toInt() + statusBarHeight // TODO uncomment when transparent status bar is re-implemented
|
||||
topMargin = (12 * densityScalingFactor).toInt()
|
||||
}
|
||||
val app: Trackbook = (context.applicationContext as Trackbook)
|
||||
app.homepoint_generator().forEach { homepoint -> mapView.overlays.add(MapOverlayHelper(markerListener).createHomepointOverlay(context, homepoint.location))}
|
||||
|
||||
// add my location overlay
|
||||
currentPositionOverlay = MapOverlayHelper(markerListener).createMyLocationOverlay(context, startLocation, trackingState)
|
||||
|
@ -178,7 +163,7 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
|
|||
|
||||
|
||||
/* Mark current position on map */
|
||||
fun markCurrentPosition(location: Location, trackingState: Int = Keys.STATE_TRACKING_NOT_STARTED) {
|
||||
fun markCurrentPosition(location: Location, trackingState: Int = Keys.STATE_TRACKING_STOPPED) {
|
||||
mapView.overlays.remove(currentPositionOverlay)
|
||||
currentPositionOverlay = MapOverlayHelper(markerListener).createMyLocationOverlay(context, location, trackingState)
|
||||
mapView.overlays.add(currentPositionOverlay)
|
||||
|
@ -193,7 +178,7 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
|
|||
if (currentTrackSpecialMarkerOverlay != null) {
|
||||
mapView.overlays.remove(currentTrackSpecialMarkerOverlay)
|
||||
}
|
||||
if (track.wayPoints.isNotEmpty()) {
|
||||
if (track.trkpts.isNotEmpty()) {
|
||||
val mapOverlayHelper: MapOverlayHelper = MapOverlayHelper(markerListener)
|
||||
currentTrackOverlay = mapOverlayHelper.createTrackOverlay(context, track, trackingState)
|
||||
currentTrackSpecialMarkerOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, trackingState)
|
||||
|
@ -202,59 +187,20 @@ data class MapFragmentLayoutHolder(private var context: Context, private var mar
|
|||
}
|
||||
}
|
||||
|
||||
/* Update live statics */
|
||||
fun updateLiveStatics(distance: Float, duration: Long, trackingState: Int) {
|
||||
// toggle visibility
|
||||
if (trackingState == Keys.STATE_TRACKING_NOT_STARTED)
|
||||
{
|
||||
liveStatisticsDistanceView.isGone = true
|
||||
liveStatisticsDurationView.isGone = true
|
||||
liveStatisticsDistanceOutlineView.isGone = true
|
||||
liveStatisticsDurationOutlineView.isGone = true
|
||||
}
|
||||
else
|
||||
{
|
||||
liveStatisticsDistanceView.isVisible = true
|
||||
liveStatisticsDurationView.isVisible = true
|
||||
liveStatisticsDistanceOutlineView.isVisible = true
|
||||
liveStatisticsDurationOutlineView.isVisible = true
|
||||
// update distance and duration (and add outline)
|
||||
val distanceString: String = LengthUnitHelper.convertDistanceToString(distance, useImperial)
|
||||
liveStatisticsDistanceView.text = distanceString
|
||||
liveStatisticsDistanceOutlineView.text = distanceString
|
||||
liveStatisticsDistanceOutlineView.paint.strokeWidth = 5f
|
||||
liveStatisticsDistanceOutlineView.paint.style = Paint.Style.STROKE
|
||||
val durationString: String = DateTimeHelper.convertToReadableTime(context, duration, compactFormat = true)
|
||||
liveStatisticsDurationView.text = durationString
|
||||
liveStatisticsDurationOutlineView.text = durationString
|
||||
liveStatisticsDurationOutlineView.paint.strokeWidth = 5f
|
||||
liveStatisticsDurationOutlineView.paint.style = Paint.Style.STROKE
|
||||
}
|
||||
}
|
||||
|
||||
/* Toggles state of main button and additional buttons (save & resume) */
|
||||
fun updateMainButton(trackingState: Int)
|
||||
{
|
||||
when (trackingState) {
|
||||
Keys.STATE_TRACKING_NOT_STARTED -> {
|
||||
Keys.STATE_TRACKING_STOPPED -> {
|
||||
mainButton.setIconResource(R.drawable.ic_fiber_manual_record_inactive_24dp)
|
||||
mainButton.text = context.getString(R.string.button_start)
|
||||
mainButton.contentDescription = context.getString(R.string.descr_button_start)
|
||||
additionalButtons.isGone = true
|
||||
currentLocationButton.isVisible = true
|
||||
}
|
||||
Keys.STATE_TRACKING_ACTIVE -> {
|
||||
mainButton.setIconResource(R.drawable.ic_pause_24dp)
|
||||
mainButton.setIconResource(R.drawable.ic_fiber_manual_stop_24dp)
|
||||
mainButton.text = context.getString(R.string.button_pause)
|
||||
mainButton.contentDescription = context.getString(R.string.descr_button_start)
|
||||
additionalButtons.isGone = true
|
||||
currentLocationButton.isVisible = true
|
||||
}
|
||||
Keys.STATE_TRACKING_PAUSED -> {
|
||||
mainButton.setIconResource(R.drawable.ic_fiber_manual_record_inactive_24dp)
|
||||
mainButton.text = context.getString(R.string.button_resume)
|
||||
mainButton.contentDescription = context.getString(R.string.descr_button_resume)
|
||||
additionalButtons.isVisible = true
|
||||
mainButton.contentDescription = context.getString(R.string.descr_button_pause)
|
||||
currentLocationButton.isVisible = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,9 +30,6 @@ import androidx.core.view.isVisible
|
|||
import androidx.core.widget.NestedScrollView
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.osmdroid.api.IGeoPoint
|
||||
import org.osmdroid.api.IMapController
|
||||
import org.osmdroid.events.MapListener
|
||||
|
@ -50,6 +47,7 @@ import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay
|
|||
import org.y20k.trackbook.Keys
|
||||
import org.y20k.trackbook.R
|
||||
import org.y20k.trackbook.core.Track
|
||||
import org.y20k.trackbook.core.TrackStatistics
|
||||
import org.y20k.trackbook.helpers.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
@ -58,12 +56,14 @@ import kotlin.math.roundToInt
|
|||
* TrackFragmentLayoutHolder class
|
||||
*/
|
||||
//data class TrackFragmentLayoutHolder(private var context: Context, private var markerListener: MapOverlayHelper.MarkerListener, private var inflater: LayoutInflater, private var statusBarHeight: Int, private var container: ViewGroup?, var track: Track): MapListener { TODO REMOVE
|
||||
data class TrackFragmentLayoutHolder(private var context: Context, private var markerListener: MapOverlayHelper.MarkerListener, private var inflater: LayoutInflater, private var container: ViewGroup?, var track: Track): MapListener {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = LogHelper.makeLogTag(TrackFragmentLayoutHolder::class.java)
|
||||
|
||||
|
||||
data class TrackFragmentLayoutHolder(
|
||||
private var context: Context,
|
||||
private var markerListener: MapOverlayHelper.MarkerListener,
|
||||
private var inflater: LayoutInflater,
|
||||
private var container: ViewGroup?,
|
||||
var track: Track
|
||||
): MapListener
|
||||
{
|
||||
/* Main class variables */
|
||||
val rootView: View
|
||||
val shareButton: ImageButton
|
||||
|
@ -78,7 +78,6 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
|
|||
private val statisticsSheetBehavior: BottomSheetBehavior<View>
|
||||
private val statisticsSheet: NestedScrollView
|
||||
private val statisticsView: View
|
||||
private val trackidView: MaterialTextView
|
||||
private val distanceView: MaterialTextView
|
||||
private val stepsTitleView: MaterialTextView
|
||||
private val stepsView: MaterialTextView
|
||||
|
@ -115,13 +114,12 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
|
|||
mapView.setTileSource(TileSourceFactory.MAPNIK)
|
||||
mapView.setMultiTouchControls(true)
|
||||
mapView.zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
|
||||
controller.setCenter(GeoPoint(track.latitude, track.longitude))
|
||||
controller.setCenter(GeoPoint(track.view_latitude, track.view_longitude))
|
||||
controller.setZoom(track.zoomLevel)
|
||||
|
||||
// get views for statistics sheet
|
||||
statisticsSheet = rootView.findViewById(R.id.statistics_sheet)
|
||||
statisticsView = rootView.findViewById(R.id.statistics_view)
|
||||
trackidView = rootView.findViewById(R.id.statistics_data_trackid)
|
||||
distanceView = rootView.findViewById(R.id.statistics_data_distance)
|
||||
stepsTitleView = rootView.findViewById(R.id.statistics_p_steps)
|
||||
stepsView = rootView.findViewById(R.id.statistics_data_steps)
|
||||
|
@ -156,9 +154,9 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
|
|||
|
||||
// create map overlay
|
||||
val mapOverlayHelper: MapOverlayHelper = MapOverlayHelper(markerListener)
|
||||
trackOverlay = mapOverlayHelper.createTrackOverlay(context, track, Keys.STATE_TRACKING_NOT_STARTED)
|
||||
trackSpecialMarkersOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, Keys.STATE_TRACKING_NOT_STARTED, displayStartEndMarker = true)
|
||||
if (track.wayPoints.isNotEmpty()) {
|
||||
trackOverlay = mapOverlayHelper.createTrackOverlay(context, track, Keys.STATE_TRACKING_STOPPED)
|
||||
trackSpecialMarkersOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, Keys.STATE_TRACKING_STOPPED, displayStartEndMarker = true)
|
||||
if (track.trkpts.isNotEmpty()) {
|
||||
mapView.overlays.add(trackSpecialMarkersOverlay)
|
||||
mapView.overlays.add(trackOverlay)
|
||||
}
|
||||
|
@ -172,77 +170,48 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
|
|||
|
||||
|
||||
/* Updates map overlay */
|
||||
fun updateTrackOverlay() {
|
||||
fun updateTrackOverlay()
|
||||
{
|
||||
if (trackOverlay != null) {
|
||||
mapView.overlays.remove(trackOverlay)
|
||||
}
|
||||
if (trackSpecialMarkersOverlay != null) {
|
||||
mapView.overlays.remove(trackSpecialMarkersOverlay)
|
||||
}
|
||||
if (track.wayPoints.isNotEmpty()) {
|
||||
if (track.trkpts.isNotEmpty()) {
|
||||
val mapOverlayHelper: MapOverlayHelper = MapOverlayHelper(markerListener)
|
||||
trackOverlay = mapOverlayHelper.createTrackOverlay(context, track, Keys.STATE_TRACKING_NOT_STARTED)
|
||||
trackSpecialMarkersOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, Keys.STATE_TRACKING_NOT_STARTED, displayStartEndMarker = true)
|
||||
trackOverlay = mapOverlayHelper.createTrackOverlay(context, track, Keys.STATE_TRACKING_STOPPED)
|
||||
trackSpecialMarkersOverlay = mapOverlayHelper.createSpecialMakersTrackOverlay(context, track, Keys.STATE_TRACKING_STOPPED, displayStartEndMarker = true)
|
||||
mapView.overlays.add(trackOverlay)
|
||||
mapView.overlays.add(trackSpecialMarkersOverlay)
|
||||
}
|
||||
// save track
|
||||
CoroutineScope(Dispatchers.IO).launch { track.save_all_files_suspended(context) }
|
||||
}
|
||||
|
||||
|
||||
/* Saves zoom level and center of this map */
|
||||
fun saveViewStateToTrack()
|
||||
{
|
||||
if (track.latitude != 0.0 && track.longitude != 0.0)
|
||||
if (track.view_latitude != 0.0 && track.view_longitude != 0.0)
|
||||
{
|
||||
CoroutineScope(Dispatchers.IO).launch { track.save_json_suspended(context) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Sets up the statistics sheet */
|
||||
private fun setupStatisticsViews() {
|
||||
|
||||
// get step count string - hide step count if not available
|
||||
val steps: String
|
||||
if (track.stepCount == -1f)
|
||||
{
|
||||
steps = context.getString(R.string.statistics_sheet_p_steps_no_pedometer)
|
||||
stepsTitleView.isGone = true
|
||||
stepsView.isGone = true
|
||||
}
|
||||
else
|
||||
{
|
||||
steps = track.stepCount.roundToInt().toString()
|
||||
stepsTitleView.isVisible = true
|
||||
stepsView.isVisible = true
|
||||
}
|
||||
|
||||
private fun setupStatisticsViews()
|
||||
{
|
||||
// populate views
|
||||
val stats: TrackStatistics = track.statistics()
|
||||
trackNameView.text = track.name
|
||||
trackidView.text = track.id.toString()
|
||||
distanceView.text = LengthUnitHelper.convertDistanceToString(track.distance, useImperialUnits)
|
||||
stepsView.text = steps
|
||||
waypointsView.text = track.wayPoints.size.toString()
|
||||
durationView.text = DateTimeHelper.convertToReadableTime(context, track.duration)
|
||||
velocityView.text = LengthUnitHelper.convertToVelocityString(track.duration, track.recordingPaused, track.distance, useImperialUnits)
|
||||
recordingStartView.text = DateTimeHelper.convertToReadableDateAndTime(track.recordingStart)
|
||||
recordingStopView.text = DateTimeHelper.convertToReadableDateAndTime(track.recordingStop)
|
||||
maxAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.maxAltitude, useImperialUnits)
|
||||
minAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.minAltitude, useImperialUnits)
|
||||
positiveElevationView.text = LengthUnitHelper.convertDistanceToString(track.positiveElevation, useImperialUnits)
|
||||
negativeElevationView.text = LengthUnitHelper.convertDistanceToString(track.negativeElevation, useImperialUnits)
|
||||
|
||||
// show / hide recording pause
|
||||
if (track.recordingPaused != 0L) {
|
||||
recordingPausedLabelView.isVisible = true
|
||||
recordingPausedView.isVisible = true
|
||||
recordingPausedView.text = DateTimeHelper.convertToReadableTime(context, track.recordingPaused)
|
||||
} else {
|
||||
recordingPausedLabelView.isGone = true
|
||||
recordingPausedView.isGone = true
|
||||
}
|
||||
distanceView.text = LengthUnitHelper.convertDistanceToString(stats.distance, useImperialUnits)
|
||||
waypointsView.text = track.trkpts.size.toString()
|
||||
durationView.text = DateTimeHelper.convertToReadableTime(context, stats.duration)
|
||||
velocityView.text = LengthUnitHelper.convertToVelocityString(stats.velocity, useImperialUnits)
|
||||
recordingStartView.text = DateTimeHelper.convertToReadableDateAndTime(track.start_time)
|
||||
recordingStopView.text = DateTimeHelper.convertToReadableDateAndTime(track.stop_time)
|
||||
maxAltitudeView.text = LengthUnitHelper.convertDistanceToString(stats.max_altitude, useImperialUnits)
|
||||
minAltitudeView.text = LengthUnitHelper.convertDistanceToString(stats.min_altitude, useImperialUnits)
|
||||
positiveElevationView.text = LengthUnitHelper.convertDistanceToString(stats.total_ascent, useImperialUnits)
|
||||
negativeElevationView.text = LengthUnitHelper.convertDistanceToString(stats.total_descent, useImperialUnits)
|
||||
|
||||
// inform user about possible accuracy issues with altitude measurements
|
||||
elevationDataViews.referencedIds.forEach { id ->
|
||||
|
@ -265,7 +234,6 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/* Defines the behavior of the statistics sheet */
|
||||
private fun getStatisticsSheetCallback(): BottomSheetBehavior.BottomSheetCallback {
|
||||
return object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
|
@ -313,8 +281,8 @@ data class TrackFragmentLayoutHolder(private var context: Context, private var m
|
|||
return false
|
||||
} else {
|
||||
val center: IGeoPoint = mapView.mapCenter
|
||||
track.latitude = center.latitude
|
||||
track.longitude = center.longitude
|
||||
track.view_latitude = center.latitude
|
||||
track.view_longitude = center.longitude
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
9
app/src/main/res/drawable/ic_fiber_manual_stop_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_fiber_manual_stop_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="36dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0"
|
||||
android:width="36dp">
|
||||
<path
|
||||
android:fillColor="@color/recording_management_buttons_icon"
|
||||
android:pathData="M5,5h14v14H5z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_homepoint_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_homepoint_24dp.xml
Normal 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/homepoint"
|
||||
android:pathData="M 12.01 2.5 L 5.883 6.936 L 5.883 3.604 L 3.674 3.604 L 3.674 8.535 L 2.01 9.74 L 1.99 21.5 L 12.35 21.5 L 12.35 16.047 L 17.215 16.047 L 17.215 21.5 L 22.01 21.5 L 22.01 9.74 Z"/>
|
||||
</vector>
|
|
@ -28,7 +28,6 @@
|
|||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:textColor="@color/text_default"
|
||||
app:layout_constraintBottom_toTopOf="@+id/track_data"
|
||||
app:layout_constraintEnd_toStartOf="@+id/star_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@string/sample_text_track_name" />
|
||||
|
@ -37,28 +36,17 @@
|
|||
android:id="@+id/track_data"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:textColor="@color/text_lightweight"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/track_name"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/track_name"
|
||||
tools:text="@string/sample_text_track_data" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/star_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:backgroundTint="@color/default_transparent"
|
||||
android:contentDescription="@string/descr_mark_starred_button"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_star_outline_24dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
|
|
@ -22,40 +22,11 @@
|
|||
app:layout_dodgeInsetEdges="bottom">
|
||||
|
||||
<!-- BUTTON SAVE -->
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/button_save"
|
||||
style="@style/Widget.MaterialComponents.FloatingActionButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/descr_button_save"
|
||||
app:backgroundTint="@color/recording_management_buttons_background"
|
||||
app:fabSize="mini"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/main_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.15"
|
||||
app:layout_constraintStart_toEndOf="@+id/main_button"
|
||||
app:layout_constraintTop_toTopOf="@+id/main_button"
|
||||
app:srcCompat="@drawable/ic_save_24dp"
|
||||
app:tint="@color/recording_management_buttons_icon" />
|
||||
|
||||
<!-- BUTTON CLEAR -->
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/button_clear"
|
||||
style="@style/Widget.MaterialComponents.FloatingActionButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/descr_button_delete"
|
||||
app:backgroundTint="@color/recording_management_buttons_background"
|
||||
app:fabSize="mini"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/main_button"
|
||||
app:layout_constraintEnd_toStartOf="@+id/main_button"
|
||||
app:layout_constraintHorizontal_bias="0.85"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/main_button"
|
||||
app:srcCompat="@drawable/ic_delete_24dp"
|
||||
app:tint="@color/recording_management_buttons_icon" />
|
||||
|
||||
<!-- MAIN BUTTON -->
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/main_button"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -76,69 +47,19 @@
|
|||
android:id="@+id/location_button"
|
||||
style="@style/Widget.MaterialComponents.FloatingActionButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="64dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:contentDescription="@string/descr_button_location"
|
||||
android:src="@drawable/ic_current_location_24dp"
|
||||
app:backgroundTint="@color/location_button_background"
|
||||
app:fabSize="mini"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="@color/location_button_icon" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/live_statistics_distance_outline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="textEnd"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="@color/text_outline_default"
|
||||
app:layout_constraintEnd_toEndOf="@+id/live_statistics_distance"
|
||||
app:layout_constraintTop_toTopOf="@+id/live_statistics_distance" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/live_statistics_distance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="textEnd"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="@color/text_default"
|
||||
app:layout_constraintEnd_toEndOf="@+id/live_statistics_duration"
|
||||
app:layout_constraintTop_toBottomOf="@+id/live_statistics_duration"
|
||||
tools:text="@string/sample_text_default_live_statistics_distance" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/live_statistics_duration_outline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="textEnd"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="@color/text_outline_default"
|
||||
app:layout_constraintEnd_toEndOf="@+id/live_statistics_duration"
|
||||
app:layout_constraintTop_toTopOf="@+id/live_statistics_duration" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/live_statistics_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textAlignment="textEnd"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="@color/text_default"
|
||||
app:layout_constraintEnd_toEndOf="@+id/location_button"
|
||||
app:layout_constraintTop_toBottomOf="@+id/location_button"
|
||||
tools:text="@string/sample_text_default_live_statistics_duration" />
|
||||
|
||||
<!-- GROUPS -->
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/additional_buttons"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="button_clear,button_save" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
|
@ -74,30 +74,6 @@
|
|||
app:layout_constraintTop_toTopOf="@+id/statistics_track_name_headline"
|
||||
app:srcCompat="@drawable/ic_save_to_storage_24dp" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/statistics_p_trackid"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/statistics_sheet_p_trackid"
|
||||
android:textAllCaps="false"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:textColor="@color/text_lightweight"
|
||||
app:layout_constraintStart_toStartOf="@+id/statistics_track_name_headline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/statistics_track_name_headline" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/statistics_data_trackid"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:textColor="@color/text_default"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/statistics_p_trackid"
|
||||
app:layout_constraintStart_toEndOf="@+id/statistics_p_trackid"
|
||||
app:layout_constraintTop_toTopOf="@+id/statistics_p_trackid"
|
||||
tools:text="@string/sample_text_default_data" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/statistics_p_distance"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -108,7 +84,7 @@
|
|||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:textColor="@color/text_lightweight"
|
||||
app:layout_constraintStart_toStartOf="@+id/statistics_track_name_headline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/statistics_p_trackid" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/statistics_track_name_headline" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/statistics_data_distance"
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
<!-- fab sub menu_bottom_navigation -->
|
||||
<string name="button_delete">Ryd</string>
|
||||
<string name="button_save">Gem</string>
|
||||
<string name="button_resume">Fortsæt</string>
|
||||
<!-- dialogs -->
|
||||
<string name="dialog_share_gpx">Del GPX fil med</string>
|
||||
<string name="dialog_error_empty_recording_message">Trackbook har ingen rutepunkter endnu.</string>
|
||||
|
@ -86,9 +85,6 @@
|
|||
<string name="pref_about_title">Om</string>
|
||||
<string name="track_list_p_element_statistics">Samlet registreret distance</string>
|
||||
<string name="pref_report_issue_summary">Rapporter fejl og foreslå forbedringer på GitHub.</string>
|
||||
<string name="pref_recording_accuracy_title">Nøjagtighed af optagelser</string>
|
||||
<string name="pref_recording_accuracy_summary_default">Waypoints har lavere nøjagtighed, men er mere hyppige.</string>
|
||||
<string name="pref_recording_accuracy_summary_high">Waypoints har større nøjagtighed, men er mindre hyppige.</string>
|
||||
<string name="pref_imperial_measurement_units_summary_metric">I øjeblikket anvendes metriske enheder (kilometer, meter).</string>
|
||||
<string name="pref_gps_only_summary_gps_only">I øjeblikket bruger vi kun GPS til lokalisering.</string>
|
||||
<string name="pref_gps_only_summary_gps_and_network">I øjeblikket bruges GPS og netværk til lokalisering.</string>
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
<!-- fab sub menu_bottom_navigation -->
|
||||
<string name="button_delete">Zurücksetzen</string>
|
||||
<string name="button_save">Speichern</string>
|
||||
<string name="button_resume">Fortsetzen</string>
|
||||
<!-- dialogs -->
|
||||
<string name="dialog_share_gpx">GPX-Datei teilen mit</string>
|
||||
<string name="dialog_error_empty_recording_message">Trackbook hat noch keine Wegpunkte aufgenommen.</string>
|
||||
|
@ -110,7 +109,4 @@
|
|||
<string name="toast_message_poi_removed">Markierung für Ort von Interesse entfernt.</string>
|
||||
<string name="toast_message_poi_added">Markierung für Ort von Interesse hinzugefügt.</string>
|
||||
<string name="track_list_p_element_statistics">Erfasste Gesamtentfernung</string>
|
||||
<string name="pref_recording_accuracy_summary_high">Wegpunkte haben eine höhere Genauigkeit, sind aber weniger häufig.</string>
|
||||
<string name="pref_recording_accuracy_summary_default">Wegpunkte haben eine geringere Genauigkeit, sind aber häufiger.</string>
|
||||
<string name="pref_recording_accuracy_title">Aufzeichnungsgenauigkeit</string>
|
||||
</resources>
|
|
@ -28,7 +28,6 @@
|
|||
<string name="snackbar_message_location_permission_denied">Permiso de ubicación no concedido. Trackbook no funcionará.</string>
|
||||
<string name="button_delete">Limpiar</string>
|
||||
<string name="button_save">Guardar</string>
|
||||
<string name="button_resume">Resumir</string>
|
||||
<string name="dialog_generic_button_okay">OK</string>
|
||||
<string name="dialog_generic_details_button">Mostrar detalles</string>
|
||||
<string name="dialog_error_empty_recording_message">Trackbook no registró ningún punto de referencia hasta el momento.</string>
|
||||
|
@ -77,9 +76,6 @@
|
|||
<string name="pref_gps_only_summary_gps_and_network">Actualmente usando GPS y Red para localización.</string>
|
||||
<string name="pref_imperial_measurement_units_summary_metric">Actualmente se utilizan unidades métricas (Kilómetro, Metro).</string>
|
||||
<string name="pref_imperial_measurement_units_title">Utilizar medidas imperiales</string>
|
||||
<string name="pref_recording_accuracy_summary_high">Los puntos de referencia tienen mayor precisión pero son menos frecuentes.</string>
|
||||
<string name="pref_recording_accuracy_summary_default">Los waypoints tienen menor precisión pero son más frecuentes.</string>
|
||||
<string name="pref_recording_accuracy_title">Precisión de grabación</string>
|
||||
<string name="pref_report_issue_title">Reportar problema</string>
|
||||
<string name="pref_reset_advanced_summary">Restablece la configuración avanzada a los valores predeterminados.</string>
|
||||
<string name="pref_reset_advanced_title">Reiniciar</string>
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
<string name="snackbar_message_location_offline">Localisation désactivée. Le suivi ne fonctionnera pas.</string>
|
||||
<!-- fab sub menu_bottom_navigation -->
|
||||
<string name="button_delete">Supprimer</string>
|
||||
<string name="button_resume">Reprendre</string>
|
||||
<string name="button_save">Sauvegarder</string>
|
||||
<!-- dialogs -->
|
||||
<string name="dialog_error_empty_recording_button_resume">Reprendre l\'enregistrement</string>
|
||||
|
@ -109,9 +108,6 @@
|
|||
<string name="toast_message_copied_to_clipboard">Copié dans le presse-papiers.</string>
|
||||
<string name="toast_message_poi_removed">Marqueur de point d\'intérêt supprimé.</string>
|
||||
<string name="toast_message_poi_added">Marqueur de point d\'intérêt ajouté.</string>
|
||||
<string name="pref_recording_accuracy_title">Précision de l\'enregistrement</string>
|
||||
<string name="pref_recording_accuracy_summary_default">Les points de cheminements ont une précision plus faible mais sont plus fréquents.</string>
|
||||
<string name="pref_recording_accuracy_summary_high">Les points de cheminement ont une plus grande précision mais sont moins fréquents.</string>
|
||||
<string name="track_list_p_element_statistics">Distance totale enregistrée</string>
|
||||
<string name="descr_button_location">Centrer sur la position actuelle</string>
|
||||
</resources>
|
|
@ -32,7 +32,6 @@
|
|||
<string name="button_save">Spremi</string>
|
||||
<string name="pref_general_title">Opće</string>
|
||||
<string name="pref_accuracy_threshold_title">Prag točnosti</string>
|
||||
<string name="button_resume">Nastavi</string>
|
||||
<string name="pref_theme_selection_title">Tema programa</string>
|
||||
<string name="tab_tracks">Rute</string>
|
||||
<string name="descr_map_last_track">Mapa zadnje rute</string>
|
||||
|
@ -96,9 +95,6 @@
|
|||
<string name="toast_message_copied_to_clipboard">Kopirano u međuspremnik.</string>
|
||||
<string name="toast_message_poi_removed">Oznaka točke interesa uklonjena.</string>
|
||||
<string name="toast_message_poi_added">Oznaka točke interesa dodana.</string>
|
||||
<string name="pref_recording_accuracy_summary_high">Točke rute imaju veću točnost ali su rjeđe.</string>
|
||||
<string name="pref_recording_accuracy_title">Točnost snimanja</string>
|
||||
<string name="pref_recording_accuracy_summary_default">Točke rute imaju manju točnost ali su češće.</string>
|
||||
<string name="track_list_p_element_statistics">Ukupna udaljenost snimljena</string>
|
||||
<string name="descr_button_location">Centriraj na trenutačno mjesto</string>
|
||||
</resources>
|
|
@ -182,7 +182,6 @@
|
|||
<string name="dialog_generic_details_button">Tampilkan detil</string>
|
||||
<string name="dialog_generic_button_okay">Oke</string>
|
||||
<string name="dialog_generic_button_cancel">Batalkan</string>
|
||||
<string name="button_resume">Lanjutkan</string>
|
||||
<string name="button_save">Simpan</string>
|
||||
<string name="button_delete">Bersihkan</string>
|
||||
<string name="snackbar_message_location_permission_denied">Izin lokasi tak diberikan. Trackbook tidak akan berfungsi.</string>
|
||||
|
@ -192,8 +191,5 @@
|
|||
<string name="notification_show">Tampilkan</string>
|
||||
<string name="notification_resume">Lanjutkan</string>
|
||||
<string name="tab_settings">Pengaturan</string>
|
||||
<string name="pref_recording_accuracy_summary_default">Waypoints memiliki akurasi yang lebih rendah tetapi lebih sering.</string>
|
||||
<string name="pref_recording_accuracy_summary_high">Waypoints memiliki akurasi yang lebih tinggi tetapi lebih jarang.</string>
|
||||
<string name="pref_recording_accuracy_title">Akurasi Perekaman</string>
|
||||
<string name="track_list_p_element_statistics">Jarak Total Terekam</string>
|
||||
</resources>
|
|
@ -21,7 +21,6 @@
|
|||
<!-- FAB Sub Menu -->
|
||||
<string name="button_delete">Cancella</string>
|
||||
<string name="button_save">Salva</string>
|
||||
<string name="button_resume">Riprendi</string>
|
||||
<!-- Dialogs -->
|
||||
<string name="dialog_generic_button_cancel">Annulla</string>
|
||||
<string name="dialog_generic_button_okay">OK</string>
|
||||
|
@ -116,8 +115,5 @@
|
|||
<string name="sample_text_track_data" translatable="false">23.0 km • 5 hrs 23 min 42 sec</string>
|
||||
<string name="sample_text_track_name" translatable="false">July 20, 1969</string>
|
||||
<string name="sample_text_default_data" translatable="false">track data missing</string>
|
||||
<string name="pref_recording_accuracy_title">Precisione di registrazione</string>
|
||||
<string name="pref_recording_accuracy_summary_high">I waypoint hanno una maggiore precisione ma sono meno frequenti.</string>
|
||||
<string name="pref_recording_accuracy_summary_default">I waypoint hanno una precisione inferiore ma sono più frequenti.</string>
|
||||
<string name="track_list_p_element_statistics">Distanza totale registrata</string>
|
||||
</resources>
|
|
@ -18,7 +18,6 @@
|
|||
<!-- fab sub menu_bottom_navigation -->
|
||||
<string name="button_delete">クリア</string>
|
||||
<string name="button_save">保存してクリア</string>
|
||||
<string name="button_resume">再開</string>
|
||||
<!-- dialogs -->
|
||||
<string name="dialog_share_gpx">GPX ファイルを共有...</string>
|
||||
<string name="dialog_error_empty_recording_message">トラックブックはこれまでウェイポイントを記録していません。</string>
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
<!-- fab sub menu_bottom_navigation -->
|
||||
<string name="button_delete">Tøm</string>
|
||||
<string name="button_save">Lagre og tøm</string>
|
||||
<string name="button_resume">Fortsett</string>
|
||||
<!-- dialogs -->
|
||||
<string name="dialog_share_gpx">Del GPX-fil med</string>
|
||||
<string name="dialog_error_empty_recording_message">Trackbook har ikke registrert noen veipunkter så langt.</string>
|
||||
|
@ -109,9 +108,6 @@
|
|||
<string name="toast_message_poi_added">Interessepunktmarkør lagt til.</string>
|
||||
<string name="toast_message_poi_removed">Interessepunktmarkør fjernet.</string>
|
||||
<string name="toast_message_copied_to_clipboard">Kopiert til utklippstavle.</string>
|
||||
<string name="pref_recording_accuracy_summary_high">Veipunkter har høyere nøyaktighet, men er mindre hyppige.</string>
|
||||
<string name="track_list_p_element_statistics">Totalavstand registrert</string>
|
||||
<string name="pref_recording_accuracy_summary_default">Veipunkter har lavere nøyaktighet, men er hyppigere.</string>
|
||||
<string name="pref_recording_accuracy_title">Opptaksnøyaktighet</string>
|
||||
<string name="descr_button_location">Sentrer på nåværende sted</string>
|
||||
</resources>
|
|
@ -18,7 +18,6 @@
|
|||
<!-- fab sub menu_bottom_navigation -->
|
||||
<string name="button_delete">Wissen</string>
|
||||
<string name="button_save">Opslaan</string>
|
||||
<string name="button_resume">Hervatten</string>
|
||||
<!-- dialogs -->
|
||||
<string name="dialog_share_gpx">GPX-bestand delen met</string>
|
||||
<string name="dialog_error_empty_recording_message">Trackbook heeft nog geen routepunten vastgelegd.</string>
|
||||
|
@ -109,9 +108,6 @@
|
|||
<string name="toast_message_copied_to_clipboard">Gekopieerd naar klembord.</string>
|
||||
<string name="toast_message_poi_removed">POI-indicatie verwijderd.</string>
|
||||
<string name="toast_message_poi_added">POI-indicatie toegevoegd.</string>
|
||||
<string name="pref_recording_accuracy_title">Opname nauwkeurigheid</string>
|
||||
<string name="pref_recording_accuracy_summary_default">Routepunten zijn minder nauwkeurig, maar frequenter.</string>
|
||||
<string name="pref_recording_accuracy_summary_high">Routepunten zijn nauwkeuriger, maar minder frequent.</string>
|
||||
<string name="track_list_p_element_statistics">Totaal afgelegde afstand</string>
|
||||
<string name="descr_button_location">Centreren op huidige locatie</string>
|
||||
</resources>
|
|
@ -3,7 +3,6 @@
|
|||
<string name="dialog_generic_details_button">Pokaż szczegóły</string>
|
||||
<string name="dialog_generic_button_okay">OK</string>
|
||||
<string name="dialog_generic_button_cancel">Anuluj</string>
|
||||
<string name="button_resume">Kontynuuj</string>
|
||||
<string name="button_save">Zapisz</string>
|
||||
<string name="button_delete">Wyczyść</string>
|
||||
<string name="snackbar_message_location_offline">Lokalizacja jest wyłączona. Trackbook nie będzie działać.</string>
|
||||
|
@ -76,9 +75,6 @@
|
|||
<string name="pref_reset_advanced_title">Resetuj</string>
|
||||
<string name="pref_reset_advanced_summary">Przywróć ustawienia zaawansowane do wartości domyślnych.</string>
|
||||
<string name="pref_report_issue_summary">Zgłaszaj błędy i proponuj ulepszenia na GitHub.</string>
|
||||
<string name="pref_recording_accuracy_title">Dokładność zapisu</string>
|
||||
<string name="pref_recording_accuracy_summary_default">Punkty trasy mają mniejszą dokładność, ale są częstsze.</string>
|
||||
<string name="pref_recording_accuracy_summary_high">Punkty trasy mają większą dokładność, ale występują rzadziej.</string>
|
||||
<string name="descr_button_resume">Przycisk Wznów</string>
|
||||
<string name="descr_map_current_track">Odwzorowanie bieżącego toru</string>
|
||||
<string name="abbreviation_minutes">min</string>
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
<!-- App Name -->
|
||||
<string name="app_name">Trackbook</string>
|
||||
<!-- please do not translate app_name - transcription into different alphabet types is fine though -->
|
||||
<string name="app_version_name" translatable="false">\"Echoes\"</string>
|
||||
<!-- Tabs -->
|
||||
<string name="tab_map">Mapa</string>
|
||||
<string name="tab_tracks">Caminhos</string>
|
||||
|
@ -22,7 +21,6 @@
|
|||
<!-- FAB Sub Menu -->
|
||||
<string name="button_delete">Limpar</string>
|
||||
<string name="button_save">Salvar</string>
|
||||
<string name="button_resume">Retomar</string>
|
||||
<!-- Dialogs -->
|
||||
<string name="dialog_generic_button_cancel">Cancelar</string>
|
||||
<string name="dialog_generic_button_okay">OK</string>
|
||||
|
@ -113,8 +111,5 @@
|
|||
<string name="descr_statistics_sheet_delete_button">Botão apagar rota</string>
|
||||
<string name="descr_statistics_sheet_edit_button">Botão editar rota</string>
|
||||
<string name="descr_statistics_sheet_save_button">Botão salvar como GPX</string>
|
||||
<string name="pref_recording_accuracy_summary_high">Os waypoints têm maior precisão, mas são menos freqüentes.</string>
|
||||
<string name="pref_recording_accuracy_title">Precisão de Gravação</string>
|
||||
<string name="track_list_p_element_statistics">Distância Total Registrada</string>
|
||||
<string name="pref_recording_accuracy_summary_default">Os waypoints têm menor precisão, mas são mais freqüentes.</string>
|
||||
</resources>
|
|
@ -33,7 +33,6 @@
|
|||
<string name="dialog_generic_details_button">Показать подробности</string>
|
||||
<string name="dialog_generic_button_okay">ОЕ</string>
|
||||
<string name="dialog_generic_button_cancel">Отмена</string>
|
||||
<string name="button_resume">Продолжить</string>
|
||||
<string name="button_save">Сохранить</string>
|
||||
<string name="button_delete">Очистить</string>
|
||||
<string name="snackbar_message_location_permission_denied">Разрешение на определение местоположения не предоставлено. Trackbook работать не будет.</string>
|
||||
|
@ -67,8 +66,6 @@
|
|||
<string name="pref_gps_only_summary_gps_and_network">В настоящее время для локализации используется GPS и сеть.</string>
|
||||
<string name="pref_imperial_measurement_units_summary_metric">В настоящее время используются метрические единицы (километр, метр).</string>
|
||||
<string name="pref_gps_only_summary_gps_only">В настоящее время для локализации используется только GPS.</string>
|
||||
<string name="pref_recording_accuracy_title">Точность записи</string>
|
||||
<string name="pref_recording_accuracy_summary_default">Путевые точки имеют более низкую точность, но встречаются чаще.</string>
|
||||
<string name="pref_reset_advanced_summary">Сброс расширенных настроек до значений по умолчанию.</string>
|
||||
<string name="pref_report_issue_title">Выпуск отчета</string>
|
||||
<string name="pref_theme_selection_mode_dark">Темный режим</string>
|
||||
|
@ -81,7 +78,6 @@
|
|||
<string name="abbreviation_seconds">сек</string>
|
||||
<string name="abbreviation_minutes">мин</string>
|
||||
<string name="pref_report_issue_summary">Сообщайте об ошибках и предлагайте улучшения на GitHub.</string>
|
||||
<string name="pref_recording_accuracy_summary_high">Путевые точки имеют более высокую точность, но встречаются реже.</string>
|
||||
<string name="pref_imperial_measurement_units_title">Используйте имперские меры</string>
|
||||
<string name="pref_imperial_measurement_units_summary_imperial">В настоящее время используются имперские единицы (мили, футы).</string>
|
||||
<string name="pref_gps_only_title">Ограничение на GPS</string>
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
<!-- fab sub menu_bottom_navigation -->
|
||||
<string name="button_delete">Rensa</string>
|
||||
<string name="button_save">Spara</string>
|
||||
<string name="button_resume">Återuppta</string>
|
||||
<!-- dialogs -->
|
||||
<string name="dialog_share_gpx">Dela GPX-fil med</string>
|
||||
<string name="dialog_error_empty_recording_message">Trackbook spelade inte in några vägpunkter så här långt.</string>
|
||||
|
@ -96,7 +95,6 @@
|
|||
<string name="descr_statistics_sheet_edit_button">Spårredigeringsknapp</string>
|
||||
<string name="pref_reset_advanced_title">Återställ</string>
|
||||
<string name="toast_message_elevation_info">Tips: Noggrannheten hos höjddata beror på din enhet. Höjden i upp- och nedförsbacke för hela rutten mäts.</string>
|
||||
<string name="pref_recording_accuracy_summary_high">Vägpunkter har högre noggrannhet men är mindre frekventa.</string>
|
||||
<string name="pref_imperial_measurement_units_summary_metric">För närvarande används metriska enheter (kilometer, meter).</string>
|
||||
<string name="pref_advanced_title">Avancerad</string>
|
||||
<string name="pref_accuracy_threshold_title">Tröskel för noggrannhet</string>
|
||||
|
@ -115,7 +113,6 @@
|
|||
<string name="pref_reset_advanced_summary">Återställ avancerade inställningar till standardinställningarna.</string>
|
||||
<string name="pref_report_issue_title">Rapportera frågan</string>
|
||||
<string name="pref_report_issue_summary">Rapportera fel och föreslå förbättringar på GitHub.</string>
|
||||
<string name="pref_recording_accuracy_title">Noggrannhet vid inspelning</string>
|
||||
<string name="track_list_onboarding_h1_part_2">... kommer att visas här.</string>
|
||||
<string name="track_list_onboarding_h1_part_1">Dina inspelade spår</string>
|
||||
<string name="layout_onboarding_description_app_icon">Ikon för Trackbook-appen</string>
|
||||
|
@ -156,7 +153,6 @@
|
|||
<string name="descr_quick_settings_tile_title_pause">Stoppa inspelningen</string>
|
||||
<string name="descr_mark_starred_button">Markera som stjärnmärkt</string>
|
||||
<string name="descr_button_resume">Återuppta inspelning</string>
|
||||
<string name="pref_recording_accuracy_summary_default">Vägpunkter har lägre noggrannhet men är mer frekventa.</string>
|
||||
<string name="statistics_sheet_p_duration">Total varaktighet:</string>
|
||||
<string name="pref_gps_only_summary_gps_only">För närvarande används endast GPS för lokalisering.</string>
|
||||
<string name="pref_delete_non_starred_summary">Ta bort alla inspelningar i \"Tracks\" som inte har stjärnor.</string>
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
<string name="descr_mark_starred_button">Yıldızlı olarak işaretle düğmesi</string>
|
||||
<string name="descr_button_resume">Kaydı devam ettir</string>
|
||||
<string name="notification_resume">Devam ettir</string>
|
||||
<string name="button_resume">Devam ettir</string>
|
||||
<string name="descr_button_delete">Kaydı temizle</string>
|
||||
<string name="descr_button_save">Kaydı kaydet</string>
|
||||
<string name="descr_button_start">Kaydı başlat düğmesi</string>
|
||||
|
@ -96,9 +95,6 @@
|
|||
<string name="tab_tracks">Yollar</string>
|
||||
<string name="tab_map">Harita</string>
|
||||
<string name="app_name">Trackbook</string>
|
||||
<string name="pref_recording_accuracy_title">Kayıt Doğruluğu</string>
|
||||
<string name="pref_recording_accuracy_summary_default">Ara noktalar daha düşük doğruluğa sahiptir ancak daha sıktır.</string>
|
||||
<string name="pref_recording_accuracy_summary_high">Ara noktalar daha yüksek doğruluğa sahiptir ancak daha az sıklıktadır.</string>
|
||||
<string name="track_list_p_element_statistics">Kaydedilen Toplam Mesafe</string>
|
||||
<string name="descr_button_location">Geçerli konuma ortala</string>
|
||||
</resources>
|
|
@ -21,7 +21,6 @@
|
|||
<!-- Buttons -->
|
||||
<string name="button_delete">删除</string>
|
||||
<string name="button_pause">暂停</string>
|
||||
<string name="button_resume">恢复</string>
|
||||
<string name="button_save">保存</string>
|
||||
<string name="button_start">开始</string>
|
||||
<!-- Dialogs -->
|
||||
|
@ -92,9 +91,6 @@
|
|||
<string name="pref_imperial_measurement_units_summary_metric">当前正使用公制单位(千米,米)。</string>
|
||||
<string name="pref_imperial_measurement_units_summary_imperial">当前正使用英制单位(英里,英尺)。</string>
|
||||
<string name="pref_imperial_measurement_units_title">使用英制测量</string>
|
||||
<string name="pref_recording_accuracy_summary_high">航点的精确度较高,但频率较低。</string>
|
||||
<string name="pref_recording_accuracy_summary_default">航点的精确度较低,但频率较高。</string>
|
||||
<string name="pref_recording_accuracy_title">记录精确度</string>
|
||||
<string name="pref_report_issue_summary">在 Github 上报告漏洞并提出改进建议。</string>
|
||||
<string name="pref_report_issue_title">报告问题</string>
|
||||
<string name="pref_reset_advanced_summary">重置高级设置为默认值。</string>
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
<color name="default_red">#FFDC3D33</color> <!-- Slightly muted variant of -> Material Design 2: Red 600 -->
|
||||
<color name="default_red_dark">#FFCA2D23</color>
|
||||
<color name="default_blue">#FF3C98DB</color>
|
||||
<color name="homepoint">#FFFFC107</color>
|
||||
<color name="default_green">#FF4CAF50</color>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- App Name -->
|
||||
<string name="app_name">Trackbook-v</string>
|
||||
<string name="app_name">trkpt</string>
|
||||
<!-- please do not translate app_name - transcription into different alphabet types is fine though -->
|
||||
<string name="app_version_name" translatable="false">\"See Emily Play\"</string>
|
||||
<!-- Tabs -->
|
||||
<string name="tab_map">Map</string>
|
||||
<string name="tab_tracks">Tracks</string>
|
||||
<string name="tab_settings">Settings</string>
|
||||
<!-- Notification -->
|
||||
<string name="notification_title_trackbook_running">Trackbook running</string>
|
||||
<string name="notification_title_trackbook_not_running">Trackbook not running</string>
|
||||
<string name="notification_pause">Pause</string>
|
||||
<string name="notification_resume">Resume</string>
|
||||
<string name="notification_title_trackbook_not_running">Trackbook stopped</string>
|
||||
<string name="notification_pause">Stop</string>
|
||||
<string name="notification_resume">Record</string>
|
||||
<string name="notification_show">Show</string>
|
||||
<string name="notification_channel_recording_name">Movement Recording State</string>
|
||||
<string name="notification_channel_recording_description">Display duration and distance. Option to pause movement recording.</string>
|
||||
|
@ -21,10 +20,9 @@
|
|||
<string name="snackbar_message_location_permission_denied">Location permission not granted. Trackbook will not work.</string>
|
||||
<!-- Buttons -->
|
||||
<string name="button_delete">Delete</string>
|
||||
<string name="button_pause">Pause</string>
|
||||
<string name="button_resume">Resume</string>
|
||||
<string name="button_pause">Stop</string>
|
||||
<string name="button_save">Save</string>
|
||||
<string name="button_start">Start</string>
|
||||
<string name="button_start">Record</string>
|
||||
<!-- Dialogs -->
|
||||
<string name="dialog_delete_current_recording_message">Discard current recording?</string>
|
||||
<string name="dialog_delete_current_recording_button_discard">Discard</string>
|
||||
|
@ -86,7 +84,9 @@
|
|||
<string name="pref_altitude_smoothing_value_summary" translatable="false">Number of waypoints used to smooth the elevation curve.</string>
|
||||
<string name="pref_altitude_smoothing_value_title" translatable="false">Altitude Smoothing</string>
|
||||
<string name="pref_auto_export_interval_summary">Automatically export GPX file after this many hours.</string>
|
||||
<string name="pref_device_id_summary">A unique ID to distinguish tracks recorded across multiple devices.</string>
|
||||
<string name="pref_auto_export_interval_title">Auto Export Interval</string>
|
||||
<string name="pref_device_id">Device ID</string>
|
||||
<string name="pref_advanced_title">Advanced</string>
|
||||
<string name="pref_delete_non_starred_summary">Delete all recordings in \"Tracks\" that are not starred.</string>
|
||||
<string name="pref_delete_non_starred_title">Delete Non-Starred Recordings</string>
|
||||
|
@ -117,7 +117,7 @@
|
|||
<!-- Descriptions -->
|
||||
<string name="descr_button_delete">Discard recording</string>
|
||||
<string name="descr_button_location">Center on current location</string>
|
||||
<string name="descr_button_pause">Pause recording</string>
|
||||
<string name="descr_button_pause">Stop recording</string>
|
||||
<string name="descr_button_resume">Resume recording</string>
|
||||
<string name="descr_button_save">Save recording</string>
|
||||
<string name="descr_button_start">Start recording</string>
|
||||
|
|
Loading…
Reference in a new issue