HealthKit Running Splits In Kilometres Code Inaccurate – Why? - swift

So below is the code that I've got thus far, cannot figure out why I'm getting inaccurate data.
Not accounting for the pause events yet that should not affect the first two kilometre inaccuracies...
So the output would be the distance 1km and the duration that km took.
Any ideas for improvement, please help?
func getHealthKitWorkouts(){
print("HealthKit Workout:")
/* Boris here: Looks like we need some sort of Health Kit manager */
let healthStore:HKHealthStore = HKHealthStore()
let durationFormatter = NSDateComponentsFormatter()
var workouts = [HKWorkout]()
// Predicate to read only running workouts
let predicate = HKQuery.predicateForWorkoutsWithWorkoutActivityType(HKWorkoutActivityType.Running)
// Order the workouts by date
let sortDescriptor = NSSortDescriptor(key:HKSampleSortIdentifierStartDate, ascending: false)
// Create the query
let sampleQuery = HKSampleQuery(sampleType: HKWorkoutType.workoutType(), predicate: predicate, limit: 0, sortDescriptors: [sortDescriptor])
{ (sampleQuery, results, error ) -> Void in
if let queryError = error {
print( "There was an error while reading the samples: \(queryError.localizedDescription)")
}
workouts = results as! [HKWorkout]
let target:Int = 0
print(workouts[target].workoutEvents)
print("Energy ", workouts[target].totalEnergyBurned)
print(durationFormatter.stringFromTimeInterval(workouts[target].duration))
print((workouts[target].totalDistance!.doubleValueForUnit(HKUnit.meterUnit())))
self.coolMan(workouts[target])
self.coolManStat(workouts[target])
}
// Execute the query
healthStore.executeQuery(sampleQuery)
}
func coolMan(let workout: HKWorkout){
let expectedOutput = [
NSTimeInterval(293),
NSTimeInterval(359),
NSTimeInterval(359),
NSTimeInterval(411),
NSTimeInterval(810)
]
let healthStore:HKHealthStore = HKHealthStore()
let distanceType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)
let workoutPredicate = HKQuery.predicateForObjectsFromWorkout(workout)
let startDateSort = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
let query = HKSampleQuery(sampleType: distanceType!, predicate: workoutPredicate,
limit: 0, sortDescriptors: [startDateSort]) {
(sampleQuery, results, error) -> Void in
// Process the detailed samples...
if let distanceSamples = results as? [HKQuantitySample] {
var count = 0.00, countPace = 0.00, countDistance = 0.0, countPacePerMeterSum = 0.0
var countSplits = 0
var firstStart = distanceSamples[0].startDate
let durationFormatter = NSDateComponentsFormatter()
print("🕒 Time Splits: ")
for (index, element) in distanceSamples.enumerate() {
count += element.quantity.doubleValueForUnit(HKUnit.meterUnit())
/* Calculate Pace */
let duration = ((element.endDate.timeIntervalSinceDate(element.startDate)))
let distance = distanceSamples[index].quantity
let pacePerMeter = distance.doubleValueForUnit(HKUnit.meterUnit()) / duration
countPace += duration
countPacePerMeterSum += pacePerMeter
if count > 1000 {
/* Account for extra bits */
let percentageUnder = (1000 / count)
//countPace = countPace * percentageUnder
// 6.83299013038 * 2.5
print("👣 Reached Kilometer \(count) ")
// MARK: Testing
let testOutput = durationFormatter.stringFromTimeInterval(NSTimeInterval.init(floatLiteral: test)),
testOutputExpected = durationFormatter.stringFromTimeInterval(expectedOutput[countSplits])
print(" Output Accuracy (", round(test - expectedOutput[countSplits]) , "): expected \(testOutputExpected) versus \(testOutput)")
print(" ", firstStart, " until ", element.endDate)
/* Print The Split Time Taken */
firstStart = distanceSamples[index].endDate;
count = (count % 1000) //0.00
countPace = (count % 1000) * pacePerMeter
countSplits++
/* Noise
\(countSplits) – \(count) – Pace \(countPace) – Pace Per Meter \(pacePerMeter) – Summed Pace Per Meter \(countPacePerMeterSum) – \(countPacePerMeterSum / Double.init(index))"
*/
}
/* Account for the last entry */
if (distanceSamples.count - 1 ) == index {
print("We started a kilometer \(countSplits+1) – \(count)")
let pacePerKM = (count / countPace) * 1000
print(durationFormatter.stringFromTimeInterval(NSTimeInterval.init(floatLiteral: (pacePerKM ))))
}
}
}else {
// Perform proper error handling here...
print("*** An error occurred while adding a sample to " + "the workout: \(error!.localizedDescription)")
abort()
}
}
healthStore.executeQuery(query)
}
func coolManStat(let workout: HKWorkout){
let healthStore:HKHealthStore = HKHealthStore()
let stepsCount = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)
let sumOption = HKStatisticsOptions.CumulativeSum
let statisticsSumQuery = HKStatisticsQuery(quantityType: stepsCount!, quantitySamplePredicate: HKQuery.predicateForObjectsFromWorkout(workout),
options: sumOption)
{ (query, result, error) in
if let sumQuantity = result?.sumQuantity() {
let numberOfSteps = Int(sumQuantity.doubleValueForUnit(HKUnit.meterUnit()))/1000
print("👣 Right -O: ",numberOfSteps)
}
}
healthStore.executeQuery(statisticsSumQuery)
}

