Add route files, show overview
This commit is contained in:
221
CHDataManagement/Workouts/WorkoutData.swift
Normal file
221
CHDataManagement/Workouts/WorkoutData.swift
Normal file
@@ -0,0 +1,221 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user