Add route files, show overview

This commit is contained in:
Christoph Hagen
2025-08-21 20:26:22 +02:00
parent 43b761b593
commit 9ec207014c
14 changed files with 938 additions and 3 deletions

View 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
}
}