summaryrefslogtreecommitdiff
path: root/LearnMapKit/DataSources/FileADSBDataSource.swift
blob: 448721b46c6710159ae7408bd44edbdf308f324c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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)"
        }
    }
}