Begin statistics creation

This commit is contained in:
Christoph Hagen
2025-08-31 16:27:32 +02:00
parent f972a2c020
commit 96bd07bdb7
33 changed files with 1406 additions and 187 deletions

View File

@@ -0,0 +1,13 @@
struct DataRanges {
let duration: RangeInterval
let time: RangeInterval
let distance: RangeInterval
}
extension DataRanges: Codable {
}

View File

@@ -0,0 +1,11 @@
struct RangeInterval {
let min: Double
let max: Double
}
extension RangeInterval: Codable {
}

View File

@@ -0,0 +1,26 @@
import Foundation
/**
All data needed to create statistic displays
*/
struct RouteData {
let series: RouteSeries
let ranges: DataRanges
let samples: [RouteSample]
}
extension RouteData: Codable {
func encoded(prettyPrinted: Bool = false) -> Data {
let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
if prettyPrinted {
encoder.outputFormatting.insert(.prettyPrinted)
}
return try! encoder.encode(self)
}
}

View File

@@ -0,0 +1,32 @@
struct RouteProfile: Codable {
let min: Double
let max: Double
let ticks: Int
let span: Double
let scale: Double
init(min: Double, max: Double, ticks: Int) {
self.min = min
self.max = max
self.ticks = ticks
self.span = max - min
self.scale = 1 / span
}
func scale(_ value: Double) -> Double {
if value < min {
return 0
}
if value > max {
return 1
}
return (value - min) / span
}
}

View File

@@ -0,0 +1,95 @@
struct RouteSample {
/// The x-coordinate in the map image (left to right) in the range [0,1]
var x: Double
/// The y-coordinate in the map image (top to bottom) in the range [0,1]
var y: Double
/// The timestamp of the sample in the range [0,1]
var time: Double
/// The distance of the sample in the range [0,1]
var distance: Double
/// The elevation of the sample in the range [0,1]
var elevation: Double
/// The speed of the sample in the range [0,1]
var speed: Double
/// The pace of the sample in the range [0,1]
var pace: Double
/// The heart rate of the sample in the range [0,1]
var hr: Double
/// The active energy rate of the sample in the range [0,1]
var energy: Double?
}
extension RouteSample: Codable {
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(x.rounded(decimals: 4), forKey: .x)
try container.encode(y.rounded(decimals: 4), forKey: .y)
try container.encode(time.rounded(decimals: 4), forKey: .time)
try container.encode(distance.rounded(decimals: 4), forKey: .distance)
try container.encode(elevation.rounded(decimals: 4), forKey: .elevation)
try container.encode(speed.rounded(decimals: 4), forKey: .speed)
try container.encode(pace.rounded(decimals: 4), forKey: .pace)
try container.encode(hr.rounded(decimals: 4), forKey: .hr)
try container.encodeIfPresent(energy?.rounded(decimals: 4), forKey: .energy)
}
}
extension RouteSample: Identifiable {
var id: Double { x }
}
extension RouteSample {
static var zero: RouteSample {
.init(x: 0, y: 0, time: 0, distance: 0, elevation: 0, speed: 0, pace: 0, hr: 0, energy: nil)
}
}
extension Collection where Element == RouteSample {
var averageSample: RouteSample {
guard var average = first else {
return .zero
}
var energySamples = average.energy == nil ? 0 : 1
for sample in dropFirst() {
average.x += sample.x
average.y += sample.y
average.time += sample.time
average.distance += sample.distance
average.elevation += sample.elevation
average.speed += sample.speed
average.pace += sample.pace
average.hr += sample.hr
if let energy = sample.energy {
average.energy = (average.energy ?? 0) + energy
energySamples += 1
}
}
let scale = 1 / Double(count)
average.x *= scale
average.y *= scale
average.time *= scale
average.distance *= scale
average.elevation *= scale
average.speed *= scale
average.pace *= scale
average.hr *= scale
if let energy = average.energy, energySamples > 0 {
average.energy = energy / Double(energySamples)
}
return average
}
}

View File

@@ -0,0 +1,18 @@
import Foundation
struct RouteSeries {
let elevation: RouteProfile
let speed: RouteProfile
let pace: RouteProfile
let hr: RouteProfile
let energy: RouteProfile?
}
extension RouteSeries: Codable {
}