222 lines
5.8 KiB
Swift
222 lines
5.8 KiB
Swift
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
|
|
}
|
|
}
|