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