169 lines
6.2 KiB
Swift
169 lines
6.2 KiB
Swift
import CoreLocation
|
|
|
|
extension Array where Element == CLLocation {
|
|
|
|
var totalDistance: CLLocationDistance {
|
|
zip(self, dropFirst())
|
|
.map { $0.distance(from: $1) }
|
|
.reduce(0, +)
|
|
}
|
|
|
|
var duration: TimeInterval {
|
|
guard let start = first, let end = last else { return 0 }
|
|
return end.timestamp.timeIntervalSince(start.timestamp)
|
|
}
|
|
|
|
/**
|
|
Sample the locations using a given time interval.
|
|
*/
|
|
func samplePeriodically(at interval: TimeInterval) -> [CLLocation] {
|
|
guard interval > 0 else { return [] }
|
|
guard let start = first, let end = last else { return self }
|
|
let totalTime = end.timestamp.timeIntervalSince(start.timestamp)
|
|
let numberOfSamples = Int((totalTime / interval).rounded(.up))
|
|
return periodicSamples(interval: interval, numberOfSamples: numberOfSamples)
|
|
}
|
|
|
|
/**
|
|
Sample the locations at a fixed period determined by the number of desired sampels
|
|
*/
|
|
func samplePeriodically(numberOfSamples: Int) -> [CLLocation] {
|
|
guard numberOfSamples > 0 else { return [] }
|
|
guard let start = first, let end = last else { return self }
|
|
let totalTime = end.timestamp.timeIntervalSince(start.timestamp)
|
|
let timeInterval = totalTime / TimeInterval(count - 1)
|
|
return periodicSamples(interval: timeInterval, numberOfSamples: numberOfSamples)
|
|
}
|
|
|
|
private func periodicSamples(interval: TimeInterval, numberOfSamples: Int) -> [CLLocation] {
|
|
guard let start = first else { return [] }
|
|
var currentIndex = 0
|
|
var currentTime = start.timestamp
|
|
|
|
var samples = [start]
|
|
for _ in 1..<numberOfSamples {
|
|
currentTime = currentTime.addingTimeInterval(interval)
|
|
while true {
|
|
let nextIndex = currentIndex + 1
|
|
if nextIndex >= count { break }
|
|
let nextTime = self[nextIndex].timestamp
|
|
if nextTime > currentTime { break }
|
|
currentIndex += 1
|
|
}
|
|
if currentIndex + 1 == count {
|
|
samples.append(self[currentIndex])
|
|
} else {
|
|
let before = self[currentIndex]
|
|
let after = self[currentIndex + 1]
|
|
let interpolated = before.interpolate(currentTime, to: after)
|
|
samples.append(interpolated)
|
|
}
|
|
}
|
|
return samples
|
|
}
|
|
|
|
/// Computes path length by moving along center-to-center lines, intersecting uncertainty spheres
|
|
func minimumTraveledDistance3D() -> CLLocationDistance {
|
|
guard count > 1 else { return 0 }
|
|
|
|
// Remove the uncertainty radius of the first location
|
|
var current = self.first!
|
|
var totalDistance: CLLocationDistance = -current.uncertaintyRadius3D
|
|
for next in self[1...] {
|
|
let (movement, point) = current.minimumDistance(to: next)
|
|
current = point
|
|
totalDistance += movement
|
|
}
|
|
return totalDistance
|
|
}
|
|
|
|
/// Calculates the minimum possible ascended altitude (meters),
|
|
/// considering vertical accuracy as an uncertainty interval.
|
|
func minimumAscendedAltitude() -> CLLocationDistance {
|
|
guard let first = self.first else { return 0 }
|
|
|
|
// Start with the highest possible value of the first point
|
|
var currentAltitude = first.altitude + first.verticalAccuracy
|
|
var ascended: CLLocationDistance = 0
|
|
|
|
for next in self.dropFirst() {
|
|
let newMin = next.altitude - next.verticalAccuracy
|
|
let newMax = next.altitude + next.verticalAccuracy
|
|
|
|
if newMin > currentAltitude {
|
|
// Lower bound must be adjusted
|
|
ascended += newMin - currentAltitude
|
|
currentAltitude = newMin
|
|
} else if newMax < currentAltitude {
|
|
// Upper bound must be adjusted
|
|
currentAltitude = newMax
|
|
}
|
|
}
|
|
return ascended
|
|
}
|
|
|
|
/// Calculates the minimum possible ascended altitude (meters),
|
|
/// considering a given vertical accuracy threshold
|
|
func minimumAscendedAltitude(threshold: CLLocationDistance) -> CLLocationDistance {
|
|
guard let first = self.first else { return 0 }
|
|
|
|
// Start with the highest possible value of the first point
|
|
var currentAltitude = first.altitude + threshold
|
|
var ascended: CLLocationDistance = 0
|
|
|
|
for next in self.dropFirst() {
|
|
let newMin = next.altitude - threshold
|
|
let newMax = next.altitude + threshold
|
|
|
|
if newMin > currentAltitude {
|
|
// Lower bound must be adjusted
|
|
ascended += newMin - currentAltitude
|
|
currentAltitude = newMin
|
|
} else if newMax < currentAltitude {
|
|
// Upper bound must be adjusted
|
|
currentAltitude = newMax
|
|
}
|
|
}
|
|
return ascended
|
|
}
|
|
|
|
func interpolateAltitudes(
|
|
from startDate: Date,
|
|
to endDate: Date
|
|
) -> [CLLocation] {
|
|
|
|
// Ensure valid range
|
|
guard startDate < endDate else { return self }
|
|
|
|
// Find first and last locations in the window
|
|
guard
|
|
let startLocation = first(where: { $0.timestamp >= startDate }),
|
|
let endLocation = last(where: { $0.timestamp <= endDate })
|
|
else {
|
|
return self // No valid range found
|
|
}
|
|
|
|
let startAltitude = startLocation.altitude
|
|
let endAltitude = endLocation.altitude
|
|
let duration = endDate.timeIntervalSince(startDate)
|
|
|
|
return map { loc in
|
|
guard loc.timestamp >= startDate && loc.timestamp <= endDate else {
|
|
return loc // outside window, unchanged
|
|
}
|
|
let progress = (loc.timestamp.timeIntervalSince(startDate)) / duration
|
|
let newAltitude = startAltitude + progress * (endAltitude - startAltitude)
|
|
|
|
return CLLocation(
|
|
coordinate: loc.coordinate,
|
|
altitude: newAltitude,
|
|
horizontalAccuracy: loc.horizontalAccuracy,
|
|
verticalAccuracy: loc.verticalAccuracy,
|
|
course: loc.course,
|
|
speed: loc.speed,
|
|
timestamp: loc.timestamp
|
|
)
|
|
}
|
|
}
|
|
}
|