summaryrefslogtreecommitdiff
path: root/LearnMapKit/ViewModels/AircraftTrackingViewModel.swift
diff options
context:
space:
mode:
Diffstat (limited to 'LearnMapKit/ViewModels/AircraftTrackingViewModel.swift')
-rw-r--r--LearnMapKit/ViewModels/AircraftTrackingViewModel.swift327
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