201 lines
6.7 KiB
Swift
201 lines
6.7 KiB
Swift
|
|
enum SamplingMode {
|
|
case cumulative // e.g., distance, energy
|
|
case instantaneous // e.g., heart rate, altitude
|
|
}
|
|
|
|
struct Point: Identifiable, Equatable, Hashable {
|
|
|
|
var x, y: Double
|
|
|
|
var id: Double { x }
|
|
|
|
/**
|
|
Interpolate the y value at an x coordinate toward another point.
|
|
*/
|
|
func interpolate(to other: Point, at location: Double) -> Double {
|
|
let totalX = other.x - x
|
|
if totalX == 0 {
|
|
return (y + other.y) / 2
|
|
}
|
|
let diffX = location - x
|
|
if diffX == 0 {
|
|
return y
|
|
}
|
|
let ratio = diffX / totalX
|
|
return y + (other.y - y) * ratio
|
|
}
|
|
|
|
static let zero = Point(x: 0, y: 0)
|
|
}
|
|
|
|
extension Array where Element == Point {
|
|
|
|
func resample(numberOfSamples: Int, minX: Double? = nil, maxX: Double? = nil, mode: SamplingMode) -> [Point] {
|
|
guard count >= 2, numberOfSamples > 0 else { return [] }
|
|
let firstX = minX ?? first!.x
|
|
let lastX = maxX ?? last!.x
|
|
let totalDuration = lastX - firstX
|
|
guard totalDuration > 0 else { return [] }
|
|
|
|
let interval = totalDuration / Double(numberOfSamples)
|
|
var result: [Point] = .init(repeating: .zero, count: numberOfSamples)
|
|
|
|
var currentIndex = 0
|
|
var current = self[0]
|
|
|
|
for i in 0..<numberOfSamples {
|
|
let startOfInterval = Double(i) * interval
|
|
let endOfInterval = startOfInterval + interval
|
|
|
|
var accumulated = 0.0
|
|
var accumulatedDuration = 0.0
|
|
|
|
while currentIndex < count - 1 {
|
|
let next = self[currentIndex + 1]
|
|
|
|
if next.x <= endOfInterval {
|
|
let dt = next.x - current.x
|
|
switch mode {
|
|
case .cumulative:
|
|
accumulated += next.y - current.y
|
|
case .instantaneous:
|
|
accumulated += (current.y + next.y) / 2 * dt
|
|
accumulatedDuration += dt
|
|
}
|
|
currentIndex += 1
|
|
current = next
|
|
} else {
|
|
// partial segment at the interval end
|
|
let dt = endOfInterval - current.x
|
|
let slope = (next.y - current.y) / (next.x - current.x)
|
|
let interpolatedY = current.y + slope * dt
|
|
switch mode {
|
|
case .cumulative:
|
|
accumulated += interpolatedY - current.y
|
|
case .instantaneous:
|
|
accumulated += (current.y + interpolatedY) / 2 * dt
|
|
accumulatedDuration += dt
|
|
}
|
|
current = Point(x: endOfInterval, y: interpolatedY)
|
|
break
|
|
}
|
|
}
|
|
|
|
result[i].x = (startOfInterval + endOfInterval) / 2 // midpoint
|
|
switch mode {
|
|
case .cumulative:
|
|
result[i].y = accumulated / interval // average rate over interval
|
|
case .instantaneous:
|
|
result[i].y = accumulatedDuration > 0 ? accumulated / accumulatedDuration : current.y
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func resample(
|
|
numberOfSamples: Int,
|
|
minX: Double? = nil,
|
|
maxX: Double? = nil,
|
|
mode: SamplingMode
|
|
) -> [Double] {
|
|
guard count >= 2, numberOfSamples > 0 else { return [] }
|
|
let minX = minX ?? first!.x
|
|
let maxX = maxX ?? last!.x
|
|
let totalDuration = maxX - minX
|
|
guard totalDuration > 0 else { return [] }
|
|
|
|
let interval = totalDuration / Double(numberOfSamples)
|
|
var result = [Double](repeating: 0.0, count: numberOfSamples)
|
|
|
|
var currentIndex = 0
|
|
var current = self[0]
|
|
|
|
for i in 0..<numberOfSamples {
|
|
let startOfInterval = minX + Double(i) * interval
|
|
let endOfInterval = startOfInterval + interval
|
|
|
|
var accumulated = 0.0
|
|
var accumulatedDuration = 0.0
|
|
|
|
while currentIndex < count - 1 {
|
|
let next = self[currentIndex + 1]
|
|
|
|
if next.x <= endOfInterval {
|
|
let dt = next.x - current.x
|
|
switch mode {
|
|
case .cumulative:
|
|
accumulated += next.y - current.y
|
|
case .instantaneous:
|
|
accumulated += (current.y + next.y) / 2 * dt
|
|
accumulatedDuration += dt
|
|
}
|
|
currentIndex += 1
|
|
current = next
|
|
} else {
|
|
let dt = endOfInterval - current.x
|
|
let slope = (next.y - current.y) / (next.x - current.x)
|
|
let interpolatedY = current.y + slope * dt
|
|
switch mode {
|
|
case .cumulative:
|
|
accumulated += interpolatedY - current.y
|
|
case .instantaneous:
|
|
accumulated += (current.y + interpolatedY) / 2 * dt
|
|
accumulatedDuration += dt
|
|
}
|
|
current = Point(x: endOfInterval, y: interpolatedY)
|
|
break
|
|
}
|
|
}
|
|
|
|
let value: Double
|
|
switch mode {
|
|
case .cumulative:
|
|
value = accumulated / interval
|
|
case .instantaneous:
|
|
value = accumulatedDuration > 0 ? accumulated / accumulatedDuration : current.y
|
|
}
|
|
result[i] = value
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
|
|
extension Sequence {
|
|
/// Resamples any sequence of elements into evenly spaced intervals.
|
|
///
|
|
/// - Parameters:
|
|
/// - numberOfSamples: Number of output points
|
|
/// - xSelector: Closure to get the independent variable (e.g., time)
|
|
/// - ySelector: Closure to get the dependent variable (e.g., distance, heart rate)
|
|
/// - mode: .cumulative or .instantaneous
|
|
/// - Returns: Array of Points with evenly spaced `x` and averaged `y`
|
|
func resample(
|
|
numberOfSamples: Int,
|
|
x: (Element) -> Double,
|
|
y: (Element) -> Double,
|
|
mode: SamplingMode
|
|
) -> [Point] {
|
|
let points = self.map { Point(x: x($0), y: y($0)) }
|
|
return points.resample(numberOfSamples: numberOfSamples, mode: mode)
|
|
}
|
|
|
|
func resample(
|
|
numberOfSamples: Int,
|
|
minX: Double? = nil,
|
|
maxX: Double? = nil,
|
|
x: (Element) -> Double,
|
|
y: (Element) -> Double,
|
|
mode: SamplingMode
|
|
) -> [Double] {
|
|
let points = self.map { Point(x: x($0), y: y($0)) }
|
|
return points.resample(
|
|
numberOfSamples: numberOfSamples,
|
|
minX: minX,
|
|
maxX: maxX,
|
|
mode: mode)
|
|
}
|
|
|
|
}
|