summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LearnMapKit/App/RefactoredApp.swift277
-rw-r--r--LearnMapKit/DI/DependencyContainer.swift118
-rw-r--r--LearnMapKit/DataSources/ADSBDataSource.swift92
-rw-r--r--LearnMapKit/DataSources/FileADSBDataSource.swift185
-rw-r--r--LearnMapKit/DataSources/NetworkADSBDataSource.swift201
-rw-r--r--LearnMapKit/Managers/ConfigurationManager.swift322
-rw-r--r--LearnMapKit/Repositories/AircraftRepository.swift265
-rw-r--r--LearnMapKit/ViewModels/AircraftTrackingViewModel.swift327
8 files changed, 1787 insertions, 0 deletions
diff --git a/LearnMapKit/App/RefactoredApp.swift b/LearnMapKit/App/RefactoredApp.swift
new file mode 100644
index 0000000..fbd7417
--- /dev/null
+++ b/LearnMapKit/App/RefactoredApp.swift
@@ -0,0 +1,277 @@
+//
+// RefactoredApp.swift
+// LearnMapKit
+//
+// Refactored app entry point with dependency injection
+//
+
+import SwiftUI
+import ArgumentParser
+
+@main
+struct RefactoredLearnMapKitApp: App {
+ private let container = DependencyContainer()
+
+ init() {
+ setupFromCommandLineArguments()
+ }
+
+ var body: some Scene {
+ WindowGroup {
+ MainContentView()
+ .environmentObject(container.makeAircraftTrackingViewModel())
+ .environmentObject(container.configurationManager)
+ }
+ }
+
+ private func setupFromCommandLineArguments() {
+ guard let args = parseCommandLineArguments() else { return }
+
+ Task {
+ // Configure based on command line arguments
+ if let hostname = args.hostname, let port = args.port {
+ await configureNetworkMode(hostname: hostname, port: port)
+ } else if let inputFile = args.inputfile {
+ await configureFileMode(filePath: inputFile)
+ }
+ // Otherwise use default configuration
+ }
+ }
+
+ private func parseCommandLineArguments() -> CommandLineArgs? {
+ do {
+ return try CommandLineArgs.parse()
+ } catch {
+ // If parsing fails, continue with default configuration
+ print("Command line parsing failed: \(error)")
+ return nil
+ }
+ }
+
+ private func configureNetworkMode(hostname: String, port: Int) async {
+ let networkConfig = NetworkDataSourceConfiguration(hostname: hostname, port: port)
+ let config = AppConfiguration(
+ dataSource: networkConfig,
+ displaySettings: DisplaySettings(),
+ mapSettings: MapSettings(),
+ networkSettings: NetworkSettings()
+ )
+ await container.configurationManager.updateConfiguration(config)
+ }
+
+ private func configureFileMode(filePath: String) async {
+ let fileConfig = FileDataSourceConfiguration(filePath: filePath)
+ let config = AppConfiguration(
+ dataSource: fileConfig,
+ displaySettings: DisplaySettings(),
+ mapSettings: MapSettings(),
+ networkSettings: NetworkSettings()
+ )
+ await container.configurationManager.updateConfiguration(config)
+ }
+}
+
+// MARK: - Main Content View
+struct MainContentView: View {
+ @EnvironmentObject var viewModel: AircraftTrackingViewModel
+ @EnvironmentObject var configurationManager: ConfigurationManager
+
+ var body: some View {
+ NavigationView {
+ ZStack {
+ AircraftMapView(
+ aircraft: viewModel.aircraft,
+ selectedAircraft: viewModel.selectedAircraft,
+ mapRegion: $viewModel.mapRegion,
+ onAircraftSelected: { aircraft in
+ viewModel.selectAircraft(aircraft)
+ }
+ )
+
+ VStack {
+ Spacer()
+ HStack {
+ ConnectionStatusBar(
+ state: viewModel.connectionState,
+ aircraftCount: viewModel.aircraftCount
+ )
+ Spacer()
+ }
+ .padding()
+ }
+
+ if viewModel.isLoading {
+ LoadingOverlay()
+ }
+ }
+ .navigationTitle("Aircraft Tracking")
+ .toolbar {
+ ToolbarItemGroup(placement: .primaryAction) {
+ ConnectionControlButton(
+ connectionState: viewModel.connectionState,
+ onStart: {
+ Task { await viewModel.startTracking() }
+ },
+ onStop: {
+ Task { await viewModel.stopTracking() }
+ },
+ onReconnect: {
+ Task { await viewModel.reconnect() }
+ }
+ )
+
+ Button("Settings") {
+ viewModel.showingSettings = true
+ }
+ }
+ }
+ .sheet(isPresented: $viewModel.showingSettings) {
+ SettingsView()
+ .environmentObject(viewModel)
+ }
+ .alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
+ Button("OK") {
+ viewModel.clearError()
+ }
+ } message: {
+ Text(viewModel.errorMessage ?? "")
+ }
+ }
+ .task {
+ // Auto-start tracking when view appears
+ await viewModel.startTracking()
+ }
+ }
+}
+
+// MARK: - Supporting Views
+struct ConnectionStatusBar: View {
+ let state: DataSourceState
+ let aircraftCount: Int
+
+ var body: some View {
+ HStack(spacing: 12) {
+ StatusIndicator(state: state)
+ Text(statusText)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 6)
+ .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8))
+ }
+
+ private var statusText: String {
+ switch state {
+ case .connected:
+ return "Connected • \(aircraftCount) aircraft"
+ case .connecting:
+ return "Connecting..."
+ case .disconnected:
+ return "Disconnected"
+ case .error(let message):
+ return "Error: \(message)"
+ case .idle:
+ return "Ready"
+ case .configuring:
+ return "Configuring..."
+ }
+ }
+}
+
+struct StatusIndicator: View {
+ let state: DataSourceState
+
+ var body: some View {
+ Circle()
+ .fill(indicatorColor)
+ .frame(width: 8, height: 8)
+ }
+
+ private var indicatorColor: Color {
+ switch state {
+ case .connected:
+ return .green
+ case .connecting, .configuring:
+ return .orange
+ case .disconnected, .idle:
+ return .gray
+ case .error:
+ return .red
+ }
+ }
+}
+
+struct ConnectionControlButton: View {
+ let connectionState: DataSourceState
+ let onStart: () -> Void
+ let onStop: () -> Void
+ let onReconnect: () -> Void
+
+ var body: some View {
+ Button(action: buttonAction) {
+ Image(systemName: buttonIconName)
+ }
+ }
+
+ private var buttonIconName: String {
+ switch connectionState {
+ case .connected:
+ return "stop.circle"
+ case .connecting, .configuring:
+ return "stop.circle"
+ case .disconnected, .idle:
+ return "play.circle"
+ case .error:
+ return "arrow.clockwise.circle"
+ }
+ }
+
+ private func buttonAction() {
+ switch connectionState {
+ case .connected, .connecting, .configuring:
+ onStop()
+ case .disconnected, .idle:
+ onStart()
+ case .error:
+ onReconnect()
+ }
+ }
+}
+
+struct LoadingOverlay: View {
+ var body: some View {
+ ZStack {
+ Color.black.opacity(0.3)
+ .ignoresSafeArea()
+
+ VStack(spacing: 16) {
+ ProgressView()
+ .scaleEffect(1.2)
+ Text("Connecting...")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ .padding()
+ .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))
+ }
+ }
+}
+
+// MARK: - Command Line Arguments (Updated)
+struct CommandLineArgs: ParsableCommand {
+ @Option(name: .shortAndLong, help: "Hostname for network mode")
+ var hostname: String?
+
+ @Option(name: .shortAndLong, help: "Port for network mode")
+ var port: Int?
+
+ @Option(name: .shortAndLong, help: "Input file path for file mode")
+ var inputfile: String?
+
+ @Flag(name: .shortAndLong, help: "Enable debug mode")
+ var debug: Bool = false
+
+ @Flag(name: .shortAndLong, help: "Show version information")
+ var version: Bool = false
+} \ No newline at end of file
diff --git a/LearnMapKit/DI/DependencyContainer.swift b/LearnMapKit/DI/DependencyContainer.swift
new file mode 100644
index 0000000..f0d1323
--- /dev/null
+++ b/LearnMapKit/DI/DependencyContainer.swift
@@ -0,0 +1,118 @@
+//
+// DependencyContainer.swift
+// LearnMapKit
+//
+// Dependency injection container for the app
+//
+
+import Foundation
+
+@MainActor
+class DependencyContainer {
+
+ // MARK: - Singletons
+ lazy var configurationManager: ConfigurationManager = DefaultConfigurationManager()
+ lazy var aircraftRepository: AircraftRepository = DefaultAircraftRepository(tracker: aircraftTracker)
+ lazy var dataSourceFactory: DataSourceFactory = DefaultDataSourceFactory()
+ lazy var aircraftTracker: AircraftTracker = DefaultAircraftTracker()
+
+ // MARK: - ViewModels
+ func makeAircraftTrackingViewModel() -> AircraftTrackingViewModel {
+ AircraftTrackingViewModel(
+ configurationManager: configurationManager,
+ aircraftRepository: aircraftRepository,
+ dataSourceFactory: dataSourceFactory
+ )
+ }
+
+ func makeSettingsViewModel() -> SettingsViewModel {
+ SettingsViewModel(configurationManager: configurationManager)
+ }
+
+ // MARK: - Data Sources (Factory Methods)
+ func makeFileDataSource(configuration: FileDataSourceConfiguration) -> FileADSBDataSource {
+ FileADSBDataSource(
+ configuration: configuration,
+ tracker: aircraftTracker
+ )
+ }
+
+ func makeNetworkDataSource(configuration: NetworkDataSourceConfiguration) -> NetworkADSBDataSource {
+ NetworkADSBDataSource(
+ configuration: configuration,
+ tracker: aircraftTracker
+ )
+ }
+}
+
+// MARK: - Settings ViewModel
+@MainActor
+class SettingsViewModel: ObservableObject {
+ @Published var configuration: AppConfiguration
+ @Published var hasUnsavedChanges: Bool = false
+ @Published var validationErrors: [String] = []
+ @Published var isValid: Bool = true
+
+ private let configurationManager: ConfigurationManager
+ private var cancellables = Set<AnyCancellable>()
+
+ init(configurationManager: ConfigurationManager) {
+ self.configurationManager = configurationManager
+ self.configuration = configurationManager.getCurrentConfiguration()
+
+ setupBindings()
+ }
+
+ func saveSettings() async {
+ let validation = configurationManager.validateConfiguration(configuration)
+ validationErrors = validation.errors
+ isValid = validation.isValid
+
+ if validation.isValid {
+ do {
+ await configurationManager.updateConfiguration(configuration)
+ try await configurationManager.saveConfiguration()
+ } catch {
+ validationErrors = ["Failed to save: \(error.localizedDescription)"]
+ isValid = false
+ }
+ }
+ }
+
+ func resetToDefaults() async {
+ configuration = AppConfiguration.default
+ await configurationManager.resetToDefaults()
+ }
+
+ func validateCurrentSettings() {
+ let validation = configurationManager.validateConfiguration(configuration)
+ validationErrors = validation.errors
+ isValid = validation.isValid
+ }
+
+ private func setupBindings() {
+ configurationManager.hasUnsavedChanges
+ .receive(on: DispatchQueue.main)
+ .assign(to: &$hasUnsavedChanges)
+
+ $configuration
+ .removeDuplicates()
+ .sink { [weak self] _ in
+ self?.validateCurrentSettings()
+ }
+ .store(in: &cancellables)
+ }
+}
+
+// MARK: - Missing Protocol Implementations (Stubs)
+protocol ADSBMessageDecoder {
+ func decode(_ message: String) -> ADSBDataEvent?
+}
+
+class DefaultADSBMessageDecoder: ADSBMessageDecoder {
+ func decode(_ message: String) -> ADSBDataEvent? {
+ // This would contain the actual decoding logic
+ // For now, return nil as a stub
+ return nil
+ }
+} \ No newline at end of file
diff --git a/LearnMapKit/DataSources/ADSBDataSource.swift b/LearnMapKit/DataSources/ADSBDataSource.swift
new file mode 100644
index 0000000..b30e344
--- /dev/null
+++ b/LearnMapKit/DataSources/ADSBDataSource.swift
@@ -0,0 +1,92 @@
+//
+// ADSBDataSource.swift
+// LearnMapKit
+//
+// Created for MVVM refactoring
+//
+
+import Foundation
+import Combine
+
+// MARK: - Data Source Protocol
+protocol ADSBDataSource: AnyObject {
+ var dataStream: AnyPublisher<ADSBDataEvent, Never> { get }
+ var connectionState: AnyPublisher<DataSourceState, Never> { get }
+ var configuration: DataSourceConfiguration { get }
+
+ func configure(with config: DataSourceConfiguration) async
+ func start() async throws
+ func stop() async
+ func reconnect() async throws
+}
+
+// MARK: - Data Events
+enum ADSBDataEvent {
+ case aircraftIdentification(address: Int, icaoName: String)
+ case aircraftPosition(address: Int, latitude: Double, longitude: Double)
+ case aircraftAltitude(address: Int, altitude: Int)
+ case rawMessage(String)
+ case error(Error)
+}
+
+// MARK: - Data Source State
+enum DataSourceState: Equatable {
+ case idle
+ case configuring
+ case connecting
+ case connected
+ case disconnected
+ case error(String)
+
+ static func == (lhs: DataSourceState, rhs: DataSourceState) -> Bool {
+ switch (lhs, rhs) {
+ case (.idle, .idle), (.configuring, .configuring),
+ (.connecting, .connecting), (.connected, .connected),
+ (.disconnected, .disconnected):
+ return true
+ case let (.error(lhsError), .error(rhsError)):
+ return lhsError == rhsError
+ default:
+ return false
+ }
+ }
+}
+
+// MARK: - Configuration
+protocol DataSourceConfiguration {
+ var sourceType: DataSourceType { get }
+}
+
+enum DataSourceType {
+ case file(path: String)
+ case network(host: String, port: Int)
+ case test(mockData: [String])
+}
+
+struct FileDataSourceConfiguration: DataSourceConfiguration {
+ let sourceType: DataSourceType
+ let filePath: String
+ let processRate: Int // messages per second
+
+ init(filePath: String, processRate: Int = 120) {
+ self.filePath = filePath
+ self.processRate = processRate
+ self.sourceType = .file(path: filePath)
+ }
+}
+
+struct NetworkDataSourceConfiguration: DataSourceConfiguration {
+ let sourceType: DataSourceType
+ let hostname: String
+ let port: Int
+ let reconnectAttempts: Int
+ let timeoutInterval: TimeInterval
+
+ init(hostname: String, port: Int, reconnectAttempts: Int = 3, timeoutInterval: TimeInterval = 10.0) {
+ self.hostname = hostname
+ self.port = port
+ self.reconnectAttempts = reconnectAttempts
+ self.timeoutInterval = timeoutInterval
+ self.sourceType = .network(host: hostname, port: port)
+ }
+} \ No newline at end of file
diff --git a/LearnMapKit/DataSources/FileADSBDataSource.swift b/LearnMapKit/DataSources/FileADSBDataSource.swift
new file mode 100644
index 0000000..448721b
--- /dev/null
+++ b/LearnMapKit/DataSources/FileADSBDataSource.swift
@@ -0,0 +1,185 @@
+//
+// FileADSBDataSource.swift
+// LearnMapKit
+//
+// File-based ADSB data source implementation
+//
+
+import Foundation
+import Combine
+
+@MainActor
+class FileADSBDataSource: ADSBDataSource {
+
+ // MARK: - ADSBDataSource Protocol
+ var dataStream: AnyPublisher<ADSBDataEvent, Never> {
+ dataSubject.eraseToAnyPublisher()
+ }
+
+ var connectionState: AnyPublisher<DataSourceState, Never> {
+ stateSubject.eraseToAnyPublisher()
+ }
+
+ private(set) var configuration: DataSourceConfiguration
+
+ // MARK: - Private Properties
+ private let dataSubject = PassthroughSubject<ADSBDataEvent, Never>()
+ private let stateSubject = CurrentValueSubject<DataSourceState, Never>(.idle)
+
+ private var fileRunner: ADSBFileRunner?
+ private var processingTask: Task<Void, Error>?
+ private var timer: Timer?
+
+ // MARK: - Dependencies
+ private let decoder: ADSBMessageDecoder
+ private let tracker: AircraftTracker
+
+ init(configuration: FileDataSourceConfiguration,
+ decoder: ADSBMessageDecoder = DefaultADSBMessageDecoder(),
+ tracker: AircraftTracker = DefaultAircraftTracker()) {
+ self.configuration = configuration
+ self.decoder = decoder
+ self.tracker = tracker
+ }
+
+ // MARK: - ADSBDataSource Implementation
+ func configure(with config: DataSourceConfiguration) async {
+ guard let fileConfig = config as? FileDataSourceConfiguration else {
+ stateSubject.send(.error("Invalid configuration type for FileADSBDataSource"))
+ return
+ }
+
+ stateSubject.send(.configuring)
+ self.configuration = fileConfig
+ stateSubject.send(.idle)
+ }
+
+ func start() async throws {
+ guard let fileConfig = configuration as? FileDataSourceConfiguration else {
+ throw ADSBDataSourceError.invalidConfiguration
+ }
+
+ stateSubject.send(.connecting)
+
+ do {
+ // Initialize file runner
+ fileRunner = ADSBFileRunner(filename: fileConfig.filePath)
+
+ // Start file processing
+ try await startFileProcessing(with: fileConfig)
+
+ stateSubject.send(.connected)
+ } catch {
+ stateSubject.send(.error(error.localizedDescription))
+ throw error
+ }
+ }
+
+ func stop() async {
+ timer?.invalidate()
+ timer = nil
+
+ processingTask?.cancel()
+ processingTask = nil
+
+ fileRunner = nil
+ stateSubject.send(.disconnected)
+ }
+
+ func reconnect() async throws {
+ await stop()
+ try await start()
+ }
+
+ // MARK: - Private Methods
+ private func startFileProcessing(with config: FileDataSourceConfiguration) async throws {
+ guard let runner = fileRunner else {
+ throw ADSBDataSourceError.initializationFailed
+ }
+
+ // Start background file processing
+ processingTask = Task {
+ do {
+ runner.openFile()
+ runner.readFile()
+
+ // Start decoding in background
+ await runner.decodeFromFile()
+
+ // Start timer for data consumption
+ await startDataConsumption(runner: runner, rate: config.processRate)
+ } catch {
+ await MainActor.run {
+ dataSubject.send(.error(error))
+ stateSubject.send(.error(error.localizedDescription))
+ }
+ }
+ }
+ }
+
+ private func startDataConsumption(runner: ADSBFileRunner, rate: Int) async {
+ await MainActor.run {
+ timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
+ self?.processDataBatch(from: runner, batchSize: rate)
+ }
+ }
+ }
+
+ private func processDataBatch(from runner: ADSBFileRunner, batchSize: Int) {
+ guard runner.jobDone() && runner.getCount() > 0 else { return }
+
+ let batchSize = min(batchSize, runner.getCount())
+ let data = runner.getPlainData(batchSize)
+
+ Task {
+ for _ in 0..<data.getCount() {
+ let nextTag = data.getNextTag()
+
+ switch nextTag {
+ case .ADSB_ICAO:
+ let icao = data.getIcaoName()
+ await tracker.updateIdentification(address: icao.address, icaoName: icao.ICAOname)
+ dataSubject.send(.aircraftIdentification(address: icao.address, icaoName: icao.ICAOname))
+
+ case .ADSB_LOCATION:
+ let location = data.getLocation()
+ await tracker.updatePosition(address: location.address,
+ latitude: location.lat,
+ longitude: location.long)
+ dataSubject.send(.aircraftPosition(address: location.address,
+ latitude: location.lat,
+ longitude: location.long))
+
+ case .ADSB_ALTITUDE:
+ let altitude = data.getAltitude()
+ await tracker.updateAltitude(address: altitude.address, altitude: altitude.altitude)
+ dataSubject.send(.aircraftAltitude(address: altitude.address, altitude: altitude.altitude))
+
+ case .EMPTY:
+ break
+ }
+ }
+ }
+ }
+}
+
+// MARK: - Errors
+enum ADSBDataSourceError: Error, LocalizedError {
+ case invalidConfiguration
+ case fileNotFound(String)
+ case initializationFailed
+ case processingError(Error)
+
+ var errorDescription: String? {
+ switch self {
+ case .invalidConfiguration:
+ return "Invalid data source configuration"
+ case .fileNotFound(let path):
+ return "File not found at path: \(path)"
+ case .initializationFailed:
+ return "Failed to initialize data source"
+ case .processingError(let error):
+ return "Processing error: \(error.localizedDescription)"
+ }
+ }
+} \ No newline at end of file
diff --git a/LearnMapKit/DataSources/NetworkADSBDataSource.swift b/LearnMapKit/DataSources/NetworkADSBDataSource.swift
new file mode 100644
index 0000000..a2c3890
--- /dev/null
+++ b/LearnMapKit/DataSources/NetworkADSBDataSource.swift
@@ -0,0 +1,201 @@
+//
+// NetworkADSBDataSource.swift
+// LearnMapKit
+//
+// Network-based ADSB data source implementation
+//
+
+import Foundation
+import Combine
+
+@MainActor
+class NetworkADSBDataSource: ADSBDataSource {
+
+ // MARK: - ADSBDataSource Protocol
+ var dataStream: AnyPublisher<ADSBDataEvent, Never> {
+ dataSubject.eraseToAnyPublisher()
+ }
+
+ var connectionState: AnyPublisher<DataSourceState, Never> {
+ stateSubject.eraseToAnyPublisher()
+ }
+
+ private(set) var configuration: DataSourceConfiguration
+
+ // MARK: - Private Properties
+ private let dataSubject = PassthroughSubject<ADSBDataEvent, Never>()
+ private let stateSubject = CurrentValueSubject<DataSourceState, Never>(.idle)
+
+ private var netRunner: ADSBNetRunner?
+ private var processingTask: Task<Void, Error>?
+ private var timer: Timer?
+ private var reconnectAttempts = 0
+
+ // MARK: - Dependencies
+ private let decoder: ADSBMessageDecoder
+ private let tracker: AircraftTracker
+
+ init(configuration: NetworkDataSourceConfiguration,
+ decoder: ADSBMessageDecoder = DefaultADSBMessageDecoder(),
+ tracker: AircraftTracker = DefaultAircraftTracker()) {
+ self.configuration = configuration
+ self.decoder = decoder
+ self.tracker = tracker
+ }
+
+ // MARK: - ADSBDataSource Implementation
+ func configure(with config: DataSourceConfiguration) async {
+ guard let networkConfig = config as? NetworkDataSourceConfiguration else {
+ stateSubject.send(.error("Invalid configuration type for NetworkADSBDataSource"))
+ return
+ }
+
+ stateSubject.send(.configuring)
+ self.configuration = networkConfig
+ reconnectAttempts = 0
+ stateSubject.send(.idle)
+ }
+
+ func start() async throws {
+ guard let networkConfig = configuration as? NetworkDataSourceConfiguration else {
+ throw ADSBDataSourceError.invalidConfiguration
+ }
+
+ stateSubject.send(.connecting)
+
+ do {
+ // Initialize network runner
+ netRunner = ADSBNetRunner(address: networkConfig.hostname, port: networkConfig.port)
+
+ // Start network processing
+ try await startNetworkProcessing()
+
+ stateSubject.send(.connected)
+ reconnectAttempts = 0
+ } catch {
+ stateSubject.send(.error(error.localizedDescription))
+ await handleConnectionError(error: error)
+ throw error
+ }
+ }
+
+ func stop() async {
+ timer?.invalidate()
+ timer = nil
+
+ processingTask?.cancel()
+ processingTask = nil
+
+ await netRunner?.stop()
+ netRunner = nil
+ stateSubject.send(.disconnected)
+ reconnectAttempts = 0
+ }
+
+ func reconnect() async throws {
+ guard let networkConfig = configuration as? NetworkDataSourceConfiguration else {
+ throw ADSBDataSourceError.invalidConfiguration
+ }
+
+ // Check if we should attempt reconnection
+ guard reconnectAttempts < networkConfig.reconnectAttempts else {
+ stateSubject.send(.error("Maximum reconnection attempts reached"))
+ return
+ }
+
+ reconnectAttempts += 1
+ await stop()
+
+ // Wait before reconnecting
+ try await Task.sleep(nanoseconds: UInt64(2_000_000_000 * reconnectAttempts)) // Exponential backoff
+
+ try await start()
+ }
+
+ // MARK: - Private Methods
+ private func startNetworkProcessing() async throws {
+ guard let runner = netRunner else {
+ throw ADSBDataSourceError.initializationFailed
+ }
+
+ // Start network connection
+ processingTask = Task {
+ do {
+ try await runner.start()
+ await startDataConsumption(runner: runner)
+ } catch {
+ await MainActor.run {
+ dataSubject.send(.error(error))
+ stateSubject.send(.error(error.localizedDescription))
+ }
+ }
+ }
+ }
+
+ private func startDataConsumption(runner: ADSBNetRunner) async {
+ await MainActor.run {
+ timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
+ self?.processDataBatch(from: runner)
+ }
+ }
+ }
+
+ private func processDataBatch(from runner: ADSBNetRunner) {
+ guard runner.getCount() > 0 else { return }
+
+ Task {
+ for _ in 0..<runner.adsb_tag_stream.getCount() {
+ let nextTag = runner.adsb_tag_stream.getNextTag()
+
+ switch nextTag {
+ case .ADSB_ICAO:
+ let icao = runner.adsb_tag_stream.getIcaoName()
+ await tracker.updateIdentification(address: icao.address, icaoName: icao.ICAOname)
+ dataSubject.send(.aircraftIdentification(address: icao.address, icaoName: icao.ICAOname))
+
+ case .ADSB_LOCATION:
+ let location = runner.adsb_tag_stream.getLocation()
+ await tracker.updatePosition(address: location.address,
+ latitude: location.lat,
+ longitude: location.long)
+ dataSubject.send(.aircraftPosition(address: location.address,
+ latitude: location.lat,
+ longitude: location.long))
+
+ case .ADSB_ALTITUDE:
+ let altitude = runner.adsb_tag_stream.getAltitude()
+ await tracker.updateAltitude(address: altitude.address, altitude: altitude.altitude)
+ dataSubject.send(.aircraftAltitude(address: altitude.address, altitude: altitude.altitude))
+
+ case .EMPTY:
+ break
+ }
+ }
+ }
+ }
+
+ private func handleConnectionError(error: Error) async {
+ guard let networkConfig = configuration as? NetworkDataSourceConfiguration else { return }
+
+ // Attempt automatic reconnection for network errors
+ if reconnectAttempts < networkConfig.reconnectAttempts {
+ Task {
+ do {
+ try await reconnect()
+ } catch {
+ // Final failure after all attempts
+ stateSubject.send(.error("Connection failed after \(networkConfig.reconnectAttempts) attempts"))
+ }
+ }
+ }
+ }
+}
+
+// MARK: - Extensions
+extension ADSBNetRunner {
+ func stop() async {
+ // Add proper cleanup method to ADSBNetRunner
+ timer?.invalidate()
+ timer = nil
+ }
+} \ No newline at end of file
diff --git a/LearnMapKit/Managers/ConfigurationManager.swift b/LearnMapKit/Managers/ConfigurationManager.swift
new file mode 100644
index 0000000..15ae274
--- /dev/null
+++ b/LearnMapKit/Managers/ConfigurationManager.swift
@@ -0,0 +1,322 @@
+//
+// ConfigurationManager.swift
+// LearnMapKit
+//
+// Centralized configuration management
+//
+
+import Foundation
+import Combine
+import MapKit
+
+// MARK: - App Configuration
+struct AppConfiguration: Equatable {
+ let dataSource: DataSourceConfiguration
+ let displaySettings: DisplaySettings
+ let mapSettings: MapSettings
+ let networkSettings: NetworkSettings
+
+ static let `default` = AppConfiguration(
+ dataSource: NetworkDataSourceConfiguration(
+ hostname: "64.226.23.78",
+ port: 30003
+ ),
+ displaySettings: DisplaySettings(),
+ mapSettings: MapSettings(),
+ networkSettings: NetworkSettings()
+ )
+}
+
+// MARK: - Display Settings
+struct DisplaySettings: Equatable {
+ var showAircraftLabels: Bool = true
+ var showAltitude: Bool = true
+ var showSpeed: Bool = false
+ var aircraftHistoryLength: Int = 10
+ var updateInterval: TimeInterval = 1.0
+ var theme: AppTheme = .system
+
+ enum AppTheme: String, CaseIterable {
+ case light, dark, system
+ }
+}
+
+// MARK: - Map Settings
+struct MapSettings: Equatable {
+ var defaultCenter: CLLocationCoordinate2D = CLLocationCoordinate2D(
+ latitude: 55.90159,
+ longitude: -3.53154
+ )
+ var defaultSpan: MKCoordinateSpan = MKCoordinateSpan(
+ latitudeDelta: 0.1,
+ longitudeDelta: 0.1
+ )
+ var mapStyle: MapStyle = .hybrid
+ var showTerrain: Bool = true
+
+ enum MapStyle: String, CaseIterable {
+ case standard, hybrid, satellite
+
+ var mkMapType: MKMapType {
+ switch self {
+ case .standard: return .standard
+ case .hybrid: return .hybrid
+ case .satellite: return .satellite
+ }
+ }
+ }
+
+ // Custom equality to handle CLLocationCoordinate2D
+ static func == (lhs: MapSettings, rhs: MapSettings) -> Bool {
+ lhs.defaultCenter.latitude == rhs.defaultCenter.latitude &&
+ lhs.defaultCenter.longitude == rhs.defaultCenter.longitude &&
+ lhs.defaultSpan.latitudeDelta == rhs.defaultSpan.latitudeDelta &&
+ lhs.defaultSpan.longitudeDelta == rhs.defaultSpan.longitudeDelta &&
+ lhs.mapStyle == rhs.mapStyle &&
+ lhs.showTerrain == rhs.showTerrain
+ }
+}
+
+// MARK: - Network Settings
+struct NetworkSettings: Equatable {
+ var connectionTimeout: TimeInterval = 10.0
+ var reconnectAttempts: Int = 3
+ var bufferSize: Int = 1024
+ var enableHeartbeat: Bool = true
+ var heartbeatInterval: TimeInterval = 30.0
+}
+
+// MARK: - Configuration Manager Protocol
+protocol ConfigurationManager: AnyObject {
+ var currentConfiguration: AnyPublisher<AppConfiguration, Never> { get }
+ var hasUnsavedChanges: AnyPublisher<Bool, Never> { get }
+
+ func getCurrentConfiguration() -> AppConfiguration
+ func updateConfiguration(_ configuration: AppConfiguration) async
+ func updateDataSource(_ dataSource: DataSourceConfiguration) async
+ func updateDisplaySettings(_ settings: DisplaySettings) async
+ func updateMapSettings(_ settings: MapSettings) async
+ func updateNetworkSettings(_ settings: NetworkSettings) async
+ func resetToDefaults() async
+ func saveConfiguration() async throws
+ func validateConfiguration(_ configuration: AppConfiguration) -> ConfigurationValidationResult
+}
+
+// MARK: - Validation
+struct ConfigurationValidationResult {
+ let isValid: Bool
+ let errors: [String]
+ let warnings: [String]
+
+ static let valid = ConfigurationValidationResult(isValid: true, errors: [], warnings: [])
+}
+
+// MARK: - Default Implementation
+@MainActor
+class DefaultConfigurationManager: ConfigurationManager {
+
+ // MARK: - Published Properties
+ var currentConfiguration: AnyPublisher<AppConfiguration, Never> {
+ configurationSubject.eraseToAnyPublisher()
+ }
+
+ var hasUnsavedChanges: AnyPublisher<Bool, Never> {
+ hasChangesSubject.eraseToAnyPublisher()
+ }
+
+ // MARK: - Private Properties
+ private let configurationSubject: CurrentValueSubject<AppConfiguration, Never>
+ private let hasChangesSubject = CurrentValueSubject<Bool, Never>(false)
+ private var savedConfiguration: AppConfiguration
+ private let userDefaults = UserDefaults.standard
+
+ // MARK: - Keys for UserDefaults
+ private enum Keys {
+ static let configuration = "app_configuration"
+ }
+
+ init() {
+ // Load configuration from UserDefaults or use default
+ if let data = userDefaults.data(forKey: Keys.configuration),
+ let configuration = try? JSONDecoder().decode(AppConfiguration.self, from: data) {
+ self.savedConfiguration = configuration
+ } else {
+ self.savedConfiguration = AppConfiguration.default
+ }
+
+ self.configurationSubject = CurrentValueSubject(savedConfiguration)
+
+ // Monitor for changes
+ setupChangeMonitoring()
+ }
+
+ // MARK: - ConfigurationManager Implementation
+ func getCurrentConfiguration() -> AppConfiguration {
+ configurationSubject.value
+ }
+
+ func updateConfiguration(_ configuration: AppConfiguration) async {
+ configurationSubject.send(configuration)
+ checkForChanges()
+ }
+
+ func updateDataSource(_ dataSource: DataSourceConfiguration) async {
+ var current = getCurrentConfiguration()
+ current = AppConfiguration(
+ dataSource: dataSource,
+ displaySettings: current.displaySettings,
+ mapSettings: current.mapSettings,
+ networkSettings: current.networkSettings
+ )
+ await updateConfiguration(current)
+ }
+
+ func updateDisplaySettings(_ settings: DisplaySettings) async {
+ var current = getCurrentConfiguration()
+ current = AppConfiguration(
+ dataSource: current.dataSource,
+ displaySettings: settings,
+ mapSettings: current.mapSettings,
+ networkSettings: current.networkSettings
+ )
+ await updateConfiguration(current)
+ }
+
+ func updateMapSettings(_ settings: MapSettings) async {
+ var current = getCurrentConfiguration()
+ current = AppConfiguration(
+ dataSource: current.dataSource,
+ displaySettings: current.displaySettings,
+ mapSettings: settings,
+ networkSettings: current.networkSettings
+ )
+ await updateConfiguration(current)
+ }
+
+ func updateNetworkSettings(_ settings: NetworkSettings) async {
+ var current = getCurrentConfiguration()
+ current = AppConfiguration(
+ dataSource: current.dataSource,
+ displaySettings: current.displaySettings,
+ mapSettings: current.mapSettings,
+ networkSettings: settings
+ )
+ await updateConfiguration(current)
+ }
+
+ func resetToDefaults() async {
+ await updateConfiguration(AppConfiguration.default)
+ }
+
+ func saveConfiguration() async throws {
+ let current = getCurrentConfiguration()
+ let data = try JSONEncoder().encode(current)
+ userDefaults.set(data, forKey: Keys.configuration)
+ savedConfiguration = current
+ hasChangesSubject.send(false)
+ }
+
+ func validateConfiguration(_ configuration: AppConfiguration) -> ConfigurationValidationResult {
+ var errors: [String] = []
+ var warnings: [String] = []
+
+ // Validate data source
+ switch configuration.dataSource.sourceType {
+ case .file(let path):
+ if !FileManager.default.fileExists(atPath: path) {
+ errors.append("File does not exist at path: \(path)")
+ }
+ case .network(let host, let port):
+ if host.isEmpty {
+ errors.append("Network hostname cannot be empty")
+ }
+ if port <= 0 || port > 65535 {
+ errors.append("Invalid port number: \(port)")
+ }
+ case .test:
+ warnings.append("Using test data source")
+ }
+
+ // Validate display settings
+ if configuration.displaySettings.updateInterval < 0.1 {
+ errors.append("Update interval must be at least 0.1 seconds")
+ }
+ if configuration.displaySettings.aircraftHistoryLength < 1 {
+ errors.append("Aircraft history length must be at least 1")
+ }
+
+ // Validate network settings
+ if configuration.networkSettings.connectionTimeout < 1.0 {
+ errors.append("Connection timeout must be at least 1 second")
+ }
+ if configuration.networkSettings.reconnectAttempts < 0 {
+ errors.append("Reconnect attempts cannot be negative")
+ }
+
+ return ConfigurationValidationResult(
+ isValid: errors.isEmpty,
+ errors: errors,
+ warnings: warnings
+ )
+ }
+
+ // MARK: - Private Methods
+ private func setupChangeMonitoring() {
+ configurationSubject
+ .dropFirst() // Skip initial value
+ .sink { [weak self] _ in
+ self?.checkForChanges()
+ }
+ .store(in: &cancellables)
+ }
+
+ private func checkForChanges() {
+ let hasChanges = getCurrentConfiguration() != savedConfiguration
+ hasChangesSubject.send(hasChanges)
+ }
+
+ private var cancellables = Set<AnyCancellable>()
+}
+
+// MARK: - Extensions for Codable Support
+extension AppConfiguration: Codable {
+ // Custom coding keys and implementation would be needed here
+ // for proper serialization of the configuration
+}
+
+extension DisplaySettings: Codable {}
+extension NetworkSettings: Codable {}
+
+extension MapSettings: Codable {
+ enum CodingKeys: String, CodingKey {
+ case defaultCenterLatitude, defaultCenterLongitude
+ case defaultSpanLatitudeDelta, defaultSpanLongitudeDelta
+ case mapStyle, showTerrain
+ }
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ let latitude = try container.decode(Double.self, forKey: .defaultCenterLatitude)
+ let longitude = try container.decode(Double.self, forKey: .defaultCenterLongitude)
+ defaultCenter = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
+
+ let latDelta = try container.decode(Double.self, forKey: .defaultSpanLatitudeDelta)
+ let lonDelta = try container.decode(Double.self, forKey: .defaultSpanLongitudeDelta)
+ defaultSpan = MKCoordinateSpan(latitudeDelta: latDelta, longitudeDelta: lonDelta)
+
+ mapStyle = try container.decode(MapStyle.self, forKey: .mapStyle)
+ showTerrain = try container.decode(Bool.self, forKey: .showTerrain)
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+
+ try container.encode(defaultCenter.latitude, forKey: .defaultCenterLatitude)
+ try container.encode(defaultCenter.longitude, forKey: .defaultCenterLongitude)
+ try container.encode(defaultSpan.latitudeDelta, forKey: .defaultSpanLatitudeDelta)
+ try container.encode(defaultSpan.longitudeDelta, forKey: .defaultSpanLongitudeDelta)
+ try container.encode(mapStyle, forKey: .mapStyle)
+ try container.encode(showTerrain, forKey: .showTerrain)
+ }
+} \ No newline at end of file
diff --git a/LearnMapKit/Repositories/AircraftRepository.swift b/LearnMapKit/Repositories/AircraftRepository.swift
new file mode 100644
index 0000000..6a6bc7e
--- /dev/null
+++ b/LearnMapKit/Repositories/AircraftRepository.swift
@@ -0,0 +1,265 @@
+//
+// AircraftRepository.swift
+// LearnMapKit
+//
+// Aircraft data management and state tracking
+//
+
+import Foundation
+import Combine
+
+// MARK: - Aircraft Model
+struct Aircraft: Identifiable, Equatable {
+ let id: Int // ICAO address
+ let icaoAddress: Int
+ let icaoName: String?
+ let position: Position?
+ let altitude: Int?
+ let lastSeen: Date
+ let flightInfo: FlightInfo?
+
+ struct Position: Equatable {
+ let latitude: Double
+ let longitude: Double
+ let accuracy: PositionAccuracy
+ let timestamp: Date
+
+ enum PositionAccuracy {
+ case unknown, low, medium, high
+ }
+ }
+
+ struct FlightInfo {
+ let from: String?
+ let to: String?
+ let route: String?
+ }
+
+ // Computed properties for UI
+ var isActive: Bool {
+ Date().timeIntervalSince(lastSeen) < 300 // 5 minutes
+ }
+
+ var displayName: String {
+ icaoName ?? "Unknown (\(String(format: "%06X", icaoAddress)))"
+ }
+}
+
+// MARK: - Aircraft Repository Protocol
+protocol AircraftRepository: AnyObject {
+ var aircraftStream: AnyPublisher<[Aircraft], Never> { get }
+ var aircraftCount: AnyPublisher<Int, Never> { get }
+
+ func getAllAircraft() -> [Aircraft]
+ func getAircraft(withId id: Int) -> Aircraft?
+ func updateAircraft(with event: ADSBDataEvent)
+ func removeInactiveAircraft()
+ func clearAll()
+}
+
+// MARK: - Default Implementation
+@MainActor
+class DefaultAircraftRepository: AircraftRepository {
+
+ // MARK: - Published Properties
+ var aircraftStream: AnyPublisher<[Aircraft], Never> {
+ aircraftSubject.eraseToAnyPublisher()
+ }
+
+ var aircraftCount: AnyPublisher<Int, Never> {
+ aircraftSubject
+ .map { $0.count }
+ .removeDuplicates()
+ .eraseToAnyPublisher()
+ }
+
+ // MARK: - Private Properties
+ private let aircraftSubject = CurrentValueSubject<[Aircraft], Never>([])
+ private var aircraftCache: [Int: Aircraft] = [:]
+
+ // Dependencies
+ private let tracker: AircraftTracker
+
+ init(tracker: AircraftTracker = DefaultAircraftTracker()) {
+ self.tracker = tracker
+ startCleanupTimer()
+ }
+
+ // MARK: - AircraftRepository Implementation
+ func getAllAircraft() -> [Aircraft] {
+ Array(aircraftCache.values)
+ .filter { $0.isActive }
+ .sorted { $0.lastSeen > $1.lastSeen }
+ }
+
+ func getAircraft(withId id: Int) -> Aircraft? {
+ aircraftCache[id]
+ }
+
+ func updateAircraft(with event: ADSBDataEvent) {
+ switch event {
+ case .aircraftIdentification(let address, let icaoName):
+ updateIdentification(address: address, icaoName: icaoName)
+
+ case .aircraftPosition(let address, let latitude, let longitude):
+ updatePosition(address: address, latitude: latitude, longitude: longitude)
+
+ case .aircraftAltitude(let address, let altitude):
+ updateAltitude(address: address, altitude: altitude)
+
+ case .rawMessage, .error:
+ break // Handle these separately if needed
+ }
+ }
+
+ func removeInactiveAircraft() {
+ let activeAircraft = aircraftCache.filter { _, aircraft in
+ aircraft.isActive
+ }
+
+ if activeAircraft.count != aircraftCache.count {
+ aircraftCache = activeAircraft
+ publishUpdate()
+ }
+ }
+
+ func clearAll() {
+ aircraftCache.removeAll()
+ publishUpdate()
+ }
+
+ // MARK: - Private Update Methods
+ private func updateIdentification(address: Int, icaoName: String) {
+ let aircraft = getOrCreateAircraft(address: address)
+ let updated = Aircraft(
+ id: aircraft.id,
+ icaoAddress: aircraft.icaoAddress,
+ icaoName: icaoName,
+ position: aircraft.position,
+ altitude: aircraft.altitude,
+ lastSeen: Date(),
+ flightInfo: aircraft.flightInfo
+ )
+
+ aircraftCache[address] = updated
+ publishUpdate()
+ }
+
+ private func updatePosition(address: Int, latitude: Double, longitude: Double) {
+ Task {
+ // Update the tracker for position calculation
+ await tracker.updatePosition(address: address, latitude: latitude, longitude: longitude)
+
+ // Get calculated position from tracker
+ if let calculatedPosition = await tracker.getPosition(address: address) {
+ let aircraft = getOrCreateAircraft(address: address)
+ let position = Aircraft.Position(
+ latitude: calculatedPosition.0,
+ longitude: calculatedPosition.1,
+ accuracy: .medium, // TODO: Determine accuracy from tracker
+ timestamp: Date()
+ )
+
+ let updated = Aircraft(
+ id: aircraft.id,
+ icaoAddress: aircraft.icaoAddress,
+ icaoName: aircraft.icaoName,
+ position: position,
+ altitude: aircraft.altitude,
+ lastSeen: Date(),
+ flightInfo: aircraft.flightInfo
+ )
+
+ aircraftCache[address] = updated
+ publishUpdate()
+ }
+ }
+ }
+
+ private func updateAltitude(address: Int, altitude: Int) {
+ let aircraft = getOrCreateAircraft(address: address)
+ let updated = Aircraft(
+ id: aircraft.id,
+ icaoAddress: aircraft.icaoAddress,
+ icaoName: aircraft.icaoName,
+ position: aircraft.position,
+ altitude: altitude,
+ lastSeen: Date(),
+ flightInfo: aircraft.flightInfo
+ )
+
+ aircraftCache[address] = updated
+ publishUpdate()
+ }
+
+ private func getOrCreateAircraft(address: Int) -> Aircraft {
+ if let existing = aircraftCache[address] {
+ return existing
+ }
+
+ let newAircraft = Aircraft(
+ id: address,
+ icaoAddress: address,
+ icaoName: nil,
+ position: nil,
+ altitude: nil,
+ lastSeen: Date(),
+ flightInfo: nil
+ )
+
+ return newAircraft
+ }
+
+ private func publishUpdate() {
+ let currentAircraft = getAllAircraft()
+ aircraftSubject.send(currentAircraft)
+ }
+
+ private func startCleanupTimer() {
+ Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in
+ self?.removeInactiveAircraft()
+ }
+ }
+}
+
+// MARK: - Aircraft Tracker Protocol
+protocol AircraftTracker: Actor {
+ func updateIdentification(address: Int, icaoName: String) async
+ func updatePosition(address: Int, latitude: Double, longitude: Double) async
+ func updateAltitude(address: Int, altitude: Int) async
+ func getPosition(address: Int) async -> (Double, Double)?
+ func getAltitude(address: Int) async -> Int?
+ func getICAOName(address: Int) async -> String?
+}
+
+// MARK: - Default Aircraft Tracker (Adapter for existing AirPlaneTracker)
+actor DefaultAircraftTracker: AircraftTracker {
+ private let airplaneTracker = AirPlaneTracker()
+
+ func updateIdentification(address: Int, icaoName: String) async {
+ airplaneTracker.addDF17Indentification(address, icaoName)
+ }
+
+ func updatePosition(address: Int, latitude: Double, longitude: Double) async {
+ // Note: The original tracker expects CPR coordinates, not lat/long
+ // This is a simplified adapter - you might need to adjust based on your data flow
+ airplaneTracker.addDF17AirBornPosition(address, latitude, longitude, 0, true)
+ }
+
+ func updateAltitude(address: Int, altitude: Int) async {
+ // Altitude is typically updated with position in the original tracker
+ // This might need adjustment based on your specific needs
+ }
+
+ func getPosition(address: Int) async -> (Double, Double)? {
+ return airplaneTracker.getPosition(address)
+ }
+
+ func getAltitude(address: Int) async -> Int? {
+ return airplaneTracker.getAltitude(address)
+ }
+
+ func getICAOName(address: Int) async -> String? {
+ return airplaneTracker.getICAOname(address)
+ }
+} \ No newline at end of file
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