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,115 @@
import Foundation
extension Double {
/**
Move to a different value by the given ratio of their distance.
*/
func move(_ ratio: Double, to other: Double) -> Double {
self + (other - self) * ratio
}
}
extension Collection where Element == Double {
func sum() -> Double {
reduce(0, +)
}
func average() -> Double {
sum() / Double(count)
}
}
extension Collection where Element == Double, Index == Int {
func floatingMeanFiltered(windowSize: Int) -> [Double] {
guard windowSize > 1 else {
return self.map { $0 }
}
guard count >= windowSize else {
return .init(repeating: average(), count: count)
}
let firstHalf = windowSize / 2
let secondHalf = windowSize - firstHalf
var minSpeed: Double = 0
var maxSpeed: Double = 0
let averageScale = 1.0 / Double(windowSize)
// First calculate the filtered speeds in the normal unit
var currentAverage = self[0..<windowSize].average()
var result: [Double] = .init(repeating: currentAverage, count: firstHalf + 1)
for index in firstHalf..<count-firstHalf-1 { // Index in self
let removed = self[index-firstHalf]
let added = self[index+secondHalf]
currentAverage += (added - removed) * averageScale
result.append(currentAverage)
if currentAverage < minSpeed { minSpeed = currentAverage }
else if currentAverage > maxSpeed { maxSpeed = currentAverage }
}
result.append(contentsOf: [Double](repeating: currentAverage, count: secondHalf))
return result
}
func fittingAxisLimits(desiredNumSteps: Int = 5, minLimit: Double? = nil, maxLimit: Double? = nil) -> RouteProfile {
let (dataMin, dataMax) = minMax(minLimit: minLimit, maxLimit: maxLimit)
let dataRange = dataMax - dataMin
let roughStep = dataRange / Double(desiredNumSteps)
let exponent = floor(log10(roughStep))
let base = pow(10.0, exponent)
let step: Double
if roughStep <= base {
step = base
} else if roughStep <= 2 * base {
step = 2 * base
} else if roughStep <= 5 * base {
step = 5 * base
} else {
step = 10 * base
}
let graphMin = floor(dataMin / step) * step
let graphMax = ceil(dataMax / step) * step
let numTicks = Int((graphMax - graphMin) / step)
return .init(min: graphMin, max: graphMax, ticks: numTicks)
}
func minMax(minLimit: Double? = nil, maxLimit: Double? = nil) -> (min: Double, max: Double) {
var dataMin = self.min() ?? 0
var dataMax = self.max() ?? 1
if let minLimit {
dataMin = Swift.max(dataMin, minLimit)
}
if let maxLimit {
dataMax = Swift.min(dataMax, maxLimit)
}
return (dataMin, dataMax)
}
func fittingTimeAxisLimits(minTicks: Int = 3, maxTicks: Int = 7, minLimit: Double? = nil, maxLimit: Double? = nil) -> RouteProfile {
let (dataMin, dataMax) = minMax(minLimit: minLimit, maxLimit: maxLimit)
let dataRange = dataMax - dataMin
let niceSteps: [Double] = [15, 30, 60, 120, 300, 600, 900, 1800, 3600] // in seconds
// Find the step size that gives a nice number of ticks
var chosenStep = niceSteps.last! // fallback to largest if none fit
for step in niceSteps {
let numTicks = dataRange / step
if Double(minTicks) <= numTicks && numTicks <= Double(maxTicks) {
chosenStep = step
break
}
}
let graphMin = floor(dataMin / chosenStep) * chosenStep
let graphMax = ceil(dataMax / chosenStep) * chosenStep
let numTicks = Int((graphMax-graphMin) / chosenStep)
return .init(min: graphMin, max: graphMax, ticks: numTicks)
}
}