From 909ec06fd9a159c27e811892b94a8e8879da30e1 Mon Sep 17 00:00:00 2001 From: Jacob Powers Date: Tue, 22 Jul 2025 00:48:50 +0000 Subject: [PATCH] wip --- Localizable.xcstrings | 14 +- Meshtastic/Helpers/GeoJSONOverlayConfig.swift | 268 ++++++++++++++++-- .../Helpers/GeoJSONOverlayManager.swift | 171 +++++------ Meshtastic/Helpers/MapDataManager.swift | 156 +++++++--- .../Map/MapContent/MeshMapContent.swift | 70 +++-- .../Nodes/Helpers/Map/MapSettingsForm.swift | 104 ++++++- 6 files changed, 593 insertions(+), 190 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ee7f5824..1f66cd0a 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1346,6 +1346,9 @@ } } } + }, + "%lld features" : { + }, "%lld or less hops away" : { "localizations" : { @@ -14554,6 +14557,9 @@ } } }, + "Files Available" : { + "comment" : "Data source label when files exist but none are active" + }, "Find a contact" : { "localizations" : { "de" : { @@ -20402,6 +20408,9 @@ } } }, + "Manage map data" : { + "comment" : "Link to manage uploaded map data" + }, "Managed Device" : { "localizations" : { "it" : { @@ -23415,6 +23424,9 @@ } } }, + "No map data files uploaded" : { + "comment" : "Message when no files are uploaded" + }, "No PAX Counter Logs" : { "localizations" : { "it" : { @@ -42813,4 +42825,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Meshtastic/Helpers/GeoJSONOverlayConfig.swift b/Meshtastic/Helpers/GeoJSONOverlayConfig.swift index 5d3a5e8c..6a4860fd 100644 --- a/Meshtastic/Helpers/GeoJSONOverlayConfig.swift +++ b/Meshtastic/Helpers/GeoJSONOverlayConfig.swift @@ -1,34 +1,10 @@ import Foundation import MapKit +import SwiftUI +import CoreLocation +import OSLog -// MARK: - Configuration Models - -struct GeoJSONOverlayConfiguration: Codable { - let version: String - let metadata: OverlayMetadata - let overlays: [OverlayDefinition] -} - -struct OverlayMetadata: Codable { - let name: String - let description: String - let generated: String -} - -struct OverlayDefinition: Codable { - let id: String - let name: String - let description: String - let rendering: RenderingProperties - let geojson: GeoJSONFeatureCollection -} - -struct RenderingProperties: Codable { - let lineColor: String // Hex color (e.g., "#FF0000") - let lineOpacity: Double // 0.0 to 1.0 - let lineThickness: Double // Line width in points - let fillOpacity: Double // 0.0 to 1.0 -} +// MARK: - Raw GeoJSON Support Only struct GeoJSONFeatureCollection: Codable { let type: String // Always "FeatureCollection" @@ -40,6 +16,218 @@ struct GeoJSONFeature: Codable { let id: Int? let geometry: GeoJSONGeometry let properties: [String: AnyCodableValue]? + + // MARK: - GeoJSON Styling Properties + + /// Extract layer metadata from properties + var layerId: String? { + if case .string(let value) = properties?["layer_id"] { + return value + } + return nil + } + + var layerName: String? { + if case .string(let value) = properties?["layer_name"] { + return value + } + return nil + } + + var layerDescription: String? { + if case .string(let value) = properties?["description"] { + return value + } + return nil + } + + var isVisible: Bool { + if case .bool(let value) = properties?["visible"] { + return value + } + return true // Default to visible + } + + // MARK: - Point/Marker Styling + + var markerColor: String? { + if case .string(let value) = properties?["marker-color"] { + return value + } + return nil + } + + var markerSize: String? { + if case .string(let value) = properties?["marker-size"] { + return value + } + return "medium" // Default size + } + + var markerSymbol: String? { + if case .string(let value) = properties?["marker-symbol"] { + return value + } + return nil + } + + // MARK: - Stroke/Line Styling + + var strokeColor: String? { + if case .string(let value) = properties?["stroke"] { + return value + } + return nil + } + + var strokeWidth: Double { + if case .double(let value) = properties?["stroke-width"] { + return value + } else if case .int(let value) = properties?["stroke-width"] { + return Double(value) + } + return 1.0 // Default width + } + + var strokeOpacity: Double { + if case .double(let value) = properties?["stroke-opacity"] { + return value + } else if case .int(let value) = properties?["stroke-opacity"] { + return Double(value) + } + return 1.0 // Default opacity + } + + var lineDashArray: [Double]? { + if case .array(let values) = properties?["line-dasharray"] { + return values.compactMap { value in + switch value { + case .double(let d): return d + case .int(let i): return Double(i) + default: return nil + } + } + } + return nil + } + + // MARK: - Fill Styling + + var fillColor: String? { + if case .string(let value) = properties?["fill"] { + return value + } + return nil + } + + var fillOpacity: Double { + if case .double(let value) = properties?["fill-opacity"] { + return value + } else if case .int(let value) = properties?["fill-opacity"] { + return Double(value) + } + return 0.0 // Default to no fill + } + + // MARK: - Computed Rendering Properties + + /// Get effective stroke color (fallback to marker color for points) + var effectiveStrokeColor: String { + return strokeColor ?? markerColor ?? "#000000" + } + + /// Get effective fill color (fallback to stroke color if fill opacity > 0) + var effectiveFillColor: String { + if fillOpacity > 0 { + return fillColor ?? effectiveStrokeColor + } + return "#000000" + } + + /// Convert marker size to point radius + var markerRadius: CGFloat { + switch markerSize { + case "small": return 8.0 + case "medium": return 12.0 + case "large": return 16.0 + default: return 12.0 + } + } +} + +// MARK: - Styled Feature Wrapper + +/// Wrapper for a GeoJSON feature with its styling properties and metadata +struct GeoJSONStyledFeature: Identifiable { + let id = UUID() + let feature: GeoJSONFeature + let overlayId: String + + /// Create MKOverlay from this styled feature + func createOverlay() -> MKOverlay? { + do { + // Convert feature to standard GeoJSON format for MKGeoJSONDecoder + let featureDict: [String: Any] = [ + "type": feature.type, + "geometry": [ + "type": feature.geometry.type, + "coordinates": feature.geometry.coordinates.toAnyObject() + ], + "properties": feature.properties?.mapValues { $0.toAnyObject() } ?? [:] + ] + + // Creating overlay for geometry + + let geojsonData = try JSONSerialization.data(withJSONObject: featureDict) + let mkFeatures = try MKGeoJSONDecoder().decode(geojsonData) + + // MKGeoJSONDecoder processing + + if let mkFeature = mkFeatures.first as? MKGeoJSONFeature { + // Processing geometry objects + if let geometry = mkFeature.geometry.first as? MKOverlay { + // Successfully created overlay + return geometry + } else { + Logger.services.warning("πŸ—ΊοΈ GeoJSONStyledFeature: First geometry is not an MKOverlay: \(type(of: mkFeature.geometry.first))") + } + } else { + Logger.services.warning("πŸ—ΊοΈ GeoJSONStyledFeature: First feature is not an MKGeoJSONFeature: \(type(of: mkFeatures.first))") + } + } catch { + Logger.services.error("πŸ—ΊοΈ GeoJSONStyledFeature: Failed to convert feature to overlay: \(error.localizedDescription)") + } + return nil + } + + /// Get stroke style for this feature + var strokeStyle: StrokeStyle { + let dashArray = feature.lineDashArray + if let dashArray = dashArray, !dashArray.isEmpty { + return StrokeStyle( + lineWidth: feature.strokeWidth, + lineCap: .round, + lineJoin: .round, + dash: dashArray.map { CGFloat($0) } + ) + } else { + return StrokeStyle( + lineWidth: feature.strokeWidth, + lineCap: .round, + lineJoin: .round + ) + } + } + + /// Get stroke color with opacity + var strokeColor: Color { + return Color(hex: feature.effectiveStrokeColor).opacity(feature.strokeOpacity) + } + + /// Get fill color with opacity + var fillColor: Color { + return Color(hex: feature.effectiveFillColor).opacity(feature.fillOpacity) + } } struct GeoJSONGeometry: Codable { @@ -120,4 +308,28 @@ enum AnyCodableValue: Codable { return dict.mapValues { $0.toAnyObject() } } } + + // Helper to convert Point coordinates to CLLocationCoordinate2D + func toCoordinate() -> CLLocationCoordinate2D? { + if case .array(let coords) = self, + coords.count >= 2 { + let lon: Double + let lat: Double + + switch coords[0] { + case .double(let d): lon = d + case .int(let i): lon = Double(i) + default: return nil + } + + switch coords[1] { + case .double(let d): lat = d + case .int(let i): lat = Double(i) + default: return nil + } + + return CLLocationCoordinate2D(latitude: lat, longitude: lon) + } + return nil + } } \ No newline at end of file diff --git a/Meshtastic/Helpers/GeoJSONOverlayManager.swift b/Meshtastic/Helpers/GeoJSONOverlayManager.swift index 413e06e2..dcffbaaa 100644 --- a/Meshtastic/Helpers/GeoJSONOverlayManager.swift +++ b/Meshtastic/Helpers/GeoJSONOverlayManager.swift @@ -1,122 +1,131 @@ import SwiftUI import MapKit +import OSLog -/// Manager for loading and managing GeoJSON overlays from consolidated configuration +/// Manager for loading and managing raw GeoJSON feature collections with embedded styling class GeoJSONOverlayManager { static let shared = GeoJSONOverlayManager() private init() {} - private var configuration: GeoJSONOverlayConfiguration? - private var overlays: [String: [MKOverlay]] = [:] + private var featureCollection: GeoJSONFeatureCollection? - /// Load user-uploaded configuration only - func loadConfiguration() -> GeoJSONOverlayConfiguration? { - if let cached = configuration { + /// Load raw GeoJSON feature collection from user uploads + func loadFeatureCollection() -> GeoJSONFeatureCollection? { + Logger.services.debug("πŸ—ΊοΈ GeoJSONOverlayManager: loadFeatureCollection() called") + + if let cached = featureCollection { + Logger.services.debug("πŸ—ΊοΈ GeoJSONOverlayManager: Returning cached feature collection with \(cached.features.count) features") return cached } - // Load user-uploaded configuration - if let userConfig = MapDataManager.shared.loadUserConfiguration() { - configuration = userConfig - return userConfig + // Load user-uploaded feature collection + Logger.services.debug("πŸ—ΊοΈ GeoJSONOverlayManager: Loading feature collection from MapDataManager") + if let userFeatures = MapDataManager.shared.loadFeatureCollection() { + Logger.services.info("πŸ—ΊοΈ GeoJSONOverlayManager: Loaded feature collection with \(userFeatures.features.count) features") + featureCollection = userFeatures + return userFeatures } - // No configuration available + // No feature collection available + Logger.services.debug("πŸ—ΊοΈ GeoJSONOverlayManager: No feature collection available") return nil } - /// Load overlays for a specific overlay ID - func loadOverlays(for overlayId: String) -> [MKOverlay] { - if let cached = overlays[overlayId] { - return cached - } - - guard let config = loadConfiguration() else { + /// Load styled features for direct rendering + func loadStyledFeatures() -> [GeoJSONStyledFeature] { + Logger.services.debug("πŸ—ΊοΈ GeoJSONOverlayManager: loadStyledFeatures() called") + + guard let collection = loadFeatureCollection() else { + Logger.services.debug("πŸ—ΊοΈ GeoJSONOverlayManager: No feature collection available, returning empty array") return [] } - - guard let overlayDef = config.overlays.first(where: { $0.id == overlayId }) else { - return [] - } - - do { - // Convert our custom GeoJSON structure to the format expected by MKGeoJSONDecoder - let standardGeoJSON: [String: Any] = [ - "type": overlayDef.geojson.type, - "features": overlayDef.geojson.features.map { feature in - var featureDict: [String: Any] = [ - "type": feature.type, - "geometry": [ - "type": feature.geometry.type, - "coordinates": feature.geometry.coordinates.toAnyObject() - ], - "properties": [:] - ] - - if let id = feature.id { - featureDict["id"] = id - } - - return featureDict - } - ] - - let geojsonData = try JSONSerialization.data(withJSONObject: standardGeoJSON) - let features = try MKGeoJSONDecoder().decode(geojsonData) - - var allOverlays: [MKOverlay] = [] - for (index, feature) in features.enumerated() { - if let mkFeature = feature as? MKGeoJSONFeature { - for (geoIndex, geometry) in mkFeature.geometry.enumerated() { - if let overlay = geometry as? MKOverlay { - allOverlays.append(overlay) - } - } - } + + var styledFeatures: [GeoJSONStyledFeature] = [] + + Logger.services.info("πŸ—ΊοΈ GeoJSONOverlayManager: Processing \(collection.features.count) features") + + for feature in collection.features { + // Skip invisible features + guard feature.isVisible else { + Logger.services.debug("πŸ—ΊοΈ GeoJSONOverlayManager: Skipping invisible feature") + continue } - - overlays[overlayId] = allOverlays - return allOverlays - } catch { - return [] + + let layerId = feature.layerId ?? "default" + let styledFeature = GeoJSONStyledFeature( + feature: feature, + overlayId: layerId + ) + styledFeatures.append(styledFeature) } + + Logger.services.info("πŸ—ΊοΈ GeoJSONOverlayManager: Returning \(styledFeatures.count) styled features") + return styledFeatures } - /// Get rendering properties for an overlay - func getRenderingProperties(for overlayId: String) -> RenderingProperties? { - guard let config = loadConfiguration() else { return nil } - return config.overlays.first(where: { $0.id == overlayId })?.rendering + /// Get all features grouped by layer ID + func getFeaturesByLayer() -> [String: [GeoJSONFeature]] { + guard let collection = loadFeatureCollection() else { return [:] } + + var featuresByLayer: [String: [GeoJSONFeature]] = [:] + + for feature in collection.features { + let layerId = feature.layerId ?? "default" + if featuresByLayer[layerId] == nil { + featuresByLayer[layerId] = [] + } + featuresByLayer[layerId]?.append(feature) + } + + return featuresByLayer } - /// Get all available overlay IDs - func getAvailableOverlayIds() -> [String] { - guard let config = loadConfiguration() else { return [] } - return config.overlays.map { $0.id } + /// Get all available layer IDs from features + func getAvailableLayerIds() -> [String] { + guard let collection = loadFeatureCollection() else { return [] } + let layerIds = Set(collection.features.compactMap { $0.layerId ?? "default" }) + return Array(layerIds).sorted() } - /// Get overlay definition by ID - func getOverlayDefinition(for overlayId: String) -> OverlayDefinition? { - guard let config = loadConfiguration() else { return nil } - return config.overlays.first(where: { $0.id == overlayId }) - } - - /// Clear cached overlays (useful for testing or memory management) + /// Clear cached data (useful for testing or memory management) func clearCache() { - overlays.removeAll() - configuration = nil + Logger.services.info("πŸ—ΊοΈ GeoJSONOverlayManager: Clearing cache") + featureCollection = nil } - /// Check if user-uploaded data is available + /// Check if user-uploaded data is available (regardless of active state) func hasUserData() -> Bool { + return !MapDataManager.shared.getUploadedFiles().isEmpty + } + + /// Check if there are any active files + func hasActiveData() -> Bool { return MapDataManager.shared.getUploadedFiles().contains { $0.isActive } } /// Get the active data source name func getActiveDataSource() -> String { - if hasUserData() { + if hasActiveData() { return NSLocalizedString("User Uploaded", comment: "Data source label for user uploaded files") + } else if hasUserData() { + return NSLocalizedString("Files Available", comment: "Data source label when files exist but none are active") } else { return NSLocalizedString("No Data", comment: "Data source label when no files are available") } } + + // MARK: - File-based Filtering + + /// Get all uploaded files with their active states for UI display + func getUploadedFilesWithState() -> [MapDataMetadata] { + return MapDataManager.shared.getUploadedFiles() + } + + /// Toggle the active state of an uploaded file + func toggleFileActive(_ fileId: UUID) { + Logger.services.debug("πŸ—ΊοΈ GeoJSONOverlayManager: Toggling active state for file: \(fileId)") + MapDataManager.shared.toggleFileActive(fileId) + // Clear cache to force reload with new file states + clearCache() + } } \ No newline at end of file diff --git a/Meshtastic/Helpers/MapDataManager.swift b/Meshtastic/Helpers/MapDataManager.swift index cdda7305..50ebb0eb 100644 --- a/Meshtastic/Helpers/MapDataManager.swift +++ b/Meshtastic/Helpers/MapDataManager.swift @@ -15,7 +15,7 @@ class MapDataManager { // MARK: - Properties private var uploadedFiles: [MapDataMetadata] = [] - private var activeConfiguration: GeoJSONOverlayConfiguration? + private var activeFeatureCollection: GeoJSONFeatureCollection? // MARK: - File Management @@ -96,7 +96,7 @@ class MapDataManager { try saveMetadata() // 7. Clear cached configuration to force reload - activeConfiguration = nil + activeFeatureCollection = nil Logger.services.info("πŸ“ Successfully processed file: \(newFilename, privacy: .public)") return metadata @@ -131,7 +131,7 @@ class MapDataManager { let uploadDate = fileAttributes.creationDate ?? Date() // Read and process file content on background queue - let (processedData, overlayCount) = try await withCheckedThrowingContinuation { continuation in + let (_, overlayCount) = try await withCheckedThrowingContinuation { continuation in Task.detached { do { let data = try Data(contentsOf: url) @@ -172,50 +172,84 @@ class MapDataManager { } } - /// Get overlay count from processed data + /// Get overlay count from raw GeoJSON data private func getOverlayCount(from data: Data) throws -> Int { - do { - let config = try JSONDecoder().decode(GeoJSONOverlayConfiguration.self, from: data) - return config.overlays.count - } catch { - // Try parsing as raw GeoJSON - if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], - let features = json["features"] as? [[String: Any]] { - return features.count - } - throw MapDataError.invalidContent + // Parse as raw GeoJSON FeatureCollection + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let features = json["features"] as? [[String: Any]] { + return features.count } + throw MapDataError.invalidContent } // MARK: - Configuration Loading - /// Load user configuration (priority over bundled) - func loadUserConfiguration() -> GeoJSONOverlayConfiguration? { - if let cached = activeConfiguration { + /// Load and combine raw GeoJSON feature collections from all active files + func loadFeatureCollection() -> GeoJSONFeatureCollection? { + if let cached = activeFeatureCollection { + Logger.services.debug("πŸ“ MapDataManager: Returning cached feature collection") return cached } // Find active user files let activeFiles = uploadedFiles.filter { $0.isActive } - guard let activeFile = activeFiles.first else { + Logger.services.debug("πŸ“ MapDataManager: Found \(activeFiles.count) active files out of \(self.uploadedFiles.count) total files") + + guard !activeFiles.isEmpty else { + Logger.services.debug("πŸ“ MapDataManager: No active files found") return nil } - guard let fileURL = getUserUploadedDirectory()?.appendingPathComponent(activeFile.filename) else { - return nil - } + var allFeatures: [GeoJSONFeature] = [] + + // Load features from all active files + for activeFile in activeFiles { + Logger.services.info("πŸ“ MapDataManager: Attempting to load active file: \(activeFile.filename, privacy: .public)") - do { - let data = try Data(contentsOf: fileURL) - let processedData = try processData(data, filename: activeFile.filename) - let config = try JSONDecoder().decode(GeoJSONOverlayConfiguration.self, from: processedData) + guard let fileURL = getUserUploadedDirectory()?.appendingPathComponent(activeFile.filename) else { + Logger.services.error("πŸ“ MapDataManager: Could not construct file URL for: \(activeFile.filename, privacy: .public)") + continue + } - activeConfiguration = config - return config - } catch { - Logger.services.error("πŸ“ Failed to load user configuration: \(error.localizedDescription, privacy: .public)") - return nil + // Check if file exists before trying to load it + if !FileManager.default.fileExists(atPath: fileURL.path) { + Logger.services.error("πŸ“ MapDataManager: Active file does not exist at path: \(fileURL.path, privacy: .public)") + Logger.services.info("πŸ“ MapDataManager: Removing missing file from metadata") + + // Remove the missing file from our metadata + if let index = uploadedFiles.firstIndex(where: { $0.filename == activeFile.filename }) { + uploadedFiles.remove(at: index) + do { + try saveMetadata() + Logger.services.info("πŸ“ MapDataManager: Successfully cleaned up missing file from metadata") + } catch { + Logger.services.error("πŸ“ MapDataManager: Failed to save cleaned metadata: \(error.localizedDescription, privacy: .public)") + } + } + continue + } + + do { + let data = try Data(contentsOf: fileURL) + let processedData = try processData(data, filename: activeFile.filename) + let featureCollection = try JSONDecoder().decode(GeoJSONFeatureCollection.self, from: processedData) + + Logger.services.info("πŸ“ MapDataManager: Successfully loaded \(featureCollection.features.count) features from \(activeFile.filename, privacy: .public)") + allFeatures.append(contentsOf: featureCollection.features) + } catch { + Logger.services.error("πŸ“ MapDataManager: Failed to load feature collection from \(activeFile.filename, privacy: .public): \(error.localizedDescription, privacy: .public)") + } } + + // Create combined feature collection + let combinedCollection = GeoJSONFeatureCollection( + type: "FeatureCollection", + features: allFeatures + ) + + Logger.services.info("πŸ“ MapDataManager: Successfully combined \(allFeatures.count) total features from \(activeFiles.count) active files") + activeFeatureCollection = combinedCollection + return combinedCollection } // MARK: - File Management @@ -224,27 +258,77 @@ class MapDataManager { func getUploadedFiles() -> [MapDataMetadata] { return uploadedFiles } + + /// Toggle the active state of an uploaded file + func toggleFileActive(_ fileId: UUID) { + Logger.services.debug("πŸ“ MapDataManager: Toggling active state for file: \(fileId)") + + if let index = uploadedFiles.firstIndex(where: { $0.id == fileId }) { + uploadedFiles[index].isActive.toggle() + Logger.services.info("πŸ“ MapDataManager: File '\(self.uploadedFiles[index].filename)' active state: \(self.uploadedFiles[index].isActive)") + + // Save metadata changes + do { + try saveMetadata() + // Clear cached data to force reload + activeFeatureCollection = nil + } catch { + Logger.services.error("πŸ“ MapDataManager: Failed to save metadata after toggling file: \(error.localizedDescription)") + } + } else { + Logger.services.error("πŸ“ MapDataManager: Could not find file with ID: \(fileId)") + } + } /// Delete uploaded file func deleteFile(_ metadata: MapDataMetadata) throws { + Logger.services.info("πŸ—‘οΈ MapDataManager: Attempting to delete file: \(metadata.filename, privacy: .public)") + guard let fileURL = getUserUploadedDirectory()?.appendingPathComponent(metadata.filename) else { + Logger.services.error("πŸ—‘οΈ MapDataManager: Could not construct file URL for: \(metadata.filename, privacy: .public)") throw MapDataError.fileNotFound } - try FileManager.default.removeItem(at: fileURL) + Logger.services.debug("πŸ—‘οΈ MapDataManager: File URL: \(fileURL.path, privacy: .public)") + + // Check if file exists before trying to delete + if !FileManager.default.fileExists(atPath: fileURL.path) { + Logger.services.warning("πŸ—‘οΈ MapDataManager: File does not exist at path: \(fileURL.path, privacy: .public)") + } + + do { + try FileManager.default.removeItem(at: fileURL) + Logger.services.info("πŸ—‘οΈ MapDataManager: Successfully removed file from filesystem") + } catch { + Logger.services.error("πŸ—‘οΈ MapDataManager: Failed to remove file: \(error.localizedDescription, privacy: .public)") + throw error + } if let index = uploadedFiles.firstIndex(where: { $0.filename == metadata.filename }) { uploadedFiles.remove(at: index) + Logger.services.debug("πŸ—‘οΈ MapDataManager: Removed file from uploadedFiles array at index \(index)") + } else { + Logger.services.warning("πŸ—‘οΈ MapDataManager: File not found in uploadedFiles array") } - try saveMetadata() + do { + try saveMetadata() + Logger.services.debug("πŸ—‘οΈ MapDataManager: Successfully saved updated metadata") + } catch { + Logger.services.error("πŸ—‘οΈ MapDataManager: Failed to save metadata: \(error.localizedDescription, privacy: .public)") + throw error + } // Clear cache if this was the active file - if activeConfiguration != nil { - activeConfiguration = nil + if activeFeatureCollection != nil { + activeFeatureCollection = nil + Logger.services.debug("πŸ—‘οΈ MapDataManager: Cleared active configuration cache") } + + // Clear GeoJSON overlay manager cache + GeoJSONOverlayManager.shared.clearCache() - Logger.services.info("πŸ—‘οΈ Deleted file: \(metadata.filename, privacy: .public)") + Logger.services.info("πŸ—‘οΈ MapDataManager: Successfully deleted file: \(metadata.filename, privacy: .public)") } /// Toggle file active status @@ -265,7 +349,7 @@ class MapDataManager { try saveMetadata() // Clear cache to force reload - activeConfiguration = nil + activeFeatureCollection = nil } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 8e550d20..cfa92edf 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -7,6 +7,8 @@ import SwiftUI import MapKit +import CoreLocation +import OSLog struct IdentifiableOverlay: Identifiable { let overlay: MKOverlay @@ -231,37 +233,9 @@ struct MeshMapContent: MapContent { } } - /// GeoJSON Overlays (Configuration-Driven) + /// GeoJSON Overlays with embedded styling if showMapOverlays { - let overlayManager = GeoJSONOverlayManager.shared - let availableOverlays = overlayManager.getAvailableOverlayIds() - - ForEach(Array(availableOverlays.enumerated()), id: \.element) { _, overlayId in - let overlays = overlayManager.loadOverlays(for: overlayId) - let rendering = overlayManager.getRenderingProperties(for: overlayId) - - ForEach(Array(overlays.enumerated()), id: \.offset) { _, overlay in - if let polygon = overlay as? MKPolygon { - MapPolygon(polygon) - .stroke( - Color(hex: rendering?.lineColor ?? "#000000") - .opacity(rendering?.lineOpacity ?? 1.0), - lineWidth: rendering?.lineThickness ?? 1.0 - ) - .foregroundStyle( - Color(hex: rendering?.lineColor ?? "#000000") - .opacity(rendering?.fillOpacity ?? 0.0) - ) - } else if let polyline = overlay as? MKPolyline { - MapPolyline(polyline) - .stroke( - Color(hex: rendering?.lineColor ?? "#000000") - .opacity(rendering?.lineOpacity ?? 1.0), - lineWidth: rendering?.lineThickness ?? 1.0 - ) - } - } - } + overlayContent } positionAnnotations @@ -269,6 +243,42 @@ struct MeshMapContent: MapContent { waypointAnnotations } + var overlayContent: some MapContent { + let styledFeatures = GeoJSONOverlayManager.shared.loadStyledFeatures() + + return Group { + ForEach(0..