I'm sure you're past this problem by now, more than two years later! But I'm sure someone else will come across this thread in the future, so I thought I'd share the answer.
I started off with a version of your code (many thanks!!) and encountered the same problems. I had to make a few changes. Not all of those changes are related to the issues you were seeing, but in any case, here are all of the considerations I've thought of so far:
Drift
You don't handle the 'drift', although this isn't what's causing the big inaccuracies in your output. What I mean is that your code is saying:
if count > 1000
But you don't do anything with the remainder over 1000, so your kilometre time isn't for 1000m, it's for, let's say, 1001m. So your time both is inaccurate for the current km, and it's including some of the running from the next km, so that time will be wrong too. Over a long run, this could start to cause noticeable problems. But it's not a big deal over short runs as I don't think the difference is significant enough at small distances. But it's definitely worth fixing. In my code I'm assuming that the runner was moving at a constant pace during the current sample (which is obviously not perfect, but I don't think there's a better way), and I'm then simply finding the fraction of the current sample distance that puts the split distance over 1000m, and getting that same fraction of the current sample's duration and removing it from the current km's time, and adding it (and the distance) to the next split.
GPS drops
The real problem with your results is that you don't handle GPS drops. The way I'm currently handling this is to compare the startDate of the current sample with the endDate of the previous sample. If they're not the same then there was a GPS drop. You need to add the difference between the previous endDate and the current startDate to the current split. Edit: you also need to do this with the startDate of the activity and the startDate of the first sample. There will be a gap between these 2 dates while GPS was connecting.
Pauses
There's a slight complication to the above GPS dropping problem. If the user has paused the workout then there will also be a difference between the current sample's startDate and the previous sample's endDate. So you need to be able to detect that and not adjust the split in that case. However, if the user's GPS dropped and they also paused during that time then you'll need to subtract the pause time from the missing time before adding it to the split.
Unfortunately, my splits are still not 100% in sync with the Apple Workouts app. But they've gone from being potentially minutes off to being mostly within 1 second. The worst I've seen is 3 seconds. I've only been working on this for a couple of hours, so I plan to continue trying to get 100% accuracy. I'll update this answer if I get that. But I believe I've covered the major problems here.

Related

Swift Charts delay for realtime data

