summaryrefslogtreecommitdiff
path: root/LearnMapKit/ViewModels/AircraftTrackingViewModel.swift
blob: 048418fc4161c5325d26afabdb7b90e30aa11dfe (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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
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
        }
    }
}