Files
ChWebsiteApp/CHDataManagement/Workouts/Point.swift
2025-08-31 16:27:32 +02:00

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