import Foundation import CoreLocation import BinaryCodable private struct TrackLocation { let timestamp: TimeInterval let latitude: Double let longitude: Double let speed: Double? let speedAccuracy: Double? let course: Double? let courseAccuracy: Double? let elevation: Double let horizontalAccuracy: Double? let verticalAccuracy: Double? init( timestamp: TimeInterval, latitude: Double, longitude: Double, speed: Double, speedAccuracy: Double? = nil, course: Double, courseAccuracy: Double? = nil, elevation: Double, horizontalAccuracy: Double? = nil, verticalAccuracy: Double? = nil ) { self.timestamp = timestamp self.latitude = latitude self.longitude = longitude self.speed = speed self.speedAccuracy = speedAccuracy self.course = course self.courseAccuracy = courseAccuracy self.elevation = elevation self.horizontalAccuracy = horizontalAccuracy self.verticalAccuracy = verticalAccuracy } init(location: CLLocation) { self.timestamp = location.timestamp.timeIntervalSince1970 self.elevation = location.altitude self.latitude = location.coordinate.latitude self.longitude = location.coordinate.longitude self.speed = location.speed self.speedAccuracy = location.speedAccuracy self.course = location.course self.courseAccuracy = location.courseAccuracy self.horizontalAccuracy = location.horizontalAccuracy self.verticalAccuracy = location.verticalAccuracy } var location: CLLocation { .init( coordinate: .init( latitude: latitude, longitude: longitude), altitude: elevation, horizontalAccuracy: horizontalAccuracy ?? -1, verticalAccuracy: verticalAccuracy ?? -1, course: course ?? -1, courseAccuracy: courseAccuracy ?? -1, speed: speed ?? -1, speedAccuracy: speedAccuracy ?? -1, timestamp: .init(timeIntervalSince1970: timestamp)) } } extension TrackLocation: Codable { enum CodingKeys: Int, CodingKey { case timestamp = 1 case latitude case longitude case speed case speedAccuracy case course case courseAccuracy case elevation case horizontalAccuracy case verticalAccuracy } } extension WorkoutData { struct Sample { /// The unix time let timestamp: TimeInterval let value: Double init(timestamp: TimeInterval, value: Double) { self.timestamp = timestamp self.value = value } var time: Date { .init(timeIntervalSince1970: timestamp) } } } extension WorkoutData.Sample: Codable { enum CodingKeys: Int, CodingKey { case timestamp = 1 case value } } struct WorkoutData { let locations: [CLLocation] let heartRates: [Sample] /// The active energy in kcal let energy: [Sample] init(locations: [CLLocation], heartRates: [Sample], energy: [Sample]) { self.locations = locations self.heartRates = heartRates self.energy = energy } func encoded() throws -> Data { let encoder = BinaryEncoder() return try encoder.encode(self) } init(url: URL) throws { let data = try Data(contentsOf: url) try self.init(data: data) } init(data: Data) throws { let decoder = BinaryDecoder() self = try decoder.decode(WorkoutData.self, from: data) } /// The total active energy in kcal var totalEnergy: Double { energy.reduce(0) { $0 + $1.value } } /// The total distance of the track in meters var totalDistance: CLLocationDistance { locations.minimumTraveledDistance3D() } /// The total duration in seconds var totalDuration: TimeInterval { guard let start, let end else { return 0 } return end.timeIntervalSince(start) } /// The total ascended altitude in meters var totalAscendedElevation: CLLocationDistance { locations.minimumAscendedAltitude(threshold: 15) } var overview: RouteOverview { .init(energy: totalEnergy, distance: totalDistance, duration: totalDuration, ascendedElevation: totalAscendedElevation, start: start, end: end) } var start: Date? { let starts: [Date?] = [ locations.first?.timestamp, heartRates.first?.time, energy.first?.time] return starts.compactMap { $0 }.min() } var end: Date? { let ends: [Date?] = [locations.last?.timestamp, heartRates.last?.time, energy.last?.time] return ends.compactMap { $0 }.max() } func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(locations.map(TrackLocation.init), forKey: .locations) try container.encode(heartRates, forKey: .heartRates) try container.encode(energy, forKey: .energy) } init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.locations = try container.decode([TrackLocation].self, forKey: .locations).map { $0.location } self.heartRates = try container.decode([Sample].self, forKey: .heartRates) self.energy = try container.decode([Sample].self, forKey: .energy) } } extension WorkoutData: Codable { enum CodingKeys: Int, CodingKey { case locations = 1 case heartRates case energy } }