From bb00e18312cfc07b3515139ac76c8bba6a3de7f6 Mon Sep 17 00:00:00 2001 From: y20k Date: Sun, 2 May 2021 22:51:22 +0200 Subject: [PATCH] use smoothed altitude values to get more realistic elevation data (see #99) - v2 (needs testing) --- app/src/main/java/org/y20k/trackbook/Keys.kt | 2 +- .../java/org/y20k/trackbook/TrackerService.kt | 144 ++++++++++++++---- .../java/org/y20k/trackbook/core/WayPoint.kt | 3 +- .../y20k/trackbook/helpers/LocationHelper.kt | 48 ++---- .../org/y20k/trackbook/helpers/TrackHelper.kt | 65 ++------ 5 files changed, 143 insertions(+), 119 deletions(-) diff --git a/app/src/main/java/org/y20k/trackbook/Keys.kt b/app/src/main/java/org/y20k/trackbook/Keys.kt index fa33abc..56871d0 100644 --- a/app/src/main/java/org/y20k/trackbook/Keys.kt +++ b/app/src/main/java/org/y20k/trackbook/Keys.kt @@ -108,7 +108,7 @@ 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_ALTITUDE_SMOOTHING_VALUE: Int = 15 + const val DEFAULT_ALTITUDE_SMOOTHING_VALUE: Int = 10 const val DEFAULT_THRESHOLD_LOCATION_ACCURACY: Int = 30 // 30 meters const val DEFAULT_THRESHOLD_LOCATION_AGE: Long = 60000000000L // one minute in nanoseconds const val DEFAULT_THRESHOLD_DISTANCE: Float = 15f // 15 meters diff --git a/app/src/main/java/org/y20k/trackbook/TrackerService.kt b/app/src/main/java/org/y20k/trackbook/TrackerService.kt index 4194e7c..54d052e 100644 --- a/app/src/main/java/org/y20k/trackbook/TrackerService.kt +++ b/app/src/main/java/org/y20k/trackbook/TrackerService.kt @@ -61,7 +61,6 @@ class TrackerService: Service(), CoroutineScope, SensorEventListener { var useImperial: Boolean = false var gpsOnly: Boolean = false var accuracyMultiplier: Int = 1 - var altitudeSmoothingValue: Int = Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE var currentBestLocation: Location = LocationHelper.getDefaultLocation() var lastSave: Date = Keys.DEFAULT_DATE var stepCountOffset: Float = 0f @@ -72,6 +71,7 @@ class TrackerService: Service(), CoroutineScope, SensorEventListener { var bound: Boolean = false private val binder = LocalBinder() private val handler: Handler = Handler() + private var altitudeValues: SimpleMovingAverageQueue = SimpleMovingAverageQueue(Keys.DEFAULT_ALTITUDE_SMOOTHING_VALUE) private lateinit var locationManager: LocationManager private lateinit var sensorManager: SensorManager private lateinit var notificationManager: NotificationManager @@ -91,7 +91,7 @@ class TrackerService: Service(), CoroutineScope, SensorEventListener { gpsOnly = PreferencesHelper.loadGpsOnly(this) useImperial = PreferencesHelper.loadUseImperialUnits(this) accuracyMultiplier = PreferencesHelper.loadAccuracyMultiplier(this) - altitudeSmoothingValue = PreferencesHelper.loadAltitudeSmoothingValue(this) + locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -104,7 +104,10 @@ class TrackerService: Service(), CoroutineScope, SensorEventListener { currentBestLocation = LocationHelper.getLastKnownLocation(this) track = FileHelper.readTrack(this, FileHelper.getTempFileUri(this)) backgroundJob = Job() - PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener) + altitudeValues.capacity = PreferencesHelper.loadAltitudeSmoothingValue(this) + PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener( + sharedPreferenceChangeListener + ) } @@ -114,7 +117,10 @@ class TrackerService: Service(), CoroutineScope, SensorEventListener { // SERVICE RESTART (via START_STICKY) if (intent == null) { if (trackingState == Keys.STATE_TRACKING_ACTIVE) { - LogHelper.w(TAG, "Trackbook has been killed by the operating system. Trying to resume recording.") + LogHelper.w( + TAG, + "Trackbook has been killed by the operating system. Trying to resume recording." + ) resumeTracking() } // ACTION STOP @@ -175,7 +181,9 @@ class TrackerService: Service(), CoroutineScope, SensorEventListener { // remove notification stopForeground(true) // stop listening for changes in shared preferences - PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener) + PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener( + sharedPreferenceChangeListener + ) // stop receiving location updates removeGpsLocationListener() removeNetworkLocationListener() @@ -304,15 +312,25 @@ class TrackerService: Service(), CoroutineScope, SensorEventListener { override fun onProviderEnabled(provider: String) { LogHelper.v(TAG, "onProviderEnabled $provider") when (provider) { - LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled(locationManager) - LocationManager.NETWORK_PROVIDER -> networkProviderActive = LocationHelper.isNetworkEnabled(locationManager) + LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled( + locationManager + ) + LocationManager.NETWORK_PROVIDER -> networkProviderActive = + LocationHelper.isNetworkEnabled( + locationManager + ) } } override fun onProviderDisabled(provider: String) { LogHelper.v(TAG, "onProviderDisabled $provider") when (provider) { - LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled(locationManager) - LocationManager.NETWORK_PROVIDER -> networkProviderActive = LocationHelper.isNetworkEnabled(locationManager) + LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled( + locationManager + ) + LocationManager.NETWORK_PROVIDER -> networkProviderActive = + LocationHelper.isNetworkEnabled( + locationManager + ) } } override fun onStatusChanged(p0: String?, p1: Int, p2: Bundle?) { @@ -330,13 +348,24 @@ class TrackerService: Service(), CoroutineScope, SensorEventListener { gpsProviderActive = LocationHelper.isGpsEnabled(locationManager) if (gpsProviderActive) { // check for location permission - if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED) { // adds GPS location listener - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f,gpsLocationListener) + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 0, + 0f, + gpsLocationListener + ) gpsLocationListenerRegistered = true LogHelper.v(TAG, "Added GPS location listener.") } else { - LogHelper.w(TAG, "Unable to add GPS location listener. Location permission is not granted.") + LogHelper.w( + TAG, + "Unable to add GPS location listener. Location permission is not granted." + ) } } else { LogHelper.w(TAG, "Unable to add GPS location listener.") @@ -355,19 +384,33 @@ class TrackerService: Service(), CoroutineScope, SensorEventListener { networkProviderActive = LocationHelper.isNetworkEnabled(locationManager) if (networkProviderActive && !gpsOnly) { // check for location permission - if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED) { // adds Network location listener - locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0f, networkLocationListener) + locationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + 0, + 0f, + networkLocationListener + ) networkLocationListenerRegistered = true LogHelper.v(TAG, "Added Network location listener.") } else { - LogHelper.w(TAG, "Unable to add Network location listener. Location permission is not granted.") + LogHelper.w( + TAG, + "Unable to add Network location listener. Location permission is not granted." + ) } } else { LogHelper.w(TAG, "Unable to add Network location listener.") } } else { - LogHelper.v(TAG, "Skipping registration. Network location listener has already been added.") + LogHelper.v( + TAG, + "Skipping registration. Network location listener has already been added." + ) } } @@ -379,7 +422,10 @@ class TrackerService: Service(), CoroutineScope, SensorEventListener { gpsLocationListenerRegistered = false LogHelper.v(TAG, "Removed GPS location listener.") } else { - LogHelper.w(TAG, "Unable to remove GPS location listener. Location permission is needed.") + LogHelper.w( + TAG, + "Unable to remove GPS location listener. Location permission is needed." + ) } } @@ -391,14 +437,21 @@ class TrackerService: Service(), CoroutineScope, SensorEventListener { networkLocationListenerRegistered = false LogHelper.v(TAG, "Removed Network location listener.") } else { - LogHelper.w(TAG, "Unable to remove Network location listener. Location permission is needed.") + LogHelper.w( + TAG, + "Unable to remove Network location listener. Location permission is needed." + ) } } /* Registers a step counter listener */ private fun startStepCounter() { - val stepCounterAvailable = sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_UI) + 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 @@ -408,7 +461,12 @@ class TrackerService: Service(), CoroutineScope, SensorEventListener { /* Displays / updates notification */ private fun displayNotification(): Notification { - val notification: Notification = notificationHelper.createNotification(trackingState, track.length, track.duration, useImperial) + val notification: Notification = notificationHelper.createNotification( + trackingState, + track.length, + track.duration, + useImperial + ) notificationManager.notify(Keys.TRACKER_SERVICE_NOTIFICATION_ID, notification) return notification } @@ -459,25 +517,34 @@ class TrackerService: Service(), CoroutineScope, SensorEventListener { private val periodicTrackUpdate: Runnable = object : Runnable { override fun run() { // add waypoint to track - step count is continuously updated in onSensorChanged - val result: Pair = TrackHelper.addWayPointToTrack(this@TrackerService, track, currentBestLocation, accuracyMultiplier, altitudeSmoothingValue, resumed) - // get track from result - track = result.first - // check if waypoint was successfully added (= result.second) - if (resumed && result.second) { - // reset resumed flag, if necessary - resumed = false - } + val result: Pair = TrackHelper.addWayPointToTrack(track, currentBestLocation, accuracyMultiplier, resumed) + // get results + val successfullyAdded: Boolean = result.first + track = result.second // check, if waypoint was added - if (result.second) { + if (successfullyAdded) { // reset resumed flag, if necessary if (resumed) { resumed = false } + + // store previous smoothed altitude + val previousAltitude: Double = altitudeValues.average + // put current altitude into queue + altitudeValues.add(currentBestLocation.altitude) + // get current smoothed altitude + val currentAltitude: Double = altitudeValues.average + // calculate and store elevation differences + track = LocationHelper.calculateElevationDifferences(currentAltitude, previousAltitude, track) + // save a temp track val now: Date = GregorianCalendar.getInstance().time if (now.time - lastSave.time > Keys.SAVE_TEMP_TRACK_INTERVAL) { lastSave = now - GlobalScope.launch { FileHelper.saveTempTrackSuspended(this@TrackerService, track) } + GlobalScope.launch { FileHelper.saveTempTrackSuspended( + this@TrackerService, + track + ) } } } // update notification @@ -491,4 +558,21 @@ class TrackerService: Service(), CoroutineScope, SensorEventListener { */ + /* Simple queue that evicts older elements and holds an average */ + /* Credit: CircularQueue https://stackoverflow.com/a/51923797 */ + class SimpleMovingAverageQueue(var capacity: Int) : LinkedList() { + private var sum: Double = 0.0 + var average: Double = sum / capacity + override fun add(element: Double): Boolean { + if (this.size >= capacity) { + sum -= this.first + removeFirst() + } else { + sum += element + } + return super.add(element) + } + } + + } diff --git a/app/src/main/java/org/y20k/trackbook/core/WayPoint.kt b/app/src/main/java/org/y20k/trackbook/core/WayPoint.kt index ff26c50..c82652e 100644 --- a/app/src/main/java/org/y20k/trackbook/core/WayPoint.kt +++ b/app/src/main/java/org/y20k/trackbook/core/WayPoint.kt @@ -22,6 +22,7 @@ import android.os.Parcelable import androidx.annotation.Keep import com.google.gson.annotations.Expose import kotlinx.parcelize.Parcelize +import org.y20k.trackbook.helpers.LocationHelper /* @@ -44,7 +45,7 @@ data class WayPoint(@Expose val provider: String, constructor(location: Location) : this (location.provider, location.latitude, location.longitude, location. altitude, location.accuracy, location.time) /* Constructor using Location plus distanceToStartingPoint and numberSatellites */ - constructor(location: Location, distanceToStartingPoint: Float, numberSatellites: Int) : this (location.provider, location.latitude, location.longitude, location. altitude, location.accuracy, location.time, distanceToStartingPoint, numberSatellites) + constructor(location: Location, distanceToStartingPoint: Float) : this (location.provider, location.latitude, location.longitude, location. altitude, location.accuracy, location.time, distanceToStartingPoint, LocationHelper.getNumberOfSatellites(location)) /* Converts WayPoint into Location */ fun toLocation(): Location { diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt index dc71c90..289c051 100644 --- a/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt +++ b/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt @@ -22,11 +22,11 @@ import android.content.Context import android.content.pm.PackageManager import android.location.Location import android.location.LocationManager +import android.os.Bundle import android.os.SystemClock import androidx.core.content.ContextCompat import org.y20k.trackbook.Keys import org.y20k.trackbook.core.Track -import org.y20k.trackbook.core.WayPoint import java.util.* import kotlin.math.pow @@ -232,23 +232,17 @@ object LocationHelper { /* Calculate elevation differences */ - fun calculateElevationDifferences(previousLocation: Location?, location: Location, track: Track, altitudeSmoothingValue: Int): Pair { - // store current values - var positiveElevation: Double = track.positiveElevation - var negativeElevation: Double = track.negativeElevation - if (previousLocation != null && location.altitude != Keys.DEFAULT_ALTITUDE) { - val locationAltitudeCorrected: Double = calculateCorrectedAltitude(location, track, altitudeSmoothingValue) - val previousLocationAltitudeCorrected: Double = calculateCorrectedAltitude(previousLocation, track, altitudeSmoothingValue) - // get elevation difference and sum it up - val altitudeDifference: Double = locationAltitudeCorrected - previousLocationAltitudeCorrected + 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) { - positiveElevation = track.positiveElevation + altitudeDifference // upwards movement + track.positiveElevation += altitudeDifference // upwards movement } if (altitudeDifference < 0) { - negativeElevation = track.negativeElevation + altitudeDifference // downwards movement + track.negativeElevation += altitudeDifference // downwards movement } } - return Pair(positiveElevation, negativeElevation) + return track } @@ -260,29 +254,17 @@ object LocationHelper { } - /* Calculate a moving average taking into account previously recorded altitude values */ - private fun calculateCorrectedAltitude(location: Location, track: Track, altitudeSmoothingValue: Int): Double { - // add location to track - track.wayPoints.add(WayPoint(location)) - // get size of track - val trackSize: Int = track.wayPoints.size - // skip calculation if less than two waypoints available - if (trackSize < 2) return location.altitude - // get number of locations to be used in calculating the moving average - val numberOfLocationsUsedForSmoothing: Int = if (trackSize < altitudeSmoothingValue) { - trackSize + /* Get number of satellites from Location extras */ + fun getNumberOfSatellites(location: Location): Int { + val numberOfSatellites: Int + val extras: Bundle? = location.extras + if (extras != null && extras.containsKey("satellites")) { + numberOfSatellites = extras.getInt("satellites", 0) } else { - altitudeSmoothingValue + numberOfSatellites = 0 } - // add altitude values in range and calculate average - val mostRecentWaypointIndex: Int = trackSize - 1 - var altitudeSum: Double = 0.0 - for (i in mostRecentWaypointIndex..(mostRecentWaypointIndex - numberOfLocationsUsedForSmoothing)) { - altitudeSum = altitudeSum + track.wayPoints[i].altitude - } - return altitudeSum / numberOfLocationsUsedForSmoothing + return numberOfSatellites } - } diff --git a/app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt index 22c742e..a329bb8 100644 --- a/app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt +++ b/app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt @@ -49,8 +49,8 @@ object TrackHelper { /* Adds given locatiom as waypoint to track */ - fun addWayPointToTrack(context: Context, track: Track, location: Location, accuracyMultiplier: Int, altitudeSmoothingValue: Int, resumed: Boolean): Pair { - // get previous location + fun addWayPointToTrack(track: Track, location: Location, accuracyMultiplier: Int, resumed: Boolean): Pair { + // Step 1: Get previous location val previousLocation: Location? var numberOfWayPoints: Int = track.wayPoints.size @@ -69,90 +69,47 @@ object TrackHelper { previousLocation = track.wayPoints[numberOfWayPoints - 1].toLocation() } - // update duration + // Step 2: Update duration val now: Date = GregorianCalendar.getInstance().time val difference: Long = now.time - track.recordingStop.time track.duration = track.duration + difference track.recordingStop = now - // add only if recent and accurate and different + // Step 3: Add waypoint, ifrecent and accurate and different enough val shouldBeAdded: Boolean = (LocationHelper.isRecentEnough(location) && LocationHelper.isAccurateEnough(location, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY) && LocationHelper.isDifferentEnough(previousLocation, location, accuracyMultiplier)) - -// // Debugging for shouldBeAdded - remove for production -// val recentEnough: Boolean = LocationHelper.isRecentEnough(location) -// val accurateEnough: Boolean = LocationHelper.isAccurateEnough(location, locationAccuracyThreshold) -// val differentEnough: Boolean = LocationHelper.isDifferentEnough(previousLocation, location) -// val shouldBeAdded = recentEnough && accurateEnough && differentEnough -// if (!recentEnough && accurateEnough && differentEnough) { Toast.makeText(context, "Debug: Not recent enough", Toast.LENGTH_LONG).show() } -// else if (!accurateEnough && recentEnough && differentEnough) { Toast.makeText(context, "Debug: Not accurate enough", Toast.LENGTH_LONG).show() } -// else if (!differentEnough && recentEnough && accurateEnough) { Toast.makeText(context, "Debug: Not different enough", Toast.LENGTH_LONG).show() } -// else if (!recentEnough && !accurateEnough && differentEnough) { Toast.makeText(context, "Debug: Not recent and accurate enough", Toast.LENGTH_LONG).show() } -// else if (!recentEnough && !differentEnough && accurateEnough) { Toast.makeText(context, "Debug: Not recent and different enough", Toast.LENGTH_LONG).show() } -// else if (!accurateEnough && !differentEnough && recentEnough) { Toast.makeText(context, "Debug: Not accurate and different enough", Toast.LENGTH_LONG).show() } -// else { Toast.makeText(context, "Debug: bad location.", Toast.LENGTH_LONG).show() } - if (shouldBeAdded) { - // update distance (do not update if resumed -> we do not want to add values calculated during a recording pause) + // Step 3.1: Update distance (do not update if resumed -> we do not want to add values calculated during a recording pause) if (!resumed) { track.length = track.length + LocationHelper.calculateDistance(previousLocation, location) } - - // update altitude values + // Step 3.2: Update altitude values val altitude: Double = location.altitude if (altitude != 0.0) { - - // CASE: First location if (numberOfWayPoints == 0) { track.maxAltitude = altitude track.minAltitude = altitude } - - // CASE: Not first location else { - - // Step 1: Update altitude values if (altitude > track.maxAltitude) track.maxAltitude = altitude if (altitude < track.minAltitude) track.minAltitude = altitude - - // Step 2: Calculate and update elevation values (upwards / downwards movements) - val elevationDifferences: Pair = LocationHelper.calculateElevationDifferences(previousLocation, location, track, altitudeSmoothingValue) - // check if any differences were calculated - if (elevationDifferences != Pair(track.positiveElevation, track.negativeElevation)) { - // update elevation values (do not update if resumed -> we do not want to add values calculated during a recording pause) - if (!resumed) { - track.positiveElevation = elevationDifferences.first - track.negativeElevation = elevationDifferences.second - } - } - } } - - // toggle stop over status, if necessary + // Step 3.3: Toggle stop over status, if necessary if (track.wayPoints.size < 0) { track.wayPoints[track.wayPoints.size - 1].isStopOver = LocationHelper.isStopOver(previousLocation, location) } - // save number of satellites - val numberSatellites: Int - val extras = location.extras - if (extras != null && extras.containsKey("satellites")) { - numberSatellites = extras.getInt("satellites", 0) - } else { - numberSatellites = 0 - } - - // add current location as point to center on for later display + // Step 3.4: Add current location as point to center on for later display track.latitude = location.latitude track.longitude = location.longitude - // add location as new waypoint - track.wayPoints.add(WayPoint(location, distanceToStartingPoint = track.length, numberSatellites = numberSatellites)) + // Step 3.5: Add location as new waypoint + track.wayPoints.add(WayPoint(location = location, distanceToStartingPoint = track.length)) } - return Pair(track, shouldBeAdded) + return Pair(shouldBeAdded, track) }