mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
updates
This commit is contained in:
parent
220a062114
commit
6adefdb3aa
12 changed files with 433 additions and 154 deletions
|
|
@ -5848,70 +5848,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Map Overlays" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Karten-Overlays"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Superpositions de cartes"
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "שכבות מפה"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sovrapposizioni mappa"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "マップオーバーレイ"
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nakładki map"
|
||||
}
|
||||
},
|
||||
"se" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Kartöverlägg"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Преклапања мапе"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "地图覆盖层"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "地圖覆蓋層"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Button GPIO" : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
|
|
@ -20566,6 +20502,70 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Map Overlays" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Karten-Overlays"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Superpositions de cartes"
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "שכבות מפה"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sovrapposizioni mappa"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "マップオーバーレイ"
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nakładki map"
|
||||
}
|
||||
},
|
||||
"se" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Kartöverlägg"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Преклапања мапе"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "地图覆盖层"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "地圖覆蓋層"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Map Publish Interval" : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
|
|
@ -37980,9 +37980,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Toilets" : {
|
||||
|
||||
},
|
||||
"Topic: %@" : {
|
||||
"localizations" : {
|
||||
|
|
|
|||
|
|
@ -57,9 +57,10 @@
|
|||
25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5C12C3F6E4B008036E3 /* AppState.swift */; };
|
||||
25F5D5D12C4375DF008036E3 /* RouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5D02C4375DF008036E3 /* RouterTests.swift */; };
|
||||
3D3417B42E2730EC006A988B /* GeoJSONOverlayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */; };
|
||||
3D3417BC2E273AC6006A988B /* Street_Outlines.geojson in Resources */ = {isa = PBXBuildFile; fileRef = 3D3417BB2E273AC6006A988B /* Street_Outlines.geojson */; };
|
||||
3D3417C32E274800006A988B /* Toilets.geojson in Resources */ = {isa = PBXBuildFile; fileRef = 3D3417C12E274800006A988B /* Toilets.geojson */; };
|
||||
3D3417C42E274800006A988B /* Trash_Fence.geojson in Resources */ = {isa = PBXBuildFile; fileRef = 3D3417C22E274800006A988B /* Trash_Fence.geojson */; };
|
||||
3D3417C82E29D38A006A988B /* GeoJSONOverlayConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417C72E29D38A006A988B /* GeoJSONOverlayConfig.swift */; };
|
||||
3D3417CB2E29D3B0006A988B /* Color+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417C92E29D3B0006A988B /* Color+Hex.swift */; };
|
||||
3D3417CC2E29D3B0006A988B /* Data+Gzip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417CA2E29D3B0006A988B /* Data+Gzip.swift */; };
|
||||
3D3417CE2E29D5D6006A988B /* BurningManGeoJSONMapConfig.json.zlib in Resources */ = {isa = PBXBuildFile; fileRef = 3D3417CD2E29D5D6006A988B /* BurningManGeoJSONMapConfig.json.zlib */; };
|
||||
6D825E622C34786C008DBEE4 /* CommonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D825E612C34786C008DBEE4 /* CommonRegex.swift */; };
|
||||
6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; };
|
||||
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; };
|
||||
|
|
@ -331,9 +332,10 @@
|
|||
25F5D5C72C4375A8008036E3 /* MeshtasticTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeshtasticTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
25F5D5D02C4375DF008036E3 /* RouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterTests.swift; sourceTree = "<group>"; };
|
||||
3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSONOverlayManager.swift; sourceTree = "<group>"; };
|
||||
3D3417BB2E273AC6006A988B /* Street_Outlines.geojson */ = {isa = PBXFileReference; lastKnownFileType = text; path = Street_Outlines.geojson; sourceTree = "<group>"; };
|
||||
3D3417C12E274800006A988B /* Toilets.geojson */ = {isa = PBXFileReference; lastKnownFileType = text; path = Toilets.geojson; sourceTree = "<group>"; };
|
||||
3D3417C22E274800006A988B /* Trash_Fence.geojson */ = {isa = PBXFileReference; lastKnownFileType = text; path = Trash_Fence.geojson; sourceTree = "<group>"; };
|
||||
3D3417C72E29D38A006A988B /* GeoJSONOverlayConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSONOverlayConfig.swift; sourceTree = "<group>"; };
|
||||
3D3417C92E29D3B0006A988B /* Color+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Hex.swift"; sourceTree = "<group>"; };
|
||||
3D3417CA2E29D3B0006A988B /* Data+Gzip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Gzip.swift"; sourceTree = "<group>"; };
|
||||
3D3417CD2E29D5D6006A988B /* BurningManGeoJSONMapConfig.json.zlib */ = {isa = PBXFileReference; lastKnownFileType = file; path = BurningManGeoJSONMapConfig.json.zlib; sourceTree = "<group>"; };
|
||||
6D825E612C34786C008DBEE4 /* CommonRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonRegex.swift; sourceTree = "<group>"; };
|
||||
6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = "<group>"; };
|
||||
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -1055,12 +1057,10 @@
|
|||
DDC2E18926CE24F70042C5E4 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3D3417CD2E29D5D6006A988B /* BurningManGeoJSONMapConfig.json.zlib */,
|
||||
DDB75A192A05EB67006ED576 /* alpha.png */,
|
||||
DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */,
|
||||
DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */,
|
||||
3D3417BB2E273AC6006A988B /* Street_Outlines.geojson */,
|
||||
3D3417C12E274800006A988B /* Toilets.geojson */,
|
||||
3D3417C22E274800006A988B /* Trash_Fence.geojson */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1110,6 +1110,7 @@
|
|||
DDC2E1A526CEB32B0042C5E4 /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3D3417C72E29D38A006A988B /* GeoJSONOverlayConfig.swift */,
|
||||
BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */,
|
||||
DDD43FE12A78C86B0083A3E9 /* Mqtt */,
|
||||
DDAF8C5226EB1DF10058C060 /* BLEManager.swift */,
|
||||
|
|
@ -1178,6 +1179,8 @@
|
|||
DDDB443E29F79A9400EE2349 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3D3417C92E29D3B0006A988B /* Color+Hex.swift */,
|
||||
3D3417CA2E29D3B0006A988B /* Data+Gzip.swift */,
|
||||
DD007BB12AA59B9A00F5FA12 /* CoreData */,
|
||||
DDFFA7462B3A7F3C004730DB /* Bundle.swift */,
|
||||
DDDB444529F8A96500EE2349 /* Character.swift */,
|
||||
|
|
@ -1372,11 +1375,9 @@
|
|||
DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */,
|
||||
25AECD4F2C2F723200862C8E /* Localizable.xcstrings in Resources */,
|
||||
DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */,
|
||||
3D3417BC2E273AC6006A988B /* Street_Outlines.geojson in Resources */,
|
||||
DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */,
|
||||
3D3417C32E274800006A988B /* Toilets.geojson in Resources */,
|
||||
3D3417C42E274800006A988B /* Trash_Fence.geojson in Resources */,
|
||||
DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */,
|
||||
3D3417CE2E29D5D6006A988B /* BurningManGeoJSONMapConfig.json.zlib in Resources */,
|
||||
DD0E21012B8A6F1300F2D100 /* DeviceHardware.json in Resources */,
|
||||
DDDBC87B2BC62E4E001E8DF7 /* Settings.bundle in Resources */,
|
||||
);
|
||||
|
|
@ -1492,6 +1493,7 @@
|
|||
DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */,
|
||||
DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */,
|
||||
3D3417B42E2730EC006A988B /* GeoJSONOverlayManager.swift in Sources */,
|
||||
3D3417C82E29D38A006A988B /* GeoJSONOverlayConfig.swift in Sources */,
|
||||
237B46962DC8F1C100B22D99 /* RateLimitedButton.swift in Sources */,
|
||||
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */,
|
||||
DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */,
|
||||
|
|
@ -1589,6 +1591,8 @@
|
|||
DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */,
|
||||
DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */,
|
||||
DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */,
|
||||
3D3417CB2E29D3B0006A988B /* Color+Hex.swift in Sources */,
|
||||
3D3417CC2E29D3B0006A988B /* Data+Gzip.swift in Sources */,
|
||||
DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */,
|
||||
BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */,
|
||||
DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */,
|
||||
|
|
|
|||
30
Meshtastic/Extensions/Color+Hex.swift
Normal file
30
Meshtastic/Extensions/Color+Hex.swift
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import SwiftUI
|
||||
|
||||
extension Color {
|
||||
/// Initialize a Color from a hex string (e.g., "#FF0000" or "FF0000")
|
||||
init(hex: String) {
|
||||
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var int: UInt64 = 0
|
||||
Scanner(string: hex).scanHexInt64(&int)
|
||||
|
||||
let a, r, g, b: UInt64
|
||||
switch hex.count {
|
||||
case 3: // RGB (12-bit)
|
||||
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||
case 6: // RGB (24-bit)
|
||||
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||
case 8: // ARGB (32-bit)
|
||||
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||
default:
|
||||
(a, r, g, b) = (255, 0, 0, 0)
|
||||
}
|
||||
|
||||
self.init(
|
||||
.sRGB,
|
||||
red: Double(r) / 255,
|
||||
green: Double(g) / 255,
|
||||
blue: Double(b) / 255,
|
||||
opacity: Double(a) / 255
|
||||
)
|
||||
}
|
||||
}
|
||||
63
Meshtastic/Extensions/Data+Gzip.swift
Normal file
63
Meshtastic/Extensions/Data+Gzip.swift
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import Foundation
|
||||
import Compression
|
||||
|
||||
extension Data {
|
||||
/// Decompresses raw deflate data
|
||||
func zlibDecompressed() throws -> Data {
|
||||
guard self.count > 0 else { return Data() }
|
||||
|
||||
// Try Foundation's zlib first
|
||||
do {
|
||||
let decompressedData = try (self as NSData).decompressed(using: .zlib) as Data
|
||||
print("Data+Zlib: Successfully decompressed with Foundation \(count) bytes to \(decompressedData.count) bytes")
|
||||
return decompressedData
|
||||
} catch {
|
||||
print("Data+Zlib: Foundation decompression failed: \(error), trying raw deflate...")
|
||||
}
|
||||
|
||||
// Fallback to Compression framework with raw deflate
|
||||
let bufferSize = count * 10
|
||||
let destination = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
||||
defer { destination.deallocate() }
|
||||
|
||||
return try self.withUnsafeBytes { bytes in
|
||||
let source = bytes.bindMemory(to: UInt8.self)
|
||||
|
||||
let result = compression_decode_buffer(
|
||||
destination, bufferSize,
|
||||
source.baseAddress!, count,
|
||||
nil, COMPRESSION_ZLIB
|
||||
)
|
||||
|
||||
guard result > 0 else {
|
||||
print("Data+Zlib: Raw deflate decompression also failed, result size: \(result)")
|
||||
throw ZlibError.decompression
|
||||
}
|
||||
|
||||
print("Data+Zlib: Successfully decompressed with raw deflate \(count) bytes to \(result) bytes")
|
||||
return Data(bytes: destination, count: result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ZlibError: Error {
|
||||
case decompression
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .decompression:
|
||||
return "Failed to decompress data"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum GzipError: Error {
|
||||
case decompression
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .decompression:
|
||||
return "Failed to decompress gzip data"
|
||||
}
|
||||
}
|
||||
}
|
||||
123
Meshtastic/Helpers/GeoJSONOverlayConfig.swift
Normal file
123
Meshtastic/Helpers/GeoJSONOverlayConfig.swift
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import Foundation
|
||||
import MapKit
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
struct GeoJSONFeatureCollection: Codable {
|
||||
let type: String // Always "FeatureCollection"
|
||||
let features: [GeoJSONFeature]
|
||||
}
|
||||
|
||||
struct GeoJSONFeature: Codable {
|
||||
let type: String // Always "Feature"
|
||||
let id: Int?
|
||||
let geometry: GeoJSONGeometry
|
||||
let properties: [String: AnyCodableValue]?
|
||||
}
|
||||
|
||||
struct GeoJSONGeometry: Codable {
|
||||
let type: String // "Point", "LineString", "Polygon", etc.
|
||||
let coordinates: AnyCodableValue // Flexible coordinate structure
|
||||
}
|
||||
|
||||
// MARK: - Flexible JSON Value Type
|
||||
|
||||
enum AnyCodableValue: Codable {
|
||||
case string(String)
|
||||
case int(Int)
|
||||
case double(Double)
|
||||
case bool(Bool)
|
||||
case array([AnyCodableValue])
|
||||
case object([String: AnyCodableValue])
|
||||
case null
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
if container.decodeNil() {
|
||||
self = .null
|
||||
} else if let value = try? container.decode(Bool.self) {
|
||||
self = .bool(value)
|
||||
} else if let value = try? container.decode(Int.self) {
|
||||
self = .int(value)
|
||||
} else if let value = try? container.decode(Double.self) {
|
||||
self = .double(value)
|
||||
} else if let value = try? container.decode(String.self) {
|
||||
self = .string(value)
|
||||
} else if let value = try? container.decode([AnyCodableValue].self) {
|
||||
self = .array(value)
|
||||
} else if let value = try? container.decode([String: AnyCodableValue].self) {
|
||||
self = .object(value)
|
||||
} else {
|
||||
throw DecodingError.typeMismatch(AnyCodableValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unable to decode AnyCodableValue"))
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
switch self {
|
||||
case .null:
|
||||
try container.encodeNil()
|
||||
case .bool(let value):
|
||||
try container.encode(value)
|
||||
case .int(let value):
|
||||
try container.encode(value)
|
||||
case .double(let value):
|
||||
try container.encode(value)
|
||||
case .string(let value):
|
||||
try container.encode(value)
|
||||
case .array(let value):
|
||||
try container.encode(value)
|
||||
case .object(let value):
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to convert coordinates to the format expected by MKGeoJSONDecoder
|
||||
func toAnyObject() -> Any {
|
||||
switch self {
|
||||
case .null:
|
||||
return NSNull()
|
||||
case .bool(let value):
|
||||
return value
|
||||
case .int(let value):
|
||||
return value
|
||||
case .double(let value):
|
||||
return value
|
||||
case .string(let value):
|
||||
return value
|
||||
case .array(let values):
|
||||
return values.map { $0.toAnyObject() }
|
||||
case .object(let dict):
|
||||
return dict.mapValues { $0.toAnyObject() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +1,101 @@
|
|||
import SwiftUI
|
||||
import MapKit
|
||||
|
||||
/// Enum for supported static overlays
|
||||
enum StaticGeoJSONOverlay: String, CaseIterable {
|
||||
case streetOutlines = "Street_Outlines"
|
||||
case toilets = "Toilets"
|
||||
case trashFence = "Trash_Fence"
|
||||
|
||||
var filename: String { self.rawValue + ".geojson" }
|
||||
}
|
||||
|
||||
/// Manager for loading and adding GeoJSON overlays
|
||||
/// Manager for loading and managing GeoJSON overlays from consolidated configuration
|
||||
class GeoJSONOverlayManager {
|
||||
static let shared = GeoJSONOverlayManager()
|
||||
private init() {}
|
||||
|
||||
private var overlays: [StaticGeoJSONOverlay: [MKOverlay]] = [:]
|
||||
private var configuration: GeoJSONOverlayConfiguration?
|
||||
private var overlays: [String: [MKOverlay]] = [:]
|
||||
|
||||
/// Load overlays for a given type (from bundle)
|
||||
func loadOverlays(for type: StaticGeoJSONOverlay) -> [MKOverlay] {
|
||||
print("GeoJSONOverlayManager: Attempting to load overlays for \(type.rawValue)")
|
||||
if let cached = overlays[type] {
|
||||
print("GeoJSONOverlayManager: Returning cached overlays for \(type.rawValue), count: \(cached.count)")
|
||||
/// Load and decompress the consolidated configuration
|
||||
func loadConfiguration() -> GeoJSONOverlayConfiguration? {
|
||||
if let cached = configuration {
|
||||
return cached
|
||||
}
|
||||
guard let url = Bundle.main.url(forResource: type.rawValue, withExtension: "geojson") else {
|
||||
print("GeoJSONOverlayManager: No file found for \(type.rawValue).geojson")
|
||||
|
||||
guard let url = Bundle.main.url(forResource: "BurningManGeoJSONMapConfig", withExtension: "json.zlib") else {
|
||||
print("GeoJSONOverlayManager: No compressed configuration file found")
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let compressedData = try Data(contentsOf: url)
|
||||
print("GeoJSONOverlayManager: Loaded compressed data size: \(compressedData.count) bytes")
|
||||
|
||||
let decompressedData = try compressedData.zlibDecompressed()
|
||||
print("GeoJSONOverlayManager: Decompressed data size: \(decompressedData.count) bytes")
|
||||
|
||||
// Debug: Check the first few characters of decompressed data
|
||||
if let decompressedString = String(data: decompressedData, encoding: .utf8) {
|
||||
let firstChars = String(decompressedString.prefix(100))
|
||||
print("GeoJSONOverlayManager: First 100 chars of decompressed data: \(firstChars)")
|
||||
} else {
|
||||
print("GeoJSONOverlayManager: Decompressed data is not valid UTF-8")
|
||||
// Show first few bytes as hex
|
||||
let firstBytes = decompressedData.prefix(20).map { String(format: "%02x", $0) }.joined()
|
||||
print("GeoJSONOverlayManager: First 20 bytes (hex): \(firstBytes)")
|
||||
}
|
||||
|
||||
let config = try JSONDecoder().decode(GeoJSONOverlayConfiguration.self, from: decompressedData)
|
||||
print("GeoJSONOverlayManager: Loaded configuration with \(config.overlays.count) overlays")
|
||||
|
||||
configuration = config
|
||||
return config
|
||||
} catch {
|
||||
print("GeoJSONOverlayManager: Failed to load configuration: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Load overlays for a specific overlay ID
|
||||
func loadOverlays(for overlayId: String) -> [MKOverlay] {
|
||||
print("GeoJSONOverlayManager: Attempting to load overlays for \(overlayId)")
|
||||
|
||||
if let cached = overlays[overlayId] {
|
||||
print("GeoJSONOverlayManager: Returning cached overlays for \(overlayId), count: \(cached.count)")
|
||||
return cached
|
||||
}
|
||||
|
||||
guard let config = loadConfiguration() else {
|
||||
print("GeoJSONOverlayManager: Failed to load configuration")
|
||||
return []
|
||||
}
|
||||
print("GeoJSONOverlayManager: Found file at: \(url)")
|
||||
|
||||
guard let overlayDef = config.overlays.first(where: { $0.id == overlayId }) else {
|
||||
print("GeoJSONOverlayManager: No overlay found for ID: \(overlayId)")
|
||||
return []
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
print("GeoJSONOverlayManager: Loaded data size: \(data.count) bytes")
|
||||
let features = try MKGeoJSONDecoder().decode(data)
|
||||
print("GeoJSONOverlayManager: Decoded \(features.count) features for \(type.rawValue)")
|
||||
// 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()
|
||||
]
|
||||
]
|
||||
|
||||
if let id = feature.id {
|
||||
featureDict["id"] = id
|
||||
}
|
||||
|
||||
if let properties = feature.properties {
|
||||
featureDict["properties"] = properties.mapValues { $0.toAnyObject() }
|
||||
}
|
||||
|
||||
return featureDict
|
||||
}
|
||||
]
|
||||
|
||||
let geojsonData = try JSONSerialization.data(withJSONObject: standardGeoJSON)
|
||||
let features = try MKGeoJSONDecoder().decode(geojsonData)
|
||||
print("GeoJSONOverlayManager: Decoded \(features.count) features for \(overlayId)")
|
||||
|
||||
var allOverlays: [MKOverlay] = []
|
||||
for (index, feature) in features.enumerated() {
|
||||
|
|
@ -53,23 +115,36 @@ class GeoJSONOverlayManager {
|
|||
}
|
||||
}
|
||||
|
||||
print("GeoJSONOverlayManager: Created \(allOverlays.count) total overlays for \(type.rawValue)")
|
||||
overlays[type] = allOverlays
|
||||
print("GeoJSONOverlayManager: Created \(allOverlays.count) total overlays for \(overlayId)")
|
||||
overlays[overlayId] = allOverlays
|
||||
return allOverlays
|
||||
} catch {
|
||||
print("Failed to load GeoJSON overlay: \(type) error: \(error)")
|
||||
print("GeoJSONOverlayManager: Failed to decode overlays for \(overlayId): \(error)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/// Add overlays to the map
|
||||
func addOverlays(_ type: StaticGeoJSONOverlay, to mapView: MKMapView) {
|
||||
let overlays = loadOverlays(for: type)
|
||||
mapView.addOverlays(overlays, level: .aboveLabels)
|
||||
/// 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
|
||||
}
|
||||
/// Remove overlays from the map
|
||||
func removeOverlays(_ type: StaticGeoJSONOverlay, from mapView: MKMapView) {
|
||||
let overlays = self.overlays[type] ?? []
|
||||
mapView.removeOverlays(overlays)
|
||||
|
||||
/// Get all available overlay IDs
|
||||
func getAvailableOverlayIds() -> [String] {
|
||||
guard let config = loadConfiguration() else { return [] }
|
||||
return config.overlays.map { $0.id }
|
||||
}
|
||||
|
||||
/// 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)
|
||||
func clearCache() {
|
||||
overlays.removeAll()
|
||||
configuration = nil
|
||||
}
|
||||
}
|
||||
BIN
Meshtastic/Resources/BurningManGeoJSONMapConfig.json.gz
Normal file
BIN
Meshtastic/Resources/BurningManGeoJSONMapConfig.json.gz
Normal file
Binary file not shown.
BIN
Meshtastic/Resources/BurningManGeoJSONMapConfig.json.zlib
Normal file
BIN
Meshtastic/Resources/BurningManGeoJSONMapConfig.json.zlib
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
{"type":"FeatureCollection","features":[{"type":"Feature","id":0,"geometry":{"type":"Polygon","coordinates":[[[-119.1828,40.80314],[-119.21777,40.80735],[-119.23383,40.78337],[-119.2088,40.76435],[-119.17727,40.77656],[-119.1828,40.80314]]]},"properties":{"FID":0,"Id":0}}]}
|
||||
|
|
@ -231,47 +231,37 @@ struct MeshMapContent: MapContent {
|
|||
}
|
||||
}
|
||||
|
||||
/// Burning Man GeoJSON Overlays
|
||||
/// GeoJSON Overlays (Configuration-Driven)
|
||||
if showBurningMan {
|
||||
// Load and display street outlines
|
||||
let streetOverlays = GeoJSONOverlayManager.shared.loadOverlays(for: .streetOutlines)
|
||||
let streetIdentifiableOverlays = streetOverlays.map { IdentifiableOverlay(overlay: $0) }
|
||||
ForEach(streetIdentifiableOverlays) { identifiable in
|
||||
let overlay = identifiable.overlay
|
||||
if let polygon = overlay as? MKPolygon {
|
||||
MapPolygon(polygon)
|
||||
.stroke(.green.opacity(0.8), lineWidth: 0.5)
|
||||
.foregroundStyle(.clear)
|
||||
} else if let polyline = overlay as? MKPolyline {
|
||||
MapPolyline(polyline)
|
||||
.stroke(.green.opacity(0.9), lineWidth: 0.8)
|
||||
}
|
||||
}
|
||||
let overlayManager = GeoJSONOverlayManager.shared
|
||||
let availableOverlays = overlayManager.getAvailableOverlayIds()
|
||||
|
||||
// Load and display toilets
|
||||
let toiletOverlays = GeoJSONOverlayManager.shared.loadOverlays(for: .toilets)
|
||||
let toiletIdentifiableOverlays = toiletOverlays.map { IdentifiableOverlay(overlay: $0) }
|
||||
ForEach(toiletIdentifiableOverlays) { identifiable in
|
||||
let overlay = identifiable.overlay
|
||||
if let polygon = overlay as? MKPolygon {
|
||||
MapPolygon(polygon)
|
||||
.stroke(.blue, lineWidth: 2)
|
||||
.foregroundStyle(.blue.opacity(1.0))
|
||||
}
|
||||
}
|
||||
ForEach(availableOverlays, id: \.self) { overlayId in
|
||||
let overlays = overlayManager.loadOverlays(for: overlayId)
|
||||
let rendering = overlayManager.getRenderingProperties(for: overlayId)
|
||||
|
||||
// Load and display trash fence
|
||||
let trashFenceOverlays = GeoJSONOverlayManager.shared.loadOverlays(for: .trashFence)
|
||||
let trashFenceIdentifiableOverlays = trashFenceOverlays.map { IdentifiableOverlay(overlay: $0) }
|
||||
ForEach(trashFenceIdentifiableOverlays) { identifiable in
|
||||
let overlay = identifiable.overlay
|
||||
if let polyline = overlay as? MKPolyline {
|
||||
MapPolyline(polyline)
|
||||
.stroke(.red, lineWidth: 2)
|
||||
} else if let polygon = overlay as? MKPolygon {
|
||||
MapPolygon(polygon)
|
||||
.stroke(.red, lineWidth: 2)
|
||||
.foregroundStyle(.clear)
|
||||
ForEach(overlays.map { IdentifiableOverlay(overlay: $0) }) { identifiable in
|
||||
let overlay = identifiable.overlay
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue