diff options
Diffstat (limited to 'LearnMapKit/ViewModels/AircraftTrackingViewModel.swift')
| -rw-r--r-- | LearnMapKit/ViewModels/AircraftTrackingViewModel.swift | 327 |
1 files changed, 327 insertions, 0 deletions
diff --git a/LearnMapKit/ViewModels/AircraftTrackingViewModel.swift b/LearnMapKit/ViewModels/AircraftTrackingViewModel.swift new file mode 100644 index 0000000..048418f --- /dev/null +++ b/LearnMapKit/ViewModels/AircraftTrackingViewModel.swift @@ -0,0 +1,327 @@ +// +// AircraftTrackingViewModel.swift +// LearnMapKit +// +// Main ViewModel coordinating all aircraft tracking functionality +// + +import Foundation +import Combine +import MapKit +import SwiftUI + +@MainActor +class AircraftTrackingViewModel: ObservableObject { + + // MARK: - Published Properties (UI State) + @Published var aircraft: [Aircraft] = [] + @Published var selectedAircraft: Aircraft? + @Published var connectionState: DataSourceState = .idle + @Published var mapRegion: MKCoordinateRegion + @Published var aircraftCount: Int = 0 + @Published var isLoading: Bool = false + @Published var errorMessage: String? + @Published var showingSettings: Bool = false + + // MARK: - Configuration + @Published var currentConfiguration: AppConfiguration + + // MARK: - Dependencies + private let configurationManager: ConfigurationManager + private let aircraftRepository: AircraftRepository + private var currentDataSource: (any ADSBDataSource)? + private let dataSourceFactory: DataSourceFactory + + // MARK: - Private Properties + private var cancellables = Set<AnyCancellable>() + + // MARK: - Initialization + init( + configurationManager: ConfigurationManager, + aircraftRepository: AircraftRepository, + dataSourceFactory: DataSourceFactory + ) { + self.configurationManager = configurationManager + self.aircraftRepository = aircraftRepository + self.dataSourceFactory = dataSourceFactory + self.currentConfiguration = configurationManager.getCurrentConfiguration() + + // Initialize map region from configuration + let mapSettings = currentConfiguration.mapSettings + self.mapRegion = MKCoordinateRegion( + center: mapSettings.defaultCenter, + span: mapSettings.defaultSpan + ) + + setupBindings() + } + + // MARK: - Public Interface + func startTracking() async { + guard connectionState != .connected && connectionState != .connecting else { return } + + isLoading = true + errorMessage = nil + + do { + // Create and configure data source + currentDataSource = try createDataSource() + + // Start the data source + try await currentDataSource?.start() + + print("Aircraft tracking started") + } catch { + errorMessage = "Failed to start tracking: \(error.localizedDescription)" + connectionState = .error(error.localizedDescription) + } + + isLoading = false + } + + func stopTracking() async { + await currentDataSource?.stop() + currentDataSource = nil + connectionState = .disconnected + } + + func reconnect() async { + guard let dataSource = currentDataSource else { + await startTracking() + return + } + + isLoading = true + do { + try await dataSource.reconnect() + } catch { + errorMessage = "Reconnection failed: \(error.localizedDescription)" + } + isLoading = false + } + + func selectAircraft(_ aircraft: Aircraft?) { + selectedAircraft = aircraft + + // Center map on selected aircraft + if let aircraft = aircraft, + let position = aircraft.position { + withAnimation(.easeInOut(duration: 0.5)) { + mapRegion = MKCoordinateRegion( + center: CLLocationCoordinate2D( + latitude: position.latitude, + longitude: position.longitude + ), + span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) + ) + } + } + } + + func updateConfiguration(_ configuration: AppConfiguration) async { + // Validate configuration first + let validation = configurationManager.validateConfiguration(configuration) + guard validation.isValid else { + errorMessage = "Invalid configuration: \(validation.errors.joined(separator: ", "))" + return + } + + let wasConnected = connectionState == .connected + + // Stop current tracking if running + if wasConnected { + await stopTracking() + } + + // Update configuration + await configurationManager.updateConfiguration(configuration) + + // Update map region if changed + let newMapSettings = configuration.mapSettings + mapRegion = MKCoordinateRegion( + center: newMapSettings.defaultCenter, + span: newMapSettings.defaultSpan + ) + + // Restart tracking if it was running + if wasConnected { + await startTracking() + } + } + + func clearAircraftData() { + aircraftRepository.clearAll() + selectedAircraft = nil + } + + func exportAircraftData() -> String { + // Export current aircraft data as JSON or CSV + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + encoder.outputFormatting = .prettyPrinted + + do { + let data = try encoder.encode(aircraft) + return String(data: data, encoding: .utf8) ?? "Export failed" + } catch { + return "Export error: \(error.localizedDescription)" + } + } + + // MARK: - Configuration Shortcuts + func switchToFileMode(filePath: String) async { + let fileConfig = FileDataSourceConfiguration(filePath: filePath) + var newConfig = currentConfiguration + newConfig = AppConfiguration( + dataSource: fileConfig, + displaySettings: newConfig.displaySettings, + mapSettings: newConfig.mapSettings, + networkSettings: newConfig.networkSettings + ) + await updateConfiguration(newConfig) + } + + func switchToNetworkMode(hostname: String, port: Int) async { + let networkConfig = NetworkDataSourceConfiguration(hostname: hostname, port: port) + var newConfig = currentConfiguration + newConfig = AppConfiguration( + dataSource: networkConfig, + displaySettings: newConfig.displaySettings, + mapSettings: newConfig.mapSettings, + networkSettings: newConfig.networkSettings + ) + await updateConfiguration(newConfig) + } + + // MARK: - Private Methods + private func setupBindings() { + // Bind configuration updates + configurationManager.currentConfiguration + .receive(on: DispatchQueue.main) + .assign(to: &$currentConfiguration) + + // Bind aircraft updates + aircraftRepository.aircraftStream + .receive(on: DispatchQueue.main) + .assign(to: &$aircraft) + + // Bind aircraft count + aircraftRepository.aircraftCount + .receive(on: DispatchQueue.main) + .assign(to: &$aircraftCount) + + // Update selected aircraft when aircraft list changes + aircraftRepository.aircraftStream + .receive(on: DispatchQueue.main) + .sink { [weak self] aircraft in + self?.updateSelectedAircraft(from: aircraft) + } + .store(in: &cancellables) + } + + private func createDataSource() throws -> any ADSBDataSource { + let dataSource = try dataSourceFactory.createDataSource( + for: currentConfiguration.dataSource + ) + + // Bind data source streams + dataSource.connectionState + .receive(on: DispatchQueue.main) + .assign(to: &$connectionState) + + dataSource.dataStream + .receive(on: DispatchQueue.main) + .sink { [weak self] event in + self?.aircraftRepository.updateAircraft(with: event) + } + .store(in: &cancellables) + + return dataSource + } + + private func updateSelectedAircraft(from aircraftList: [Aircraft]) { + // Update selected aircraft if it still exists + if let selected = selectedAircraft { + selectedAircraft = aircraftList.first { $0.id == selected.id } + } + } +} + +// MARK: - Data Source Factory +protocol DataSourceFactory { + func createDataSource(for configuration: DataSourceConfiguration) throws -> any ADSBDataSource +} + +class DefaultDataSourceFactory: DataSourceFactory { + func createDataSource(for configuration: DataSourceConfiguration) throws -> any ADSBDataSource { + switch configuration.sourceType { + case .file: + guard let fileConfig = configuration as? FileDataSourceConfiguration else { + throw DataSourceFactoryError.invalidConfiguration + } + return FileADSBDataSource(configuration: fileConfig) + + case .network: + guard let networkConfig = configuration as? NetworkDataSourceConfiguration else { + throw DataSourceFactoryError.invalidConfiguration + } + return NetworkADSBDataSource(configuration: networkConfig) + + case .test: + return TestADSBDataSource(configuration: configuration) + } + } +} + +enum DataSourceFactoryError: Error { + case invalidConfiguration + case unsupportedDataSourceType +} + +// MARK: - Test Data Source +class TestADSBDataSource: ADSBDataSource { + var dataStream: AnyPublisher<ADSBDataEvent, Never> { + Empty().eraseToAnyPublisher() + } + + var connectionState: AnyPublisher<DataSourceState, Never> { + Just(.connected).eraseToAnyPublisher() + } + + private(set) var configuration: DataSourceConfiguration + + init(configuration: DataSourceConfiguration) { + self.configuration = configuration + } + + func configure(with config: DataSourceConfiguration) async { + self.configuration = config + } + + func start() async throws { + // Test implementation + } + + func stop() async { + // Test implementation + } + + func reconnect() async throws { + // Test implementation + } +} + +// MARK: - Error Handling Extension +extension AircraftTrackingViewModel { + func handleError(_ error: Error) { + errorMessage = error.localizedDescription + connectionState = .error(error.localizedDescription) + } + + func clearError() { + errorMessage = nil + if case .error = connectionState { + connectionState = .disconnected + } + } +}
\ No newline at end of file |