I am using Swift 5 with Charts 3.6.0 ( Line Chart - cubic lines ) to plot real-time watchOS core motion. The goal is to display watch movement as quickly as possible. Since my sample rate is high, I suspect there will be a bottleneck in updating the view, and as such, would only like to display N most recent items.
Here is the watch function that sends the data immediately, as mentioned in Apple docs, and numerous tutorials:
motion.deviceMotionUpdateInterval = 1.0 / 120.0
motion.startDeviceMotionUpdates(using: .xArbitraryZVertical, to: queue) { [self] (deviceMotion: CMDeviceMotion?, _ : Error?) in
guard let motion = deviceMotion else { return }
self.sendDataToPhone(quaternion: motion.attitude.quaternion, time: Double(Date().timeIntervalSince1970))
}
private func sendDataToPhone(quaternion: CMQuaternion, time: Double) {
if WCSession.default.isReachable {
WCSession.default.sendMessageData(try! NSKeyedArchiver.archivedData(withRootObject: [quaternion.x, quaternion.y, quaternion.z, quaternion.w, time], requiringSecureCoding: false), replyHandler: nil, errorHandler: nil);
}
}
Once received, the packets are interpreted by the session() function on the iPhone:
onViewDidLoad() {
self.lineChartView.leftAxis.axisMinimum = -1;
self.lineChartView.leftAxis.axisMaximum = 1;
}
func session(_ session: WCSession, didReceiveMessageData messageData: Data) {
let record : [Double] = try! NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self], from: messageData) as! [Double]
laggyFunction(qaternions: [simd_quatd.init(ix: record[0], iy: record[1], iz: record[2], r: record[3])], quaternionTimes: [record[4]])
}
private func laggyFunction(qaternions: [simd_quatd], quaternionTimes: [Double]) {
DispatchQueue.main.sync {
let dataset = self.lineChartView.data!.getDataSetByIndex(0)!
var x = dataset.entryCount + 1;
for quaternion in qaternions {
let _ = dataset.addEntry(ChartDataEntry(x: Double(x), y: quaternion.vector.w))
x += 1;
}
// limit the amount of points
while (dataset.entryCount > 5) {
let _ = dataset.removeFirst()
}
// - re index so entries start from 1
for startIdx in 1..<dataset.entryCount {
dataset.entryForIndex(startIdx - 1)!.x = Double(startIdx);
}
self.lineChartView.data!.notifyDataChanged()
self.lineChartView.notifyDataSetChanged()
}
}
Logic flow:
As packets come in, new entries are added to the initial dataset from the lineChartView. In the event there are more than 5, first N are removed. Then, the x values are re-indexed on the chart to ensure a sequential flow.
The problem:
The delay in updating the UI chart is very high. The elapsed time of both functions to complete is plotted below. At the 73rd percentile, the laggy function is able to accommodate the sample rate of incoming packets ( < 1/120 = 0.008) . The session function seems to accommodate the sample rate throughout. The CDF plot, in my opinion, does not do it justice. Visually, the chart is very "sluggish". As an experiment, if I throw the watch against the wall, I can observe it hitting the concrete well before the chart is updated.
My goal is to update the Chart as quickly as possible to observe watch motion and discard new entries until the UI is updated. What is the correct way to do this with my chart choice?

Timer initialised in 'for in' loop firing twice for each loop

I'm building an application that displays a custom driving route on a map using MapKit. It takes an array of coordinates and goes through a for loop, in each loop the corresponding coordinates (i.e [0] and [1] or [7] and [8]) are assembled into an individual request and then drawn on the map.
In order to bypass MapKit's throttling error, I have a timer that is set so that each request is spaced out 1 second apart.
My issue is that the timer is firing twice for each individual request, which is resulting in double the number of necessary requests being made.
I'm using Xcode 10 and Swift 4, this is the function where I believe the issue is occurring.
func requestDirections(arrays coordinates: [CLLocationCoordinate2D]) {
var dest = 1
let coordinatesArray = coordinates
let end = coordinatesArray.count - 2
var timerDelay:Double = 1
for origin in 0...end {
if dest <= coordinatesArray.count {
let startCoord = MKPlacemark(coordinate: coordinatesArray[origin])
let destCoord = MKPlacemark(coordinate: coordinatesArray[dest])
let request = MKDirections.Request()
request.source = MKMapItem(placemark: startCoord)
request.destination = MKMapItem(placemark: destCoord)
request.transportType = .automobile
request.requestsAlternateRoutes = false
print("Starting timer for \(dest) at \(timerDelay) seconds")
Timer.scheduledTimer(withTimeInterval: timerDelay, repeats: false) { timer in
self.createIndividualDirectionsRequest(request)
print("Timer fired")
}
dest = dest + 1
timerDelay = timerDelay + 1
}
}
}
I'm expecting the timer to fire once for each loop, if this is happening the expected console output would be
"Starting timer for 'dest' at 'timerDelay' seconds" printed 18 times (or whatever the size of the array is)
"Timer fired" being printed 18 times as well
While "Starting timer for 'dest' at 'timerDelay' seconds" is in fact being printed the correct number of times, "Timer fired" is being printed twice as often as it should.
Thank you very much for your help and your patience, I am quite new to programming and am struggling to wrap my head around this issue.
Actually I do not know why "Timer fired" is printed twice for each individual request :).
But if you are using Timer.scheduledTimer just to delay the execution of the block, you can use DispatchQueue.main.asyncAfter instead of Timer
DispatchQueue.main.asyncAfter(deadline: .now() + timerDelay) {
self.createIndividualDirectionsRequest(request)
print("Timer fired")
}
You can also use DispatchQueue.global.asyncAfter but I think you want to execute the block in main ui thread

