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