summaryrefslogtreecommitdiff
path: root/LearnMapKit/Managers
diff options
context:
space:
mode:
authorArturs Artamonovs <arturs.artamonovs@protonmail.com>2025-11-17 00:44:09 +0000
committerArturs Artamonovs <arturs.artamonovs@protonmail.com>2025-11-17 00:44:09 +0000
commite26830439dfdb97c1d685bf5cda05a5d2df9f21f (patch)
tree85a16662d47787d0975c05a22727ad4301d0fb5b /LearnMapKit/Managers
parent2b599499dc26f0f93537d1bea4733eae68333a1c (diff)
downloadADSBDecoder-e26830439dfdb97c1d685bf5cda05a5d2df9f21f.tar.gz
ADSBDecoder-e26830439dfdb97c1d685bf5cda05a5d2df9f21f.zip
refactor experiment
Diffstat (limited to 'LearnMapKit/Managers')
-rw-r--r--LearnMapKit/Managers/ConfigurationManager.swift322
1 files changed, 322 insertions, 0 deletions
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