diff options
Diffstat (limited to 'LearnMapKit/DataSources/FileADSBDataSource.swift')
| -rw-r--r-- | LearnMapKit/DataSources/FileADSBDataSource.swift | 185 |
1 files changed, 185 insertions, 0 deletions
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 |
