learning_ai_clock/ios/ChronoMind/Shared/Location/TravelTimeManager.swift

206 lines
6.4 KiB
Swift

// Travel Time Manager
// MapKit-based travel time estimates for timer pre-warnings
// Auto-adjusts cascade warnings based on real-time travel time
import Foundation
import MapKit
import Combine
@MainActor
final class TravelTimeManager: ObservableObject {
static let shared = TravelTimeManager()
@Published var cachedEstimates: [String: TravelEstimate] = [:]
private init() {}
// MARK: - Travel Time Calculation
/// Calculate travel time between current location and a destination
func estimateTravelTime(
to destination: CLLocationCoordinate2D,
transportType: MKDirectionsTransportType = .automobile,
departureDate: Date? = nil
) async -> TravelEstimate? {
guard let currentLocation = await LocationTriggerManager.shared.getCurrentLocation() else {
return nil
}
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: currentLocation.coordinate))
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: destination))
request.transportType = transportType
request.departureDate = departureDate
let directions = MKDirections(request: request)
do {
let response = try await directions.calculateETA()
let estimate = TravelEstimate(
travelTimeSeconds: response.expectedTravelTime,
distance: response.distance,
transportType: transportType,
calculatedAt: Date(),
departureDate: departureDate ?? Date(),
arrivalDate: Date().addingTimeInterval(response.expectedTravelTime)
)
return estimate
} catch {
return nil
}
}
/// Calculate travel time for a timer with a known destination
func estimateForTimer(
timerId: String,
destination: CLLocationCoordinate2D,
transportType: MKDirectionsTransportType = .automobile,
targetTime: Date
) async -> TravelEstimate? {
// Calculate when to leave to arrive on time
guard let estimate = await estimateTravelTime(
to: destination,
transportType: transportType,
departureDate: targetTime.addingTimeInterval(-3600) // estimate departure ~1h before
) else { return nil }
var result = estimate
result.timerId = timerId
result.leaveByTime = targetTime.addingTimeInterval(-estimate.travelTimeSeconds)
// Cache it
cachedEstimates[timerId] = result
return result
}
/// Generate adjusted cascade intervals based on travel time
func adjustedCascadeIntervals(
baseCascade: CascadeConfig,
travelTimeMinutes: Int
) -> [Int] {
var intervals = getCascadeIntervals(baseCascade)
// Add travel-time-based warning if not already present
let travelWarning = travelTimeMinutes + 5 // 5 min buffer
if !intervals.contains(travelWarning) {
intervals.append(travelWarning)
intervals.sort(by: >)
}
// Add a "leave now" warning at exactly travel time
if !intervals.contains(travelTimeMinutes) {
intervals.append(travelTimeMinutes)
intervals.sort(by: >)
}
return intervals
}
/// Format a travel advisory message
func travelAdvisory(for estimate: TravelEstimate, timerLabel: String, targetTime: Date) -> String {
let travelMinutes = Int(estimate.travelTimeSeconds / 60)
let distanceKm = estimate.distance / 1000
let timeUntilLeave = (estimate.leaveByTime ?? Date()).timeIntervalSinceNow
let leaveMinutes = Int(timeUntilLeave / 60)
if leaveMinutes <= 0 {
return "Leave now for \(timerLabel)\(travelMinutes) min \(estimate.transportLabel) (\(String(format: "%.1f", distanceKm)) km)"
} else {
return "\(timerLabel) in \(formatDurationCompact(targetTime.timeIntervalSinceNow)). Leave in \(leaveMinutes) min — \(travelMinutes) min \(estimate.transportLabel)"
}
}
// MARK: - Cache Management
func clearCache() {
cachedEstimates.removeAll()
}
func invalidateEstimate(for timerId: String) {
cachedEstimates.removeValue(forKey: timerId)
}
}
// MARK: - Travel Estimate Model
struct TravelEstimate: Codable {
let travelTimeSeconds: TimeInterval
let distance: Double // meters
let transportType: TransportType
let calculatedAt: Date
let departureDate: Date
let arrivalDate: Date
var timerId: String?
var leaveByTime: Date?
var travelTimeMinutes: Int {
Int(travelTimeSeconds / 60)
}
var transportLabel: String {
transportType.label
}
// Wrapper for Codable MKDirectionsTransportType
init(
travelTimeSeconds: TimeInterval,
distance: Double,
transportType: MKDirectionsTransportType,
calculatedAt: Date,
departureDate: Date,
arrivalDate: Date
) {
self.travelTimeSeconds = travelTimeSeconds
self.distance = distance
self.transportType = TransportType(from: transportType)
self.calculatedAt = calculatedAt
self.departureDate = departureDate
self.arrivalDate = arrivalDate
}
}
// MARK: - Transport Type (Codable wrapper)
enum TransportType: String, Codable, CaseIterable, Identifiable {
case automobile = "driving"
case transit = "transit"
case walking = "walking"
var id: String { rawValue }
var label: String {
switch self {
case .automobile: return "drive"
case .transit: return "transit"
case .walking: return "walk"
}
}
var icon: String {
switch self {
case .automobile: return "car.fill"
case .transit: return "bus.fill"
case .walking: return "figure.walk"
}
}
var mkType: MKDirectionsTransportType {
switch self {
case .automobile: return .automobile
case .transit: return .transit
case .walking: return .walking
}
}
init(from mkType: MKDirectionsTransportType) {
switch mkType {
case .transit: self = .transit
case .walking: self = .walking
default: self = .automobile
}
}
}