mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
remove some burning man references
This commit is contained in:
parent
42cd598c56
commit
7e0d37d76f
11 changed files with 1369 additions and 61 deletions
|
|
@ -1381,6 +1381,40 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"%lld overlays" : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld sovrapposizioni"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lldオーバーレイ"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld slojeva"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld覆盖层"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld覆蓋層"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"%lld Readings Total" : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
|
|
@ -1545,6 +1579,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"•" : {
|
||||
"shouldTranslate" : false
|
||||
},
|
||||
"• %@" : {
|
||||
"shouldTranslate" : false
|
||||
},
|
||||
|
|
@ -5814,40 +5851,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Burning Man" : {
|
||||
"localizations" : {
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ברנינג מן"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "バーニングマン"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Бернинг Мен"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "火人节"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "火人節"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Button GPIO" : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
|
|
@ -20364,6 +20367,41 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Manage custom map overlays" : {
|
||||
"comment" : "Subtitle for map data management",
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Gestisci sovrapposizioni mappa personalizzate"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "カスタムマップオーバーレイを管理"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Upravljaj prilagođenim slojevima mape"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "管理自定义地图覆盖层"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "管理自訂地圖覆蓋層"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Managed Device" : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
|
|
@ -20462,6 +20500,40 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Map Data" : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Dati Mappa"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "マップデータ"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Podaci Mape"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "地图数据"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "地圖資料"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Map Options" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
|
|
@ -23083,6 +23155,41 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"No Data" : {
|
||||
"comment" : "Data source label when no files are available",
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nessun Dato"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "データなし"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Bez Podataka"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "无数据"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "無資料"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"No device connected" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
|
|
@ -23209,6 +23316,41 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"No files uploaded yet" : {
|
||||
"comment" : "Empty state text when no files are uploaded",
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nessun file caricato ancora"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "まだファイルがアップロードされていません"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Još uvek nema otpremljenih datoteka"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "尚未上传文件"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "尚未上傳檔案"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"No Interface" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
|
|
@ -27726,6 +27868,40 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Processing file..." : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Elaborazione file..."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ファイル処理中..."
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Obrada datoteke..."
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "正在处理文件..."
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "正在處理檔案..."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Project information" : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
|
|
@ -31740,6 +31916,41 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Select Map Data File" : {
|
||||
"comment" : "Button text for selecting map data file",
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Seleziona File Dati Mappa"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "マップデータファイルを選択"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Izaberi Datoteku Podataka Mape"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "选择地图数据文件"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "選擇地圖資料檔案"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Select Node" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
|
|
@ -39875,6 +40086,213 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Upload Error" : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Errore Caricamento"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "アップロードエラー"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Greška Otpremanja"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "上传错误"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "上傳錯誤"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Upload GeoJSON files to display custom map overlays. Files are stored locally and can be up to 10MB." : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Carica file GeoJSON per visualizzare sovrapposizioni mappa personalizzate. I file sono memorizzati localmente e possono essere fino a 10MB."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "カスタムマップオーバーレイを表示するためにGeoJSONファイルをアップロードしてください。ファイルはローカルに保存され、最大10MBまでです。"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Otpremi GeoJSON datoteke da prikažeš prilagođene slojeve mape. Datoteke se čuvaju lokalno i mogu biti do 10MB."
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "上传GeoJSON文件以显示自定义地图覆盖层。文件本地存储,最大10MB。"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "上傳GeoJSON檔案以顯示自訂地圖覆蓋層。檔案本機儲存,最大10MB。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Upload Map Data" : {
|
||||
"comment" : "Title for map data upload screen",
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Carica Dati Mappa"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "マップデータをアップロード"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Otpremi Podatke Mape"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "上传地图数据"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "上傳地圖資料"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Upload map data to enable overlays" : {
|
||||
"comment" : "Prompt to upload map data when none is available",
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Carica dati mappa per abilitare sovrapposizioni"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "オーバーレイを有効にするにはマップデータをアップロード"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Otpremi podatke mape da omogućiš slojeve"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "上传地图数据以启用覆盖层"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "上傳地圖資料以啟用覆蓋層"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Upload Success" : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Caricamento Riuscito"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "アップロード成功"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Uspešno Otpremanje"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "上传成功"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "上傳成功"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Uploaded Files" : {
|
||||
"comment" : "Section header for uploaded files",
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "File Caricati"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "アップロードされたファイル"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Otpremljene Datoteke"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "已上传文件"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "已上傳檔案"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Uptime" : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
|
|
@ -40309,6 +40727,41 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"User Uploaded" : {
|
||||
"comment" : "Data source label for user uploaded files",
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Caricato dall'Utente"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ユーザーがアップロード"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Otpremio Korisnik"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "用户上传"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "使用者上傳"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Username" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
|
|
@ -40401,6 +40854,41 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Using %@ data" : {
|
||||
"comment" : "Shows which data source is being used",
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Utilizzo dati %@"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@データを使用"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Koristi %@ podatke"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "使用%@数据"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "使用%@資料"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Utilizes the network connection on your phone to connect to MQTT." : {
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
|
|
@ -42325,4 +42813,4 @@
|
|||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@
|
|||
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 */; };
|
||||
3D3417D22E2DC260006A988B /* MapDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417D12E2DC260006A988B /* MapDataManager.swift */; };
|
||||
3D3417D42E2DC293006A988B /* MapDataUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417D32E2DC293006A988B /* MapDataUpload.swift */; };
|
||||
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 */; };
|
||||
|
|
@ -335,7 +336,8 @@
|
|||
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>"; };
|
||||
3D3417D12E2DC260006A988B /* MapDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapDataManager.swift; sourceTree = "<group>"; };
|
||||
3D3417D32E2DC293006A988B /* MapDataUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapDataUpload.swift; 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>"; };
|
||||
|
|
@ -797,6 +799,7 @@
|
|||
DD4A911C2708C57100501B7E /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3D3417D32E2DC293006A988B /* MapDataUpload.swift */,
|
||||
DDD5BB0E2C285F92007E03CA /* Logs */,
|
||||
DD93800C2BA74CE3008BEC06 /* Channels */,
|
||||
DD61937A2863876A00E59241 /* Config */,
|
||||
|
|
@ -1057,7 +1060,6 @@
|
|||
DDC2E18926CE24F70042C5E4 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3D3417CD2E29D5D6006A988B /* BurningManGeoJSONMapConfig.json.zlib */,
|
||||
DDB75A192A05EB67006ED576 /* alpha.png */,
|
||||
DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */,
|
||||
DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */,
|
||||
|
|
@ -1110,6 +1112,7 @@
|
|||
DDC2E1A526CEB32B0042C5E4 /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3D3417D12E2DC260006A988B /* MapDataManager.swift */,
|
||||
3D3417C72E29D38A006A988B /* GeoJSONOverlayConfig.swift */,
|
||||
BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */,
|
||||
DDD43FE12A78C86B0083A3E9 /* Mqtt */,
|
||||
|
|
@ -1377,7 +1380,6 @@
|
|||
DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */,
|
||||
DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */,
|
||||
DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */,
|
||||
3D3417CE2E29D5D6006A988B /* BurningManGeoJSONMapConfig.json.zlib in Resources */,
|
||||
DD0E21012B8A6F1300F2D100 /* DeviceHardware.json in Resources */,
|
||||
DDDBC87B2BC62E4E001E8DF7 /* Settings.bundle in Resources */,
|
||||
);
|
||||
|
|
@ -1454,6 +1456,7 @@
|
|||
DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */,
|
||||
DDDB444C29F8AAA600EE2349 /* Color.swift in Sources */,
|
||||
DDDFE73F2D0D48FF0044463C /* IgnoreNodeButton.swift in Sources */,
|
||||
3D3417D42E2DC293006A988B /* MapDataUpload.swift in Sources */,
|
||||
DDB8F4122A9EE5DD00230ECE /* UserList.swift in Sources */,
|
||||
DDB75A0F2A05920E006ED576 /* FileManager.swift in Sources */,
|
||||
DD3D17E02C3FB67200561584 /* LocalWeatherConditions.swift in Sources */,
|
||||
|
|
@ -1601,6 +1604,7 @@
|
|||
251926922C3CB52300249DF5 /* DeleteNodeButton.swift in Sources */,
|
||||
DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */,
|
||||
DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */,
|
||||
3D3417D22E2DC260006A988B /* MapDataManager.swift in Sources */,
|
||||
D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */,
|
||||
DDF82CBD2D5BC69200DC25EC /* NavigateToButton.swift in Sources */,
|
||||
8D3F8A3F2D44BB02009EAAA4 /* PowerMetrics.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -9,26 +9,20 @@ class GeoJSONOverlayManager {
|
|||
private var configuration: GeoJSONOverlayConfiguration?
|
||||
private var overlays: [String: [MKOverlay]] = [:]
|
||||
|
||||
/// Load and decompress the consolidated configuration
|
||||
/// Load user-uploaded configuration only
|
||||
func loadConfiguration() -> GeoJSONOverlayConfiguration? {
|
||||
if let cached = configuration {
|
||||
return cached
|
||||
}
|
||||
|
||||
guard let url = Bundle.main.url(forResource: "BurningManGeoJSONMapConfig", withExtension: "json.zlib") else {
|
||||
return nil
|
||||
// Load user-uploaded configuration
|
||||
if let userConfig = MapDataManager.shared.loadUserConfiguration() {
|
||||
configuration = userConfig
|
||||
return userConfig
|
||||
}
|
||||
|
||||
do {
|
||||
let compressedData = try Data(contentsOf: url)
|
||||
let decompressedData = try compressedData.zlibDecompressed()
|
||||
let config = try JSONDecoder().decode(GeoJSONOverlayConfiguration.self, from: decompressedData)
|
||||
|
||||
configuration = config
|
||||
return config
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
// No configuration available
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Load overlays for a specific overlay ID
|
||||
|
|
@ -111,4 +105,18 @@ class GeoJSONOverlayManager {
|
|||
overlays.removeAll()
|
||||
configuration = nil
|
||||
}
|
||||
|
||||
/// Check if user-uploaded data is available
|
||||
func hasUserData() -> Bool {
|
||||
return MapDataManager.shared.getUploadedFiles().contains { $0.isActive }
|
||||
}
|
||||
|
||||
/// Get the active data source name
|
||||
func getActiveDataSource() -> String {
|
||||
if hasUserData() {
|
||||
return NSLocalizedString("User Uploaded", comment: "Data source label for user uploaded files")
|
||||
} else {
|
||||
return NSLocalizedString("No Data", comment: "Data source label when no files are available")
|
||||
}
|
||||
}
|
||||
}
|
||||
378
Meshtastic/Helpers/MapDataManager.swift
Normal file
378
Meshtastic/Helpers/MapDataManager.swift
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
import Foundation
|
||||
import MapKit
|
||||
import OSLog
|
||||
|
||||
/// Manager for handling user-uploaded map data files
|
||||
class MapDataManager {
|
||||
static let shared = MapDataManager()
|
||||
private init() {}
|
||||
|
||||
// MARK: - Constants
|
||||
private let maxFileSize: Int64 = 10 * 1024 * 1024 // 10MB
|
||||
private let mapDataDirectory = "MapData"
|
||||
private let userUploadedDirectory = "user_uploaded"
|
||||
private let metadataFileName = "upload_history.json"
|
||||
|
||||
// MARK: - Properties
|
||||
private var uploadedFiles: [MapDataMetadata] = []
|
||||
private var activeConfiguration: GeoJSONOverlayConfiguration?
|
||||
|
||||
// MARK: - File Management
|
||||
|
||||
/// Get the base URL for map data storage
|
||||
private func getMapDataDirectory() -> URL? {
|
||||
guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||
Logger.services.error("🗂️ Could not access documents directory")
|
||||
return nil
|
||||
}
|
||||
return documentsURL.appendingPathComponent(mapDataDirectory)
|
||||
}
|
||||
|
||||
/// Get the URL for user uploaded files
|
||||
private func getUserUploadedDirectory() -> URL? {
|
||||
guard let baseURL = getMapDataDirectory() else { return nil }
|
||||
return baseURL.appendingPathComponent(userUploadedDirectory)
|
||||
}
|
||||
|
||||
/// Get the URL for metadata file
|
||||
private func getMetadataFileURL() -> URL? {
|
||||
guard let baseURL = getMapDataDirectory() else { return nil }
|
||||
return baseURL.appendingPathComponent(metadataFileName)
|
||||
}
|
||||
|
||||
/// Create necessary directories
|
||||
private func createDirectoriesIfNeeded() -> Bool {
|
||||
guard let userDir = getUserUploadedDirectory() else { return false }
|
||||
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: userDir, withIntermediateDirectories: true)
|
||||
return true
|
||||
} catch {
|
||||
Logger.services.error("🗂️ Failed to create directories: \(error.localizedDescription, privacy: .public)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - File Upload & Processing
|
||||
|
||||
/// Process and store an uploaded file
|
||||
func processUploadedFile(from sourceURL: URL) async throws -> MapDataMetadata {
|
||||
Logger.services.info("📁 Processing uploaded file: \(sourceURL.lastPathComponent, privacy: .public)")
|
||||
|
||||
// 1. Start accessing security-scoped resource
|
||||
let isAccessing = sourceURL.startAccessingSecurityScopedResource()
|
||||
defer {
|
||||
if isAccessing {
|
||||
sourceURL.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Validate file
|
||||
try validateFile(at: sourceURL)
|
||||
|
||||
// 2. Create directories if needed
|
||||
guard createDirectoriesIfNeeded() else {
|
||||
throw MapDataError.directoryCreationFailed
|
||||
}
|
||||
|
||||
// 3. Generate destination filename
|
||||
let timestamp = Date().timeIntervalSince1970
|
||||
let originalName = sourceURL.deletingPathExtension().lastPathComponent
|
||||
let fileExtension = sourceURL.pathExtension
|
||||
let newFilename = "\(originalName)_\(Int(timestamp)).\(fileExtension)"
|
||||
|
||||
guard let destURL = getUserUploadedDirectory()?.appendingPathComponent(newFilename) else {
|
||||
throw MapDataError.invalidDestination
|
||||
}
|
||||
|
||||
// 4. Copy file to app storage
|
||||
try FileManager.default.copyItem(at: sourceURL, to: destURL)
|
||||
|
||||
// 5. Process and validate content
|
||||
let metadata = try await processFileContent(at: destURL, originalName: originalName)
|
||||
|
||||
// 6. Save metadata
|
||||
uploadedFiles.append(metadata)
|
||||
try saveMetadata()
|
||||
|
||||
// 7. Clear cached configuration to force reload
|
||||
activeConfiguration = nil
|
||||
|
||||
Logger.services.info("📁 Successfully processed file: \(newFilename, privacy: .public)")
|
||||
return metadata
|
||||
}
|
||||
|
||||
/// Validate uploaded file
|
||||
private func validateFile(at url: URL) throws {
|
||||
let fileAttributes = try url.resourceValues(forKeys: [.fileSizeKey, .isRegularFileKey])
|
||||
|
||||
// Check file size
|
||||
guard let fileSize = fileAttributes.fileSize, fileSize <= maxFileSize else {
|
||||
throw MapDataError.fileTooLarge
|
||||
}
|
||||
|
||||
// Check if it's a regular file
|
||||
guard fileAttributes.isRegularFile == true else {
|
||||
throw MapDataError.invalidFileType
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
let allowedExtensions = ["json", "geojson", "kml", "kmz", "gz", "zlib"]
|
||||
let fileExtension = url.pathExtension.lowercased()
|
||||
guard allowedExtensions.contains(fileExtension) else {
|
||||
throw MapDataError.unsupportedFormat
|
||||
}
|
||||
}
|
||||
|
||||
/// Process file content and extract metadata
|
||||
private func processFileContent(at url: URL, originalName: String) async throws -> MapDataMetadata {
|
||||
let fileAttributes = try url.resourceValues(forKeys: [.fileSizeKey, .creationDateKey])
|
||||
let fileSize = fileAttributes.fileSize ?? 0
|
||||
let uploadDate = fileAttributes.creationDate ?? Date()
|
||||
|
||||
// Read and process file content on background queue
|
||||
let (processedData, overlayCount) = try await withCheckedThrowingContinuation { continuation in
|
||||
Task.detached {
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
let processedData = try self.processData(data, filename: url.lastPathComponent)
|
||||
let overlayCount = try self.getOverlayCount(from: processedData)
|
||||
continuation.resume(returning: (processedData, overlayCount))
|
||||
} catch {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If this is the first file uploaded, make it active by default
|
||||
let isFirstFile = uploadedFiles.isEmpty
|
||||
|
||||
return MapDataMetadata(
|
||||
filename: url.lastPathComponent,
|
||||
originalName: originalName,
|
||||
uploadDate: uploadDate,
|
||||
fileSize: Int64(fileSize),
|
||||
format: url.pathExtension.lowercased(),
|
||||
license: nil, // Will be extracted from content if available
|
||||
attribution: nil, // Will be extracted from content if available
|
||||
overlayCount: overlayCount,
|
||||
isActive: isFirstFile
|
||||
)
|
||||
}
|
||||
|
||||
/// Process data (decompress if needed)
|
||||
private func processData(_ data: Data, filename: String) throws -> Data {
|
||||
let fileExtension = filename.components(separatedBy: ".").last?.lowercased() ?? ""
|
||||
|
||||
switch fileExtension {
|
||||
case "gz", "zlib":
|
||||
return try data.zlibDecompressed()
|
||||
default:
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
/// Get overlay count from processed 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
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Configuration Loading
|
||||
|
||||
/// Load user configuration (priority over bundled)
|
||||
func loadUserConfiguration() -> GeoJSONOverlayConfiguration? {
|
||||
if let cached = activeConfiguration {
|
||||
return cached
|
||||
}
|
||||
|
||||
// Find active user files
|
||||
let activeFiles = uploadedFiles.filter { $0.isActive }
|
||||
guard let activeFile = activeFiles.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let fileURL = getUserUploadedDirectory()?.appendingPathComponent(activeFile.filename) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
let processedData = try processData(data, filename: activeFile.filename)
|
||||
let config = try JSONDecoder().decode(GeoJSONOverlayConfiguration.self, from: processedData)
|
||||
|
||||
activeConfiguration = config
|
||||
return config
|
||||
} catch {
|
||||
Logger.services.error("📁 Failed to load user configuration: \(error.localizedDescription, privacy: .public)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - File Management
|
||||
|
||||
/// Get all uploaded files
|
||||
func getUploadedFiles() -> [MapDataMetadata] {
|
||||
return uploadedFiles
|
||||
}
|
||||
|
||||
/// Delete uploaded file
|
||||
func deleteFile(_ metadata: MapDataMetadata) throws {
|
||||
guard let fileURL = getUserUploadedDirectory()?.appendingPathComponent(metadata.filename) else {
|
||||
throw MapDataError.fileNotFound
|
||||
}
|
||||
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
|
||||
if let index = uploadedFiles.firstIndex(where: { $0.filename == metadata.filename }) {
|
||||
uploadedFiles.remove(at: index)
|
||||
}
|
||||
|
||||
try saveMetadata()
|
||||
|
||||
// Clear cache if this was the active file
|
||||
if activeConfiguration != nil {
|
||||
activeConfiguration = nil
|
||||
}
|
||||
|
||||
Logger.services.info("🗑️ Deleted file: \(metadata.filename, privacy: .public)")
|
||||
}
|
||||
|
||||
/// Toggle file active status
|
||||
func toggleFileActive(_ metadata: MapDataMetadata) throws {
|
||||
if let index = uploadedFiles.firstIndex(where: { $0.filename == metadata.filename }) {
|
||||
let newActiveState = !uploadedFiles[index].isActive
|
||||
|
||||
// If making this file active, deactivate all others (only one can be active)
|
||||
if newActiveState {
|
||||
for i in uploadedFiles.indices {
|
||||
uploadedFiles[i].isActive = (i == index)
|
||||
}
|
||||
} else {
|
||||
// Just deactivate this file
|
||||
uploadedFiles[index].isActive = false
|
||||
}
|
||||
|
||||
try saveMetadata()
|
||||
|
||||
// Clear cache to force reload
|
||||
activeConfiguration = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Metadata Persistence
|
||||
|
||||
/// Load metadata from disk
|
||||
func loadMetadata() {
|
||||
guard let metadataURL = getMetadataFileURL(),
|
||||
let data = try? Data(contentsOf: metadataURL),
|
||||
let files = try? JSONDecoder().decode([MapDataMetadata].self, from: data) else {
|
||||
uploadedFiles = []
|
||||
return
|
||||
}
|
||||
|
||||
uploadedFiles = files
|
||||
}
|
||||
|
||||
/// Save metadata to disk
|
||||
private func saveMetadata() throws {
|
||||
guard let metadataURL = getMetadataFileURL() else {
|
||||
throw MapDataError.invalidDestination
|
||||
}
|
||||
|
||||
let data = try JSONEncoder().encode(uploadedFiles)
|
||||
try data.write(to: metadataURL)
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/// Initialize the manager
|
||||
func initialize() {
|
||||
loadMetadata()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Supporting Types
|
||||
|
||||
/// Metadata for uploaded map data files
|
||||
struct MapDataMetadata: Codable, Identifiable {
|
||||
let id: UUID
|
||||
let filename: String
|
||||
let originalName: String
|
||||
let uploadDate: Date
|
||||
let fileSize: Int64
|
||||
let format: String
|
||||
let license: String?
|
||||
let attribution: String?
|
||||
let overlayCount: Int
|
||||
var isActive: Bool
|
||||
|
||||
init(filename: String, originalName: String, uploadDate: Date, fileSize: Int64, format: String, license: String?, attribution: String?, overlayCount: Int, isActive: Bool) {
|
||||
self.id = UUID()
|
||||
self.filename = filename
|
||||
self.originalName = originalName
|
||||
self.uploadDate = uploadDate
|
||||
self.fileSize = fileSize
|
||||
self.format = format
|
||||
self.license = license
|
||||
self.attribution = attribution
|
||||
self.overlayCount = overlayCount
|
||||
self.isActive = isActive
|
||||
}
|
||||
|
||||
var fileSizeString: String {
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.allowedUnits = [.useKB, .useMB]
|
||||
formatter.countStyle = .file
|
||||
return formatter.string(fromByteCount: fileSize)
|
||||
}
|
||||
|
||||
var uploadDateString: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .short
|
||||
return formatter.string(from: uploadDate)
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur during map data operations
|
||||
enum MapDataError: Error, LocalizedError {
|
||||
case fileTooLarge
|
||||
case invalidFileType
|
||||
case unsupportedFormat
|
||||
case invalidContent
|
||||
case directoryCreationFailed
|
||||
case invalidDestination
|
||||
case fileNotFound
|
||||
case saveFailed
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .fileTooLarge:
|
||||
return "File is too large. Maximum size is 10MB."
|
||||
case .invalidFileType:
|
||||
return "Invalid file type. Please select a regular file."
|
||||
case .unsupportedFormat:
|
||||
return "Unsupported file format. Supported formats: JSON, GeoJSON, KML, KMZ, GZ, ZLIB."
|
||||
case .invalidContent:
|
||||
return "Invalid file content. Please check the file format."
|
||||
case .directoryCreationFailed:
|
||||
return "Failed to create storage directory."
|
||||
case .invalidDestination:
|
||||
return "Invalid destination path."
|
||||
case .fileNotFound:
|
||||
return "File not found."
|
||||
case .saveFailed:
|
||||
return "Failed to save file."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,39 @@
|
|||
<string>gvh.MeshtasticApple.mbtiles</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>GeoJSON Map Data</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.json</string>
|
||||
<string>gvh.MeshtasticApple.geojson</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>KML Map Data</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.xml</string>
|
||||
<string>gvh.MeshtasticApple.kml</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>KMZ Map Data</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.zip-archive</string>
|
||||
<string>gvh.MeshtasticApple.kmz</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
|
|
@ -138,6 +171,63 @@
|
|||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.json</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>GeoJSON Map Data</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>gvh.MeshtasticApple.geojson</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>geojson</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.xml</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>KML Map Data</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>gvh.MeshtasticApple.kml</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>kml</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.zip-archive</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>KMZ Map Data</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>gvh.MeshtasticApple.kmz</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>kmz</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>com.apple.developer.carplay-communication</key>
|
||||
<true/>
|
||||
|
|
|
|||
|
|
@ -67,6 +67,9 @@ struct MeshtasticAppleApp: App {
|
|||
self.persistenceController = persistenceController
|
||||
// Wire up router
|
||||
self.appDelegate.router = appState.router
|
||||
|
||||
// Initialize map data manager
|
||||
MapDataManager.shared.initialize()
|
||||
#if DEBUG
|
||||
// Show tips in development
|
||||
try? Tips.resetDatastore()
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -29,8 +29,8 @@ struct MeshMapContent: MapContent {
|
|||
@AppStorage("enableMapWaypoints") private var showWaypoints = true
|
||||
@Binding var selectedWaypoint: WaypointEntity?
|
||||
|
||||
// Burning Man GeoJSON overlays
|
||||
@AppStorage("burningManShowAll") private var showBurningMan = false
|
||||
// Map overlays
|
||||
@AppStorage("mapOverlaysEnabled") private var showMapOverlays = false
|
||||
|
||||
@FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .easeIn)
|
||||
var positions: FetchedResults<PositionEntity>
|
||||
|
|
@ -232,7 +232,7 @@ struct MeshMapContent: MapContent {
|
|||
}
|
||||
|
||||
/// GeoJSON Overlays (Configuration-Driven)
|
||||
if showBurningMan {
|
||||
if showMapOverlays {
|
||||
let overlayManager = GeoJSONOverlayManager.shared
|
||||
let availableOverlays = overlayManager.getAvailableOverlayIds()
|
||||
|
||||
|
|
|
|||
|
|
@ -116,19 +116,56 @@ struct MapSettingsForm: View {
|
|||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Map Overlays")) {
|
||||
Section(header: Text("Map Overlays")) {
|
||||
let hasUserData = GeoJSONOverlayManager.shared.hasUserData()
|
||||
|
||||
// Master toggle for map overlays
|
||||
Toggle(isOn: Binding(
|
||||
get: { UserDefaults.standard.bool(forKey: "burningManShowAll") },
|
||||
set: { UserDefaults.standard.set($0, forKey: "burningManShowAll") }
|
||||
get: { hasUserData && UserDefaults.standard.bool(forKey: "mapOverlaysEnabled") },
|
||||
set: { UserDefaults.standard.set($0, forKey: "mapOverlaysEnabled") }
|
||||
)) {
|
||||
Label {
|
||||
Text("Burning Man")
|
||||
VStack(alignment: .leading) {
|
||||
Text("Map Overlays")
|
||||
Text(GeoJSONOverlayManager.shared.getActiveDataSource())
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "flame.fill")
|
||||
.foregroundColor(.orange)
|
||||
Image(systemName: "map")
|
||||
.foregroundColor(hasUserData ? .accentColor : .secondary)
|
||||
}
|
||||
}
|
||||
.tint(.accentColor)
|
||||
.disabled(!hasUserData)
|
||||
|
||||
// Show data source info or upload prompt
|
||||
if hasUserData && UserDefaults.standard.bool(forKey: "mapOverlaysEnabled") {
|
||||
HStack {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.secondary)
|
||||
Text(String(format: NSLocalizedString("Using %@ data", comment: "Shows which data source is being used"), GeoJSONOverlayManager.shared.getActiveDataSource()))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.leading, 35)
|
||||
} else if !hasUserData {
|
||||
NavigationLink(destination: MapDataUpload()) {
|
||||
HStack {
|
||||
Image(systemName: "arrow.up.doc")
|
||||
.foregroundColor(.accentColor)
|
||||
Text(NSLocalizedString("Upload map data to enable overlays", comment: "Prompt to upload map data when none is available"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.leading, 35)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,33 @@ struct AppData: View {
|
|||
GPSStatus()
|
||||
}
|
||||
Divider()
|
||||
|
||||
// Map Data Section
|
||||
Section(header: Text("Map Data")) {
|
||||
NavigationLink(destination: MapDataUpload()) {
|
||||
HStack {
|
||||
Image(systemName: "map")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(idiom == .phone ? .callout : .title)
|
||||
.frame(width: 35)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(NSLocalizedString("Upload Map Data", comment: "Title for map data upload screen"))
|
||||
.font(.headline)
|
||||
Text(NSLocalizedString("Manage custom map overlays", comment: "Subtitle for map data management"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
|
||||
List(files, id: \.self) { file in
|
||||
|
|
|
|||
273
Meshtastic/Views/Settings/MapDataUpload.swift
Normal file
273
Meshtastic/Views/Settings/MapDataUpload.swift
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import OSLog
|
||||
|
||||
struct MapDataUpload: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
@State private var isShowingFilePicker = false
|
||||
@State private var isProcessing = false
|
||||
@State private var processingProgress: Double = 0.0
|
||||
@State private var showError = false
|
||||
@State private var errorMessage = ""
|
||||
@State private var showSuccess = false
|
||||
@State private var successMessage = ""
|
||||
|
||||
private let mapDataManager = MapDataManager.shared
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
// Header
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(NSLocalizedString("Upload Map Data", comment: "Title for map data upload screen"))
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
|
||||
Text("Upload GeoJSON files to display custom map overlays. Files are stored locally and can be up to 10MB.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal)
|
||||
|
||||
// Upload Button
|
||||
Button(action: {
|
||||
isShowingFilePicker = true
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "doc.badge.plus")
|
||||
.font(.title2)
|
||||
Text(NSLocalizedString("Select Map Data File", comment: "Button text for selecting map data file"))
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.accentColor)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.disabled(isProcessing)
|
||||
.padding(.horizontal)
|
||||
|
||||
// Processing Indicator
|
||||
if isProcessing {
|
||||
VStack(spacing: 12) {
|
||||
ProgressView(value: processingProgress)
|
||||
.progressViewStyle(LinearProgressViewStyle())
|
||||
.padding(.horizontal)
|
||||
|
||||
Text("Processing file...")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
// Current Files Section
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(NSLocalizedString("Uploaded Files", comment: "Section header for uploaded files"))
|
||||
.font(.headline)
|
||||
.padding(.horizontal)
|
||||
|
||||
let uploadedFiles = mapDataManager.getUploadedFiles()
|
||||
|
||||
if uploadedFiles.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "doc.text")
|
||||
.font(.title)
|
||||
.foregroundColor(.secondary)
|
||||
Text(NSLocalizedString("No files uploaded yet", comment: "Empty state text when no files are uploaded"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 40)
|
||||
} else {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 8) {
|
||||
ForEach(uploadedFiles) { file in
|
||||
MapDataFileRow(file: file) {
|
||||
toggleFileActive(file)
|
||||
} onDelete: {
|
||||
deleteFile(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle("Map Data")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.fileImporter(
|
||||
isPresented: $isShowingFilePicker,
|
||||
allowedContentTypes: [
|
||||
UTType.json,
|
||||
UTType(filenameExtension: "geojson") ?? UTType.json,
|
||||
UTType(filenameExtension: "kml") ?? UTType.xml,
|
||||
UTType(filenameExtension: "kmz") ?? UTType.zip,
|
||||
UTType(filenameExtension: "gz") ?? UTType.data,
|
||||
UTType(filenameExtension: "zlib") ?? UTType.data
|
||||
],
|
||||
allowsMultipleSelection: false
|
||||
) { result in
|
||||
handleFileSelection(result)
|
||||
}
|
||||
.alert("Upload Error", isPresented: $showError) {
|
||||
Button("OK") { }
|
||||
} message: {
|
||||
Text(errorMessage)
|
||||
}
|
||||
.alert("Upload Success", isPresented: $showSuccess) {
|
||||
Button("OK") { }
|
||||
} message: {
|
||||
Text(successMessage)
|
||||
}
|
||||
.onAppear {
|
||||
// Initialize map data manager if needed
|
||||
mapDataManager.initialize()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - File Handling
|
||||
|
||||
private func handleFileSelection(_ result: Result<[URL], Error>) {
|
||||
do {
|
||||
guard let selectedFile = try result.get().first else { return }
|
||||
|
||||
// Start processing
|
||||
isProcessing = true
|
||||
processingProgress = 0.0
|
||||
|
||||
// Process file asynchronously
|
||||
Task {
|
||||
do {
|
||||
// Simulate progress
|
||||
await simulateProgress()
|
||||
|
||||
let metadata = try await mapDataManager.processUploadedFile(from: selectedFile)
|
||||
|
||||
await MainActor.run {
|
||||
isProcessing = false
|
||||
processingProgress = 1.0
|
||||
|
||||
successMessage = "Successfully uploaded '\(metadata.originalName)' with \(metadata.overlayCount) overlays"
|
||||
showSuccess = true
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
isProcessing = false
|
||||
processingProgress = 0.0
|
||||
|
||||
errorMessage = error.localizedDescription
|
||||
showError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
errorMessage = "Failed to access file: \(error.localizedDescription)"
|
||||
showError = true
|
||||
}
|
||||
}
|
||||
|
||||
private func simulateProgress() async {
|
||||
for i in 1...10 {
|
||||
await MainActor.run {
|
||||
processingProgress = Double(i) / 10.0
|
||||
}
|
||||
try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleFileActive(_ file: MapDataMetadata) {
|
||||
do {
|
||||
try mapDataManager.toggleFileActive(file)
|
||||
} catch {
|
||||
errorMessage = "Failed to toggle file: \(error.localizedDescription)"
|
||||
showError = true
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteFile(_ file: MapDataMetadata) {
|
||||
do {
|
||||
try mapDataManager.deleteFile(file)
|
||||
} catch {
|
||||
errorMessage = "Failed to delete file: \(error.localizedDescription)"
|
||||
showError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Supporting Views
|
||||
|
||||
struct MapDataFileRow: View {
|
||||
let file: MapDataMetadata
|
||||
let onToggle: () -> Void
|
||||
let onDelete: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(file.originalName)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle("", isOn: Binding(
|
||||
get: { file.isActive },
|
||||
set: { _ in onToggle() }
|
||||
))
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text(file.format.uppercased())
|
||||
.font(.caption)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.secondary.opacity(0.2))
|
||||
.cornerRadius(4)
|
||||
|
||||
Text(file.fileSizeString)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("•")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("\(file.overlayCount) overlays")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(file.uploadDateString)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: onDelete) {
|
||||
Image(systemName: "trash")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(8)
|
||||
.shadow(color: .black.opacity(0.1), radius: 2, x: 0, y: 1)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationView {
|
||||
MapDataUpload()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue