From 0b6829a4e61a5a94cfbf122953d8e466e603ec61 Mon Sep 17 00:00:00 2001 From: Eric Internicola Date: Fri, 19 Oct 2018 15:48:06 -0600 Subject: [PATCH 1/2] Some minor updates to the models (mostly for debugging) and added some more verification to the tests. --- GeoTrackKit/Core/Models/Analyze/Leg.swift | 78 +++++++++++++++++-- .../GeoTrackAnalyzerTests.swift | 17 ---- .../Core/GeoTrackStatisticsTests.swift | 62 ++++++++++++++- 3 files changed, 134 insertions(+), 23 deletions(-) diff --git a/GeoTrackKit/Core/Models/Analyze/Leg.swift b/GeoTrackKit/Core/Models/Analyze/Leg.swift index e030f8d..5120418 100644 --- a/GeoTrackKit/Core/Models/Analyze/Leg.swift +++ b/GeoTrackKit/Core/Models/Analyze/Leg.swift @@ -10,18 +10,25 @@ import CoreLocation /// This class keeps track of statistics for a Leg (ascent or descent) of a track. public class Stat { + + public static var zero: Stat { + return Stat() + } + /// Minimum altitude in meters - fileprivate(set) public var minimumAltitude: CLLocationDistance = 0 + fileprivate(set) public var minimumAltitude: CLLocationDistance = CLLocationDistanceMax /// Maximum altitude in meters fileprivate(set) public var maximumAltitude: CLLocationDistance = 0 /// The distance travelled fileprivate(set) public var distance: CLLocationDistance = 0 /// The maximum recorded speed (in meters per second) fileprivate(set) public var maximumSpeed: CLLocationSpeed = 0 + /// What is the change in vertical? public var verticalDelta: CLLocationDistance { return maximumAltitude - minimumAltitude } + fileprivate var initialized = false internal(set) public var direction: Direction = .unknown @@ -53,12 +60,28 @@ public class Stat { maximumSpeed = max(maximumSpeed, stat.maximumSpeed) initialized = initialized || stat.initialized } + } +// MARK: - Stat: CustomDebugStringConvertible + +extension Stat: CustomDebugStringConvertible { + + public var debugDescription: String { + return "\(direction.rawValue) - \(verticalDelta.int)m, " + + "Altitude: \(minimumAltitude.int)m - \(maximumAltitude.int)m, " + + "Distance: \(distance.int)m, Max Speed: \(maximumSpeed.int)" + } + +} + + /// This class keeps track of statistics for an entire Geo Track. It summarizes the stats of all of the legs that comprise it and it keeps trak of the number of runs. -public class TrackStat: Stat { +public class TrackStat { /// The number of "ski runs" (aka the number of times descended) public let runs: Int + /// The number of "legs" (ascent count + descent count) + public let legs: Int /// The total vertical ascent for the entire track public let verticalAscent: CLLocationDistance /// The total vertical descent for the entire track @@ -69,23 +92,36 @@ public class TrackStat: Stat { public let descentDistance: CLLocationDistance /// The total distance covered for this track public let totalDistance: CLLocationDistance + /// Minimum altitude in meters + public let minimumAltitude: CLLocationDistance + /// Maximum altitude in meters + public let maximumAltitude: CLLocationDistance + /// The maximum recorded speed (in meters per second) + public let maximumSpeed: CLLocationSpeed /// Initialize this TrackStat with the required properties. Generally you want to create this stat using the `summarize(from legs: [Leg])` factory creation function to create one of these objects. That function will compute all of the required fields and delegate to this initializer. /// /// - Parameters: /// - runs: the number of runs for the track + /// - legs: The number of legs for the track (number of ascents + descents) /// - ascent: the vertical ascent for the track /// - descent: the vertical descent for this track /// - ascentDistance: the total ascent distance for this track /// - descentDistance: the total descent distance for this track /// - totalDistance: the total distance for this track - init(runs: Int, ascent: CLLocationDistance, descent: CLLocationDistance, ascentDistance: CLLocationDistance, descentDistance: CLLocationDistance, totalDistance: CLLocationDistance) { + init(runs: Int, legs: Int, ascent: CLLocationDistance, descent: CLLocationDistance, + ascentDistance: CLLocationDistance, descentDistance: CLLocationDistance, totalDistance: CLLocationDistance, + minimumAltitude: CLLocationDistance, maximumAltitude: CLLocationDistance, maximumSpeed: CLLocationSpeed) { self.runs = runs + self.legs = legs verticalAscent = ascent verticalDescent = descent self.ascentDistance = ascentDistance self.descentDistance = descentDistance self.totalDistance = totalDistance + self.minimumAltitude = minimumAltitude + self.maximumAltitude = maximumAltitude + self.maximumSpeed = maximumSpeed } /// Using the provided array of Legs, this function will compute the track summary stats and provide you with a a summary TrackStat for the entire track. @@ -100,6 +136,9 @@ public class TrackStat: Stat { var aDistance: CLLocationDistance = 0 var dDistance: CLLocationDistance = 0 var tDistance: CLLocationDistance = 0 + var minAlt: CLLocationDistance = CLLocationDistanceMax + var maxAlt: CLLocationDistance = 0 + var maxSpeed: CLLocationSpeed = 0 for leg in legs { let stat = leg.stat @@ -113,12 +152,31 @@ public class TrackStat: Stat { runs += 1 } tDistance += stat.distance + minAlt = min(minAlt, stat.minimumAltitude) + maxAlt = max(maxAlt, stat.maximumAltitude) + maxSpeed = max(maxSpeed, stat.maximumSpeed) } - let stat = TrackStat(runs: runs, ascent: vAscent, descent: vDescent, ascentDistance: aDistance, descentDistance: dDistance, totalDistance: tDistance) - stat.combine(with: baseOverallStat) + let stat = TrackStat(runs: runs, legs: legs.count, ascent: vAscent, descent: vDescent, + ascentDistance: aDistance, descentDistance: dDistance, totalDistance: tDistance, + minimumAltitude: minAlt, maximumAltitude: maxAlt, maximumSpeed: maxSpeed) return stat } + +} + +// MARK: - Stat: CustomDebugStringConvertible + +extension TrackStat: CustomDebugStringConvertible { + + + public var debugDescription: String { + return "\(runs) runs, (\(legs) legs), " + + "Altitude: \(minimumAltitude.int)m - \(maximumAltitude.int)m, " + + "Vertical ascent: \(verticalAscent.int)m, Vertical descent: \(verticalDescent.int)m, " + + "Distance(ascent): \(ascentDistance.int)m, Distance(descent): \(descentDistance.int)m, " + + "Distance(total): \(totalDistance.int)m, Max Speed: \(maximumSpeed.int)m/s" + } } /// The direction that we're going @@ -266,3 +324,13 @@ extension Leg: Equatable { return true } } + +// MARK: - Number Helpers + +extension CLLocationDistance { + + var int: Int { + return Int(self) + } + +} diff --git a/GeoTrackKitExample/GeoTrackKitExampleTests/GeoTrackAnalyzerTests.swift b/GeoTrackKitExample/GeoTrackKitExampleTests/GeoTrackAnalyzerTests.swift index 1aa5e13..95c7ecd 100644 --- a/GeoTrackKitExample/GeoTrackKitExampleTests/GeoTrackAnalyzerTests.swift +++ b/GeoTrackKitExample/GeoTrackKitExampleTests/GeoTrackAnalyzerTests.swift @@ -28,23 +28,6 @@ class GeoTrackAnalyzerTests: XCTestCase { } } -// func testRewriteInOrder() { -// guard let track = track else { -// return XCTFail("No track") -// } -// -// guard let data = try? JSONSerialization.data(withJSONObject: track.map, options: .prettyPrinted) else { -// return XCTFail("Failed to serialize the track into data") -// } -// -// let path = "/tmp/out.json" -// do { -// try data.write(to: URL(fileURLWithPath: path)) -// } catch { -// XCTFail(error.localizedDescription) -// } -// } - func testAnalyze() { guard let track = track else { return XCTFail("No track") diff --git a/GeoTrackKitExample/GeoTrackKitExampleTests/GeoTrackKit/Core/GeoTrackStatisticsTests.swift b/GeoTrackKitExample/GeoTrackKitExampleTests/GeoTrackKit/Core/GeoTrackStatisticsTests.swift index 6218436..7c8de28 100644 --- a/GeoTrackKitExample/GeoTrackKitExampleTests/GeoTrackKit/Core/GeoTrackStatisticsTests.swift +++ b/GeoTrackKitExample/GeoTrackKitExampleTests/GeoTrackKit/Core/GeoTrackStatisticsTests.swift @@ -6,10 +6,70 @@ // Copyright © 2018 Eric Internicola. All rights reserved. // +import CoreLocation +@testable import GeoTrackKit import XCTest class GeoTrackStatisticsTests: XCTestCase { - + var track: GeoTrack? + + override func setUp() { + super.setUp() + guard let referenceTrack = TrackReader(filename: "reference-track-1", type: "json").track else { + return XCTFail("Failed to load the reference track") + } + track = referenceTrack + } + + func testVerifyLegStats() { + guard let track = track else { + return XCTFail("No reference track") + } + let analyzer = GeoTrackAnalyzer(track: track) + analyzer.calculate() + + guard let stats = analyzer.stats else { + return XCTFail("No stats from the track") + } + XCTAssertEqual(6, stats.runs, "Wrong number of runs") + XCTAssertEqual(12, stats.legs, "Wrong number of legs") + XCTAssertTrue(stats.ascentDistance > 0, "The ascent distance is wrong") + XCTAssertTrue(stats.descentDistance > 0, "The descent distance is wrong") + XCTAssertTrue(stats.totalDistance > 0, "The total distance is wrong") + XCTAssertTrue(stats.verticalAscent > 0, "The vertical ascent is wrong") + XCTAssertTrue(stats.verticalDescent < 0, "The vertical descent is wrong") + XCTAssertTrue(stats.totalDistance > 0, "The distance is wrong") + XCTAssertTrue(stats.maximumAltitude > 0, "The maximum altitude is wrong") + XCTAssertTrue(stats.maximumSpeed > 0, "The maximum speed is wrong") + XCTAssertTrue(stats.minimumAltitude < CLLocationDistanceMax, "The minimum altitude is wrong") + + let total = Stat.zero + var ascentDistance = CLLocationDistance(0) + var descentDistance = CLLocationDistance(0) + var verticalAscent = CLLocationDistance(0) + var verticalDescent = CLLocationDistance(0) + + for leg in analyzer.legs { + total.combine(with: leg.stat) + if leg.direction == .downward { + descentDistance += leg.stat.distance + verticalDescent += leg.stat.verticalDelta + } + if leg.direction == .upward { + ascentDistance += leg.stat.distance + verticalAscent += leg.stat.verticalDelta + } + } + + XCTAssertEqual(total.distance, stats.totalDistance, accuracy: 0.01) + XCTAssertEqual(total.maximumAltitude, stats.maximumAltitude, accuracy: 0.01) + XCTAssertEqual(total.minimumAltitude, stats.minimumAltitude, accuracy: 0.01) + XCTAssertEqual(total.maximumSpeed, stats.maximumSpeed, accuracy: 0.01) + XCTAssertEqual(ascentDistance, stats.ascentDistance, accuracy: 0.01) + XCTAssertEqual(descentDistance, stats.descentDistance, accuracy: 0.01) + XCTAssertEqual(verticalAscent, stats.verticalAscent, accuracy: 0.01) + XCTAssertEqual(abs(verticalDescent), abs(stats.verticalDescent), accuracy: 0.01) + } } From 64e577804b276244289377e46641f54d5108cab4 Mon Sep 17 00:00:00 2001 From: Eric Internicola Date: Fri, 19 Oct 2018 16:20:11 -0600 Subject: [PATCH 2/2] Updated the travis config to point to Xcode 10. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index dff2ce6..ba1a3a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: objective-c -osx_image: xcode9.4 +osx_image: xcode10 script: - bundle exec fastlane test