116 lines
3.9 KiB
Swift
116 lines
3.9 KiB
Swift
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)
|
|
}
|
|
|
|
}
|