Very slow operation due to lack of GroupBy and Sum in Realm [iOS]

I am trying to build an accounting app where there can be many accounts and each account can have many entries. Each entry has date and amount.
Entries are linked to the Account class like so:
let entries = LinkingObjects(fromType: Entry.self, property: "account").sorted(byKeyPath: #keyPath(Entry.date))
I would like to group by Account name, have sum of amount for each month between any given dates. Dates can vary for reporting purposes.
Realm doesn't have groupby function I cannot easily get a result where the columns are account name, total, average, sumOf(month1), sumOf(month2) etc etc
Therefore; I need to do it in code, but the result is very slow. Is there anything realm specific missing from my code that would dramatically improve the speed of the calculations?
This code is being run for each account and for each period (for a yearly report that means 12 times for each account) and cause of the slowness:
let total: Double = realm
.objects(Entry.self)
.filter("_account.fullPath == %#", account.fullPath)
.filter("date >= %# AND date <= %#", period.startDate, period.endDate)
.filter("isConsolidation != TRUE")
.sum(ofProperty: "scaledAmount")
Below is the complete code for I have to loop each account and run a query for each period:
private func getMonthlyValues(for accounts: [Account], in realm: Realm, maxLevel: Int) -> String {
guard accounts.count > 0 else { return "" }
guard accounts[0].displayLevel < maxLevel else { return "" }
var rows: String = ""
for account in accounts.sorted(by: { $0.fullPath < $1.fullPath }) {
if account.isFolder {
let row = [account.localizedName.htmlColumn].htmlRow
rows += row
rows += monthlyValues(for: Array(account.children), in: realm, maxLevel: maxLevel)
} else {
var row: [String] = []
var totals: [Double] = []
// period has a start date and an end date
// monthlyPeriods returns the period in months
// for a datePeriod starting February 10, and ending in November 20 (for whatever reason)
// monthlyPeriods returns 10 periods for each month starting from [February 10 - February 28],
// and ending [November 1 - November 20]
// below gets the some for the given period for the current account
// for this example it runs 10 times for each account getting the sum for each period
for period in datePeriod.monthlyPeriods {
let total: Double = realm
.objects(Entry.self)
.filter("_account.fullPath == %#", account.fullPath)
.filter("date >= %# AND date <= %#", period.startDate, period.endDate)
.filter("isConsolidation != TRUE")
.sum(ofProperty: "scaledAmount")
totals.append(total)
}
let total = totals.reduce(0, +)
guard total > 0 else { continue }
let average = AccountingDouble(total / Double(totals.count)).value
row.append("\(account.localizedName.htmlColumn)")
row.append("\(total.htmlFormatted().htmlColumn)")
row.append("\(average.htmlFormatted().htmlColumn)")
row.append(contentsOf: totals.map { $0.htmlFormatted().htmlColumn })
rows += row.htmlRow
}
}
return rows
}
Any help is much appreciated.
Since you are not using the auto updating property of Results, what I would try is not running different queries for each month, but getting all entries using a single query, then doing the filtering and summing using Swift's built in filter and reduce functions, this way you can reduce the overhead of Realm queries.
Just have let entries = realm.objects(Entry.self) to store all entries using a single query, then you can filter this using Array(entries).filter({$0.account.fullPath == account.fullPath}), etc. for each account and date and this won't query Realm each time you do the calculation. If your entries won't change between calculations, you can even cast entries to Array right away using let entries = Array(realm.objects(Entry.self))

Getting stuck with repeat statement

I am trying to delete all old coordinates from an array from the condition that the coordinates are too far away from my current location. However the repeat statement gets stuck on repeat, deleting all the coordinates.
var locationArray = [Double]()
var distArray = [CLLocationDistance]()
let maxDis: CLLocationDistance = CLLocationDistance(exactly: 2000)!
let LAT = Double(location.coordinate.latitude)
let LONG = Double(location.coordinate.longitude)
repeat{
locationArray.insert(contentsOf: [LAT, LONG], at: 0)
} while locationArray.count <= 4
let oldCo = CLLocation(latitude: LAT, longitude: LONG)
let newlat = locationArray[2]
let newlong = locationArray[3]
let newCo = CLLocation(latitude: newlat, longitude: newlong)
let dist = newCo.distance(from: oldCo)
distArray.append(dist)
let distArraySum = (distArray.reduce(0) { $0 + $1 })
print(distArraySum)
repeat{
if distArraySum >= maxDis {
locationArray.remove(at: locationArray.count-2)
locationArray.remove(at: locationArray.count-1)
distArray.remove(at: 0)
print("deleted Coordinates")
}
} while distArraySum >= maxDis
The app terminates because I am getting stuck on my repeat statement, and every element in the distArray gets removed until there are no more elements. So when it repeats again and I try to remove an element from an empty array, I get a fatal error.
Once you calculate disArraySum you never recalculate it within the repeat loop. The result of distArraySum >= maxDis will never change and so your loop will either always execute or never execute, but it will never dynamically change state.
Additionally, you might want to consider changing locationArray to an array of tuples; it looks like you're interleaving lat and lon, it would be easier conceptually e.g.:
var locationArray:[(lat: Double, lon: Double)] = []
This will let you avoid having to do
locationArray.remove(at: locationArray.count-2)
locationArray.remove(at: locationArray.count-1)
And while we're at it, if I can make a suggestion to use for each instead, then the final loop might look like the following untested code:
for (i, coordinates) in locationArray.enumerate().reverse() {
if (some condition using coordinates.lat and coordinates.lon) {
locationArray.removeAtIndex(i)
}
}

How to get different random delays in a SpriteKit sequence?

I have a sequence where i spawn a obstacle and then wait for a random amount of time, but if I run the game and for example the first random delay 1.4 seconds, but its not just for the first delay it's just all the time 1.4 and it doesn't change (it doesn't have to be 1.4 it's just an example). I have tried to make a function which has a random return value but its doesn't work. I have no idea how i could solve this. Here's my Code for the function with the random return value. If it helps obstSwitch() is the function that creates the Obstacle:
func getRandomDelay() ->Double {
let randomNumber = arc4random_uniform(20) + 5
let randomDelay: Double = Double(randomNumber) / 10
print(randomDelay)
return randomDelay
}
and heres the function that get's called when the game started:
func gameStarted() {
gameAbleToStart = false
moveLine()
scoreTimer()
let runObstSwitch = SKAction.run {
self.obstSwitch()
}
let wait = SKAction.wait(forDuration: getRandomDelay())
let sequence = SKAction.sequence([runObstSwitch, wait])
self.run(SKAction.repeatForever(sequence))
}
let wait = SKAction.wait(forDuration: getRandomDelay())
let sequence = SKAction.sequence([runObstSwitch, wait])
creates the wait action once, which is then used in the sequence,
so the same amount of idle time is spent between the runObstSwitch
actions.
If you want the idle time to be variable, use
wait(forDuration:withRange:) instead. For example with
let wait = SKAction.wait(forDuration: 1.5, withRange: 2.0)
let sequence = SKAction.sequence([runObstSwitch, wait])
the delay will be a random number between 1.5-2.0/2 = 0.5 and 1.5+2.0/2 = 2.5 seconds, varying for each execution.