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

105 lines
4.2 KiB
Swift

import CoreLocation
extension CLLocation {
func duration(since other: CLLocation) -> TimeInterval {
duration(since: other.timestamp)
}
func duration(since other: Date) -> TimeInterval {
timestamp.timeIntervalSince(other)
}
func speed(from other: CLLocation) -> Double {
distance(from: other) / duration(since: other)
}
/// Combined uncertainty sphere radius (meters) from horizontal+vertical accuracy
var uncertaintyRadius3D: CLLocationDistance {
let h = max(0, horizontalAccuracy)
let v = max(0, verticalAccuracy)
return sqrt(h * h + v * v)
}
func verticalDistance(from other: CLLocation) -> CLLocationDistance {
abs(self.altitude - other.altitude)
}
func minimumDistance(to other: CLLocation) -> (distance: CLLocationDistance, point: CLLocation) {
let horizontalDistance = distance(from: other)
let horizontalMovement = Swift.max(0, horizontalDistance - Swift.max(0, other.horizontalAccuracy))
let latitude: CLLocationDegrees
let longitude: CLLocationDegrees
if horizontalDistance == 0 || horizontalMovement == 0 {
latitude = coordinate.latitude
longitude = coordinate.longitude
} else {
let horizontalRatio = horizontalMovement / horizontalDistance
latitude = coordinate.latitude.move(horizontalRatio, to: other.coordinate.latitude)
longitude = coordinate.longitude.move(horizontalRatio, to: other.coordinate.longitude)
}
let verticalDistance = verticalDistance(from: other)
let verticalMovement = Swift.max(0, verticalDistance - Swift.max(0, other.verticalAccuracy))
let altitude: CLLocationDistance
if verticalDistance == 0 || verticalMovement == 0 {
altitude = self.altitude
} else {
let verticalRatio = verticalMovement / verticalDistance
altitude = self.altitude.move(verticalRatio, to: other.altitude)
}
let movement = sqrt(horizontalMovement * horizontalMovement + verticalMovement * verticalMovement)
let point = CLLocation(
coordinate: .init(latitude: latitude, longitude: longitude),
altitude: altitude,
horizontalAccuracy: 0,
verticalAccuracy: 0,
timestamp: other.timestamp
)
return (movement, point)
}
func interpolate(_ time: Date, to other: CLLocation) -> CLLocation {
if self.timestamp > other.timestamp {
return other.interpolate(time, to: self)
}
let totalDuration = other.timestamp.timeIntervalSince(self.timestamp)
if totalDuration == 0 { return move(0.5, to: other) }
let ratio = time.timeIntervalSince(self.timestamp) / totalDuration
return move(ratio, to: other)
}
func move(_ ratio: Double, to other: CLLocation) -> CLLocation {
if ratio <= 0 { return self }
if ratio >= 1 { return other }
let time = timestamp.addingTimeInterval(other.timestamp.timeIntervalSince(timestamp) * ratio)
return CLLocation(
coordinate: .init(
latitude: coordinate.latitude.move(ratio, to: other.coordinate.latitude),
longitude: coordinate.longitude.move(ratio, to: other.coordinate.longitude)),
altitude: altitude.move(ratio, to: other.altitude),
horizontalAccuracy: move(from: horizontalAccuracy, to: other.horizontalAccuracy, by: ratio),
verticalAccuracy: move(from: verticalAccuracy, to: other.verticalAccuracy, by: ratio),
course: move(from: course, to: other.course, by: ratio),
courseAccuracy: move(from: courseAccuracy, to: other.courseAccuracy, by: ratio),
speed: move(from: speed, to: other.speed, by: ratio),
speedAccuracy: move(from: speedAccuracy, to: other.speedAccuracy, by: ratio),
timestamp: time)
}
private func move(from source: Double, to other: Double, by ratio: Double) -> Double {
if source == -1 {
return other
}
if other == -1 {
return source
}
return source.move(ratio, to: other)
}
}