From 0dcdd4f009054db36edf0206d3e7195bbc9d0b8c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 13 May 2023 14:23:12 -0700 Subject: [PATCH 1/7] Button size cleanup --- Meshtastic.xcodeproj/project.pbxproj | 4 - Meshtastic/Enums/LoraConfigEnums.swift | 20 +++++ Meshtastic/Helpers/BLEManager.swift | 3 +- Meshtastic/Views/Map/Custom/TilesView.swift | 49 ---------- Meshtastic/Views/Map/WaypointFormView.swift | 6 +- Meshtastic/Views/Settings/AppSettings.swift | 99 +++++++++++++-------- en.lproj/Localizable.strings | 4 +- 7 files changed, 87 insertions(+), 98 deletions(-) delete mode 100644 Meshtastic/Views/Map/Custom/TilesView.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index aec6aa6a..07b1d397 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -103,7 +103,6 @@ DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */; }; DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A152A0594AD006ED576 /* TileOverlay.swift */; }; DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = DDB75A192A05EB67006ED576 /* alpha.png */; }; - DDB75A1C2A076DFA006ED576 /* TilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1B2A076DFA006ED576 /* TilesView.swift */; }; DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */; }; DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */; }; DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */; }; @@ -293,7 +292,6 @@ DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineTileManager.swift; sourceTree = ""; }; DDB75A152A0594AD006ED576 /* TileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileOverlay.swift; sourceTree = ""; }; DDB75A192A05EB67006ED576 /* alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = alpha.png; sourceTree = ""; }; - DDB75A1B2A076DFA006ED576 /* TilesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TilesView.swift; sourceTree = ""; }; DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrengthIndicator.swift; sourceTree = ""; }; DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV8.xcdatamodel; sourceTree = ""; }; DDC2E15426CE248E0042C5E4 /* Meshtastic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Meshtastic.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -403,7 +401,6 @@ DD964FC32974767D007C176F /* MapViewFitExtension.swift */, DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */, DDDB443529F6287000EE2349 /* MapButtons.swift */, - DDB75A1B2A076DFA006ED576 /* TilesView.swift */, ); path = Custom; sourceTree = ""; @@ -1010,7 +1007,6 @@ DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, DDDB444229F8A88700EE2349 /* Double.swift in Sources */, DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */, - DDB75A1C2A076DFA006ED576 /* TilesView.swift in Sources */, DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */, DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */, DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */, diff --git a/Meshtastic/Enums/LoraConfigEnums.swift b/Meshtastic/Enums/LoraConfigEnums.swift index b6738878..805a4288 100644 --- a/Meshtastic/Enums/LoraConfigEnums.swift +++ b/Meshtastic/Enums/LoraConfigEnums.swift @@ -135,6 +135,26 @@ enum ModemPresets: Int, CaseIterable, Identifiable { return "Short Range - Fast" } } + func snrLimit() -> Float { + switch self { + case .longFast: + return -17.5 + case .longSlow: + return -7.5 + case .longModerate: + return -17.5 + case .vLongSlow: + return -20 + case .medSlow: + return -15 + case .medFast: + return -12.5 + case .shortSlow: + return -10 + case .shortFast: + return -7.5 + } + } func protoEnumValue() -> Config.LoRaConfig.ModemPreset { switch self { case .longFast: diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 107e6f1b..43cbfd8e 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -787,9 +787,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if LocationHelper.currentSpeed >= 0 { positionPacket.groundSpeed = UInt32(LocationHelper.currentSpeed * 3.6) } - if LocationHelper.currentHeading >= 0 { + if (!LocationHelper.currentHeading.isNaN || !LocationHelper.currentHeading.isInfinite) { positionPacket.groundTrack = UInt32(LocationHelper.currentHeading) } + var meshPacket = MeshPacket() meshPacket.to = UInt32(destNum) meshPacket.from = UInt32(fromNodeNum) diff --git a/Meshtastic/Views/Map/Custom/TilesView.swift b/Meshtastic/Views/Map/Custom/TilesView.swift deleted file mode 100644 index b5927945..00000000 --- a/Meshtastic/Views/Map/Custom/TilesView.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// TilesView.swift -// Meshtastic -// -// Copyright(c) Garth Vander Houwen on 5/6/23. -// - -import SwiftUI -import MapKit - -struct TilesView: View { - - @ObservedObject var tileManager = OfflineTileManager.shared - @State var totalDownloadedTileSize = "" - - var body: some View { - - Button(action: { - tileManager.removeAll() - totalDownloadedTileSize = tileManager.getAllDownloadedSize() - print("delete all tiles") - }) { - - HStack { - Image(systemName: "trash") - .font(.callout) - .foregroundColor(.red) - Text("\("map.tiles.delete".localized) (\(totalDownloadedTileSize))") - .font(.callout) - .foregroundColor(.red) - Spacer() - } - } - .onAppear(perform: { - totalDownloadedTileSize = tileManager.getAllDownloadedSize() - }) - } -} - -// MARK: Previews -struct TilesView_Previews: PreviewProvider { - - static var previews: some View { - - TilesView() - .previewLayout(.fixed(width: 300, height: 80)) - .environment(\.colorScheme, .light) - } -} diff --git a/Meshtastic/Views/Map/WaypointFormView.swift b/Meshtastic/Views/Map/WaypointFormView.swift index 4f1171b6..1b375f4d 100644 --- a/Meshtastic/Views/Map/WaypointFormView.swift +++ b/Meshtastic/Views/Map/WaypointFormView.swift @@ -167,7 +167,7 @@ struct WaypointFormView: View { } .buttonStyle(.bordered) .buttonBorderShape(.capsule) - .controlSize(.large) + .controlSize(.regular) .disabled(bleManager.connectedPeripheral == nil) .padding(.bottom) @@ -178,7 +178,7 @@ struct WaypointFormView: View { } .buttonStyle(.bordered) .buttonBorderShape(.capsule) - .controlSize(.large) + .controlSize(.regular) .padding(.bottom) if coordinate.waypointId > 0 { @@ -230,7 +230,7 @@ struct WaypointFormView: View { } .buttonStyle(.bordered) .buttonBorderShape(.capsule) - .controlSize(.large) + .controlSize(.regular) .padding(.bottom) } } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 3da32607..febb27e3 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -8,11 +8,14 @@ struct AppSettings: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager + @ObservedObject var tileManager = OfflineTileManager.shared + @State var totalDownloadedTileSize = "" @StateObject var locationHelper = LocationHelper() @State var meshtasticUsername: String = UserDefaults.meshtasticUsername @State var provideLocation: Bool = UserDefaults.provideLocation @State var provideLocationInterval: Int = UserDefaults.provideLocationInterval @State private var isPresentingCoreDataResetConfirm = false + @State private var isPresentingDeleteMapTilesConfirm = false var body: some View { VStack { @@ -40,8 +43,8 @@ struct AppSettings: View { .font(.footnote) } Label("Coordinate \(String(format: "%.5f", locationHelper.locationManager.location?.coordinate.latitude ?? 0)), \(String(format: "%.5f", locationHelper.locationManager.location?.coordinate.longitude ?? 0))", systemImage: "mappin") - .font(.footnote) - .textSelection(.enabled) + .font(.footnote) + .textSelection(.enabled) if locationHelper.locationManager.location?.verticalAccuracy ?? 0 > 0 { Label("Altitude \(altitiude.formatted())", systemImage: "mountain.2") .font(.footnote) @@ -55,56 +58,74 @@ struct AppSettings: View { .font(.footnote) } + } + Section(header: Text("Location Settings")) { + Toggle(isOn: $provideLocation) { - Label("provide.location", systemImage: "location.circle.fill") - .font(.footnote) } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) if UserDefaults.provideLocation { - - Picker("update.interval", selection: $provideLocationInterval) { - ForEach(LocationUpdateInterval.allCases) { lu in - Text(lu.description) + VStack { + Picker("update.interval", selection: $provideLocationInterval) { + ForEach(LocationUpdateInterval.allCases) { lu in + Text(lu.description) + } } + .pickerStyle(DefaultPickerStyle()) + .onChange(of: (provideLocationInterval)) { newProvideLocationInterval in + UserDefaults.provideLocationInterval = newProvideLocationInterval + } + Text("phone.gps.interval.description") + .font(.caption2) + .foregroundColor(.gray) } - .pickerStyle(DefaultPickerStyle()) - .onChange(of: (provideLocationInterval)) { newProvideLocationInterval in - UserDefaults.provideLocationInterval = newProvideLocationInterval - } - - Text("phone.gps.interval.description") - .font(.caption2) - .foregroundColor(.gray) } + } - TilesView() - } - HStack { - Button { - isPresentingCoreDataResetConfirm = true - } label: { - Label("clear.app.data", systemImage: "trash") - .foregroundColor(.red) - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .confirmationDialog( - "are.you.sure", - isPresented: $isPresentingCoreDataResetConfirm, - titleVisibility: .visible - ) { - Button("Erase all app data?", role: .destructive) { - bleManager.disconnectPeripheral() - clearCoreDataDatabase(context: context) - UserDefaults.standard.reset() - UserDefaults.standard.synchronize() + Section(header: Text("App Data")) { + Button { + isPresentingDeleteMapTilesConfirm = true + } label: { + Label("\("map.tiles.delete".localized) (\(totalDownloadedTileSize))", systemImage: "trash") + .foregroundColor(.red) + } + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingDeleteMapTilesConfirm, + titleVisibility: .visible + ) { + Button("Delete all map tiles?", role: .destructive) { + tileManager.removeAll() + totalDownloadedTileSize = tileManager.getAllDownloadedSize() + print("delete all tiles") + } + } + + Button { + isPresentingCoreDataResetConfirm = true + } label: { + Label("clear.app.data", systemImage: "trash") + .foregroundColor(.red) + } + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingCoreDataResetConfirm, + titleVisibility: .visible + ) { + Button("Erase all app data?", role: .destructive) { + bleManager.disconnectPeripheral() + clearCoreDataDatabase(context: context) + UserDefaults.standard.reset() + UserDefaults.standard.synchronize() + } } } } + .onAppear(perform: { + totalDownloadedTileSize = tileManager.getAllDownloadedSize() + }) } .navigationTitle("app.settings") .navigationBarItems(trailing: diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 8f22be14..80820174 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -139,7 +139,7 @@ "map"="Mesh Map"; "map.type"="Default Type"; "map.centering"="Centering Mode"; -"map.tiles.delete"="Delete Cached Map Tiles"; +"map.tiles.delete"="Delete Map Tiles"; "map.recentering"="Automatic Re-centering"; "map.usertrackingmode"="User tracking mode"; "map.usertrackingmode.follow"="Follow"; @@ -207,7 +207,7 @@ "position"="Position"; "position.config"="Position Config"; "preferred.radio"="Preferred Radio"; -"provide.location"="Provide location to mesh"; +"provide.location"="Share location"; "radio.configuration"="Radio Configuration"; "range.test"="Range Test"; "range.test.config"="Range Test Config"; From 0c915863d58bb6c34c6b5137a4e7d9607e8fe0a1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 13 May 2023 15:46:18 -0700 Subject: [PATCH 2/7] Fix crashes --- Meshtastic/Extensions/String.swift | 6 +++++- Meshtastic/Helpers/BLEManager.swift | 2 +- Meshtastic/Views/Nodes/NodeMap.swift | 8 +++----- Meshtastic/Views/Settings/Config/DeviceConfig.swift | 2 +- Meshtastic/Views/Settings/Settings.swift | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index da401a00..30b77102 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -38,7 +38,11 @@ extension String { return false } else { let characters = Array(self) - return characters[0].isEmoji + if characters.count <= 0 { + return false + } else { + return characters[0].isEmoji + } } } diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 43cbfd8e..e69ffe7a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -784,7 +784,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { positionPacket.timestamp = UInt32(LocationHelper.currentTimestamp.timeIntervalSince1970) positionPacket.altitude = Int32(LocationHelper.currentAltitude) positionPacket.satsInView = UInt32(LocationHelper.satsInView) - if LocationHelper.currentSpeed >= 0 { + if !LocationHelper.currentSpeed.isNaN || !LocationHelper.currentSpeed.isInfinite { positionPacket.groundSpeed = UInt32(LocationHelper.currentSpeed * 3.6) } if (!LocationHelper.currentHeading.isNaN || !LocationHelper.currentHeading.isInfinite) { diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 2390711b..30e76b4b 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -143,10 +143,9 @@ struct NodeMap: View { Text("Enable Offline Maps") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.enableOfflineMaps.toggle() - UserDefaults.enableOfflineMaps = self.enableOfflineMaps - if !self.enableOfflineMaps { + .onChange(of: (enableOfflineMaps)) { newEnableOfflineMaps in + UserDefaults.enableOfflineMaps = newEnableOfflineMaps + if !newEnableOfflineMaps { if self.selectedMapLayer == .offline { self.selectedMapLayer = .standard } @@ -166,7 +165,6 @@ struct NodeMap: View { .pickerStyle(DefaultPickerStyle()) .onChange(of: (selectedTileServer)) { newSelectedTileServer in UserDefaults.mapTileServer = newSelectedTileServer - selectedMapLayer = .standard } Text("Attribution:") .fontWeight(.semibold) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 9d7cb63b..a8d55d87 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -247,7 +247,7 @@ struct DeviceConfig: View { if bleManager.connectedPeripheral != nil && node?.deviceConfig == nil { print("empty device config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) - if node != nil && connectedNode != nil { + if node != nil && connectedNode != nil && connectedNode?.user != nil { _ = bleManager.requestDeviceConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 4b128ba1..c7f3793b 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -89,7 +89,7 @@ struct Settings: View { let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) - if node?.metadata == nil { + if connectedNode != nil && node?.metadata == nil { let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) if adminMessageId > 0 { From cbb6e4fc5577ee47dba32bf8f5cf59b423ff768c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 13 May 2023 15:47:08 -0700 Subject: [PATCH 3/7] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 07b1d397..e4f36526 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1297,7 +1297,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.9; + MARKETING_VERSION = 2.1.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1331,7 +1331,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.9; + MARKETING_VERSION = 2.1.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1450,7 +1450,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.9; + MARKETING_VERSION = 2.1.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1481,7 +1481,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.9; + MARKETING_VERSION = 2.1.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; From ba3ef4af3ed5a61730303d08e600d40b12bad798 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 13 May 2023 20:50:20 -0700 Subject: [PATCH 4/7] Managed Device Delete by tile server --- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Enums/AppSettingsEnums.swift | 11 +- .../Helpers/Map/OfflineTileManager.swift | 39 ++ Meshtastic/Helpers/NetworkManager.swift | 1 + .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 339 +++++++++++++ .../Persistence/PositionEntityExtension.swift | 1 + Meshtastic/Persistence/UpdateCoreData.swift | 4 +- .../Protobufs/meshtastic/config.pb.swift | 11 + Meshtastic/Views/Settings/AppSettings.swift | 48 +- .../Views/Settings/Config/DeviceConfig.swift | 17 +- Meshtastic/Views/Settings/Settings.swift | 444 +++++++++--------- en.lproj/Localizable.strings | 2 +- 13 files changed, 677 insertions(+), 246 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV13.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e4f36526..35f9264f 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -293,6 +293,7 @@ DDB75A152A0594AD006ED576 /* TileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileOverlay.swift; sourceTree = ""; }; DDB75A192A05EB67006ED576 /* alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = alpha.png; sourceTree = ""; }; DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrengthIndicator.swift; sourceTree = ""; }; + DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV13.xcdatamodel; sourceTree = ""; }; DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV8.xcdatamodel; sourceTree = ""; }; DDC2E15426CE248E0042C5E4 /* Meshtastic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Meshtastic.app; sourceTree = BUILT_PRODUCTS_DIR; }; DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticApp.swift; sourceTree = ""; }; @@ -1578,6 +1579,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */, DDB759E12A04B264006ED576 /* MeshtasticDataModelV12.xcdatamodel */, DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */, DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */, @@ -1591,7 +1593,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DDB759E12A04B264006ED576 /* MeshtasticDataModelV12.xcdatamodel */; + currentVersion = DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index be2c2850..8044cde8 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -254,11 +254,11 @@ enum MapTileServerLinks: String, CaseIterable, Identifiable { case .openStreetMapHot: return [Int](0...18) case .usgsTopo: - return [Int](6...15) + return [Int](6...16) case .usgsImageryTopo: - return [Int](6...15) + return [Int](6...16) case .usgsImageryOnly: - return [Int](6...15) + return [Int](6...16) case .terrain: return [Int](0...15) case .toner: @@ -268,3 +268,8 @@ enum MapTileServerLinks: String, CaseIterable, Identifiable { } } } + +//enum MapOverlayServerLinks: String, CaseIterable, Identifiable { +// +// +//} diff --git a/Meshtastic/Helpers/Map/OfflineTileManager.swift b/Meshtastic/Helpers/Map/OfflineTileManager.swift index 4c8e5dba..640fa078 100644 --- a/Meshtastic/Helpers/Map/OfflineTileManager.swift +++ b/Meshtastic/Helpers/Map/OfflineTileManager.swift @@ -48,6 +48,32 @@ class OfflineTileManager: ObservableObject { return Double(count) * size } + func getDownloadedSize(for mapTileLink: MapTileServerLinks) -> Double { + + var accumulatedSize: UInt64 = 0 + let mapTiles = try! fileManager.contentsOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"), includingPropertiesForKeys: []) + let matchingTiles = mapTiles.filter { fileName in + let fileNameLower = fileName.absoluteString + return fileNameLower.contains(mapTileLink.id) + } + print("Deleting \(matchingTiles.count) tiles for \(mapTileLink.id)") + for tile in matchingTiles { + let url = documentsDirectory.appendingPathComponent(tile.absoluteString) + accumulatedSize += (try? url.regularFileAllocatedSize()) ?? 0 + } + + return Double(accumulatedSize) + +// let paths = self.computeTileOverlayPaths(boundingBox: boundingBox) +// +// for path in paths { +// let file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png" +// let url = documentsDirectory.appendingPathComponent(file) +// accumulatedSize += (try? url.regularFileAllocatedSize()) ?? 0 +// } +// return Double(accumulatedSize) + } + func getDownloadedSize(for boundingBox: MKMapRect) -> Double { let paths = self.computeTileOverlayPaths(boundingBox: boundingBox) var accumulatedSize: UInt64 = 0 @@ -64,6 +90,19 @@ class OfflineTileManager: ObservableObject { createDirectoriesIfNecessary() } + func remove(for mapTileLink: MapTileServerLinks) { + + let mapTiles = try! fileManager.contentsOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"), includingPropertiesForKeys: []) + let matchingTiles = mapTiles.filter { fileName in + let fileNameLower = fileName.absoluteString + return fileNameLower.contains(mapTileLink.id) + } + print("Deleting \(matchingTiles.count) tiles for \(mapTileLink.id)") + for tile in matchingTiles { + try? fileManager.removeItem(at: tile.absoluteURL) + } + } + func remove(for boundingBox: MKMapRect) { let paths = self.computeTileOverlayPaths(boundingBox: boundingBox) for path in paths { diff --git a/Meshtastic/Helpers/NetworkManager.swift b/Meshtastic/Helpers/NetworkManager.swift index 61d3a70a..040e06a9 100644 --- a/Meshtastic/Helpers/NetworkManager.swift +++ b/Meshtastic/Helpers/NetworkManager.swift @@ -18,6 +18,7 @@ class NetworkManager { pathMonitor.pathUpdateHandler = { guard $0.status == .satisfied else { // No network available + print("Network Not available") return pathMonitor.cancel() } pathMonitor.cancel() diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index cfa704c4..17b97ce6 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV12.xcdatamodel + MeshtasticDataModelV13.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV13.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV13.xcdatamodel/contents new file mode 100644 index 00000000..b009f304 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV13.xcdatamodel/contents @@ -0,0 +1,339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/PositionEntityExtension.swift b/Meshtastic/Persistence/PositionEntityExtension.swift index d9079158..5f96575c 100644 --- a/Meshtastic/Persistence/PositionEntityExtension.swift +++ b/Meshtastic/Persistence/PositionEntityExtension.swift @@ -50,6 +50,7 @@ extension PositionEntity { var annotaton: MKPointAnnotation { let pointAnn = MKPointAnnotation() + if nodeCoordinate != nil { pointAnn.coordinate = nodeCoordinate! } diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 696df7b5..f28fb5d5 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -325,6 +325,7 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) newDeviceConfig.nodeInfoBroadcastSecs = Int32(config.nodeInfoBroadcastSecs) newDeviceConfig.doubleTapAsButtonPress = config.doubleTapAsButtonPress + newDeviceConfig.isManaged = config.isManaged fetchedNode[0].deviceConfig = newDeviceConfig } else { fetchedNode[0].deviceConfig?.role = Int32(config.role.rawValue) @@ -333,8 +334,9 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.buttonGpio) fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.buzzerGpio) fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) - fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(config.nodeInfoBroadcastSecs) + fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress + fetchedNode[0].deviceConfig?.isManaged = config.isManaged } do { try context.save() diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index 8c502d60..917fb889 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -181,6 +181,11 @@ struct Config { /// Treat double tap interrupt on supported accelerometers as a button press if set to true var doubleTapAsButtonPress: Bool = false + /// + /// If true, device is considered to be "managed" by a mesh administrator + /// Clients should then limit available configuration and administrative options inside the user interface + var isManaged: Bool = false + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -1592,6 +1597,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl 6: .standard(proto: "rebroadcast_mode"), 7: .standard(proto: "node_info_broadcast_secs"), 8: .standard(proto: "double_tap_as_button_press"), + 9: .standard(proto: "is_managed"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1608,6 +1614,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl case 6: try { try decoder.decodeSingularEnumField(value: &self.rebroadcastMode) }() case 7: try { try decoder.decodeSingularUInt32Field(value: &self.nodeInfoBroadcastSecs) }() case 8: try { try decoder.decodeSingularBoolField(value: &self.doubleTapAsButtonPress) }() + case 9: try { try decoder.decodeSingularBoolField(value: &self.isManaged) }() default: break } } @@ -1638,6 +1645,9 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if self.doubleTapAsButtonPress != false { try visitor.visitSingularBoolField(value: self.doubleTapAsButtonPress, fieldNumber: 8) } + if self.isManaged != false { + try visitor.visitSingularBoolField(value: self.isManaged, fieldNumber: 9) + } try unknownFields.traverse(visitor: &visitor) } @@ -1650,6 +1660,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if lhs.rebroadcastMode != rhs.rebroadcastMode {return false} if lhs.nodeInfoBroadcastSecs != rhs.nodeInfoBroadcastSecs {return false} if lhs.doubleTapAsButtonPress != rhs.doubleTapAsButtonPress {return false} + if lhs.isManaged != rhs.isManaged {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index febb27e3..4310d105 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -16,6 +16,7 @@ struct AppSettings: View { @State var provideLocationInterval: Int = UserDefaults.provideLocationInterval @State private var isPresentingCoreDataResetConfirm = false @State private var isPresentingDeleteMapTilesConfirm = false + @State var selectedTileServer: MapTileServerLinks? = nil var body: some View { VStack { @@ -85,6 +86,27 @@ struct AppSettings: View { } Section(header: Text("App Data")) { + + Button { + isPresentingCoreDataResetConfirm = true + } label: { + Label("clear.app.data", systemImage: "trash") + .foregroundColor(.red) + } + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingCoreDataResetConfirm, + titleVisibility: .visible + ) { + Button("Erase all app data?", role: .destructive) { + bleManager.disconnectPeripheral() + clearCoreDataDatabase(context: context) + UserDefaults.standard.reset() + UserDefaults.standard.synchronize() + } + } + } + Section(header: Text("Map Tile Data")) { Button { isPresentingDeleteMapTilesConfirm = true } label: { @@ -102,23 +124,15 @@ struct AppSettings: View { print("delete all tiles") } } - - Button { - isPresentingCoreDataResetConfirm = true - } label: { - Label("clear.app.data", systemImage: "trash") - .foregroundColor(.red) - } - .confirmationDialog( - "are.you.sure", - isPresented: $isPresentingCoreDataResetConfirm, - titleVisibility: .visible - ) { - Button("Erase all app data?", role: .destructive) { - bleManager.disconnectPeripheral() - clearCoreDataDatabase(context: context) - UserDefaults.standard.reset() - UserDefaults.standard.synchronize() + ForEach(MapTileServerLinks.allCases, id: \.self) { tsl in + + Button { + tileManager.remove(for: tsl) + totalDownloadedTileSize = tileManager.getAllDownloadedSize() + } label: { + Label("Delete \(tsl.description) Tiles", systemImage: "trash") + .foregroundColor(.red) + .font(.footnote) } } } diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index a8d55d87..0dc8e05d 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -26,6 +26,7 @@ struct DeviceConfig: View { @State var debugLogEnabled = false @State var rebroadcastMode = 0 @State var doubleTapAsButtonPress = false + @State var isManaged = false var body: some View { @@ -88,6 +89,13 @@ struct DeviceConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Text("Treat double tap on supported accelerometers as a user button press.") .font(.caption) + + Toggle(isOn: $isManaged) { + Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it.") + .font(.caption) } Section(header: Text("Debug")) { @@ -217,6 +225,7 @@ struct DeviceConfig: View { dc.buzzerGpio = UInt32(buzzerGPIO) dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue() dc.doubleTapAsButtonPress = doubleTapAsButtonPress + dc.isManaged = isManaged let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { @@ -295,12 +304,15 @@ struct DeviceConfig: View { } } .onChange(of: doubleTapAsButtonPress) { newDoubleTapAsButtonPress in - if node != nil && node!.deviceConfig != nil { - if newDoubleTapAsButtonPress != node!.deviceConfig!.doubleTapAsButtonPress { hasChanges = true } } } + .onChange(of: isManaged) { newIsManaged in + if node != nil && node!.deviceConfig != nil { + if newIsManaged != node!.deviceConfig!.isManaged { hasChanges = true } + } + } } func setDeviceValues() { self.deviceRole = Int(node?.deviceConfig?.role ?? 0) @@ -310,6 +322,7 @@ struct DeviceConfig: View { self.buzzerGPIO = Int(node?.deviceConfig?.buzzerGpio ?? 0) self.rebroadcastMode = Int(node?.deviceConfig?.rebroadcastMode ?? 0) self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false + self.isManaged = node?.deviceConfig?.isManaged ?? false self.hasChanges = false } } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index c7f3793b..3fa24d14 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -8,7 +8,7 @@ import SwiftUI struct Settings: View { - + @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "user.longName", ascending: true)], animation: .default) @@ -16,9 +16,9 @@ struct Settings: View { @State private var selectedNode: Int = 0 @State private var connectedNodeNum: Int = 0 @State private var initialLoad: Bool = true - + @State private var selection: SettingsSidebar = .about - + enum SettingsSidebar { case appSettings case shareChannels @@ -41,7 +41,7 @@ struct Settings: View { case adminMessageLog case about } - + var body: some View { NavigationSplitView { List { @@ -50,7 +50,7 @@ struct Settings: View { } label: { Image(systemName: "questionmark.app") .symbolRenderingMode(.hierarchical) - + Text("about.meshtastic") } .tag(SettingsSidebar.about) @@ -63,227 +63,231 @@ struct Settings: View { } .tag(SettingsSidebar.appSettings) let node = nodes.first(where: { $0.num == connectedNodeNum }) - Section("Configure") { - Picker("Configuring Node", selection: $selectedNode) { - if selectedNode == 0 { - Text("Connect to a Node").tag(0) - } - ForEach(nodes) { node in - if node.num == bleManager.connectedPeripheral?.num ?? 0 { - Text("BLE Config: \(node.user?.longName ?? "unknown".localized)") - .tag(Int(node.num)) - } else if node.metadata != nil { - Text("Remote Config: \(node.user?.longName ?? "unknown".localized)") - .tag(Int(node.num)) - } else { - Text("Request Admin: \(node.user?.longName ?? "unknown".localized)") - .tag(Int(node.num)) + if !(node?.deviceConfig?.isManaged ?? false) { + + Section("Configure") { + Picker("Configuring Node", selection: $selectedNode) { + if selectedNode == 0 { + Text("Connect to a Node").tag(0) + } + ForEach(nodes) { node in + if node.num == bleManager.connectedPeripheral?.num ?? 0 { + Text("BLE Config: \(node.user?.longName ?? "unknown".localized)") + .tag(Int(node.num)) + } else if node.metadata != nil { + Text("Remote Config: \(node.user?.longName ?? "unknown".localized)") + .tag(Int(node.num)) + } else { + Text("Request Admin: \(node.user?.longName ?? "unknown".localized)") + .tag(Int(node.num)) + } } } - } - .pickerStyle(.automatic) - .labelsHidden() - .onChange(of: selectedNode) { newValue in - if selectedNode > 0 { - let node = nodes.first(where: { $0.num == newValue }) - let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) - connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) - - if connectedNode != nil && node?.metadata == nil { - let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) - - if adminMessageId > 0 { - print("Sent node metadata request from node details") + .pickerStyle(.automatic) + .labelsHidden() + .onChange(of: selectedNode) { newValue in + if selectedNode > 0 { + let node = nodes.first(where: { $0.num == newValue }) + let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) + connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) + + if connectedNode != nil && node?.metadata == nil { + let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) + + if adminMessageId > 0 { + print("Sent node metadata request from node details") + } } } } } - } - Section("radio.configuration") { - - NavigationLink { - ShareChannels(node: nodes.first(where: { $0.num == connectedNodeNum })) - } label: { - Image(systemName: "qrcode") - .symbolRenderingMode(.hierarchical) - Text("share.channels") + Section("radio.configuration") { + + NavigationLink { + ShareChannels(node: nodes.first(where: { $0.num == connectedNodeNum })) + } label: { + Image(systemName: "qrcode") + .symbolRenderingMode(.hierarchical) + Text("share.channels") + } + .tag(SettingsSidebar.shareChannels) + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) + + NavigationLink { + UserConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + + Image(systemName: "person.crop.rectangle.fill") + .symbolRenderingMode(.hierarchical) + Text("user") + } + .tag(SettingsSidebar.userConfig) + + NavigationLink { + LoRaConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "dot.radiowaves.left.and.right") + .symbolRenderingMode(.hierarchical) + Text("lora") + } + .tag(SettingsSidebar.loraConfig) + + NavigationLink { + Channels(node: nodes.first(where: { $0.num == connectedNodeNum })) + } label: { + Image(systemName: "fibrechannel") + .symbolRenderingMode(.hierarchical) + Text("channels") + } + .tag(SettingsSidebar.channelConfig) + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) + + NavigationLink { + BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "antenna.radiowaves.left.and.right") + .symbolRenderingMode(.hierarchical) + Text("bluetooth") + } + .tag(SettingsSidebar.bluetoothConfig) + + NavigationLink { + DeviceConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "flipphone") + .symbolRenderingMode(.hierarchical) + Text("device") + } + .tag(SettingsSidebar.deviceConfig) + + NavigationLink { + DisplayConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "display") + .symbolRenderingMode(.hierarchical) + Text("display") + } + .tag(SettingsSidebar.displayConfig) + + NavigationLink { + NetworkConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + + Image(systemName: "network") + .symbolRenderingMode(.hierarchical) + Text("network") + } + .tag(SettingsSidebar.networkConfig) + + NavigationLink { + PositionConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + + Image(systemName: "location") + .symbolRenderingMode(.hierarchical) + Text("position") + } + .tag(SettingsSidebar.positionConfig) + } - .tag(SettingsSidebar.shareChannels) - .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) - - NavigationLink { - UserConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - - Image(systemName: "person.crop.rectangle.fill") - .symbolRenderingMode(.hierarchical) - Text("user") + Section("module.configuration") { + + NavigationLink { + CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + + Image(systemName: "list.bullet.rectangle.fill") + .symbolRenderingMode(.hierarchical) + + Text("canned.messages") + } + .tag(SettingsSidebar.cannedMessagesConfig) + + NavigationLink { + ExternalNotificationConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "megaphone") + .symbolRenderingMode(.hierarchical) + Text("external.notification") + } + .tag(SettingsSidebar.externalNotificationConfig) + + NavigationLink { + MQTTConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "dot.radiowaves.right") + .symbolRenderingMode(.hierarchical) + Text("mqtt") + } + .tag(SettingsSidebar.mqttConfig) + + NavigationLink { + RangeTestConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "point.3.connected.trianglepath.dotted") + .symbolRenderingMode(.hierarchical) + Text("range.test") + } + NavigationLink { + RtttlConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "music.note.list") + .symbolRenderingMode(.hierarchical) + Text("ringtone") + } + .tag(SettingsSidebar.ringtoneConfig) + + NavigationLink { + SerialConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "terminal") + .symbolRenderingMode(.hierarchical) + Text("serial") + } + .tag(SettingsSidebar.serialConfig) + + NavigationLink { + TelemetryConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "chart.xyaxis.line") + .symbolRenderingMode(.hierarchical) + Text("telemetry") + } + .tag(SettingsSidebar.telemetryConfig) } - .tag(SettingsSidebar.userConfig) - - NavigationLink { - LoRaConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - Image(systemName: "dot.radiowaves.left.and.right") - .symbolRenderingMode(.hierarchical) - Text("lora") + + Section(header: Text("logging")) { + NavigationLink { + MeshLog() + } label: { + Image(systemName: "list.bullet.rectangle") + .symbolRenderingMode(.hierarchical) + Text("mesh.log") + } + .tag(SettingsSidebar.meshLog) + + NavigationLink { + let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) + AdminMessageList(user: connectedNode?.user) + } label: { + Image(systemName: "building.columns") + .symbolRenderingMode(.hierarchical) + Text("admin.log") + } + .tag(SettingsSidebar.adminMessageLog) } - .tag(SettingsSidebar.loraConfig) - - NavigationLink { - Channels(node: nodes.first(where: { $0.num == connectedNodeNum })) - } label: { - Image(systemName: "fibrechannel") - .symbolRenderingMode(.hierarchical) - Text("channels") + Section(header: Text("Firmware")) { + NavigationLink { + Firmware(node: nodes.first(where: { $0.num == connectedNodeNum })) + } label: { + Image(systemName: "arrow.up.arrow.down.square") + .symbolRenderingMode(.hierarchical) + + Text("Firmware Updates") + } + .tag(SettingsSidebar.about) + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) } - .tag(SettingsSidebar.channelConfig) - .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) - - NavigationLink { - BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - Image(systemName: "antenna.radiowaves.left.and.right") - .symbolRenderingMode(.hierarchical) - Text("bluetooth") - } - .tag(SettingsSidebar.bluetoothConfig) - - NavigationLink { - DeviceConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - Image(systemName: "flipphone") - .symbolRenderingMode(.hierarchical) - Text("device") - } - .tag(SettingsSidebar.deviceConfig) - - NavigationLink { - DisplayConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - Image(systemName: "display") - .symbolRenderingMode(.hierarchical) - Text("display") - } - .tag(SettingsSidebar.displayConfig) - - NavigationLink { - NetworkConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - - Image(systemName: "network") - .symbolRenderingMode(.hierarchical) - Text("network") - } - .tag(SettingsSidebar.networkConfig) - - NavigationLink { - PositionConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - - Image(systemName: "location") - .symbolRenderingMode(.hierarchical) - Text("position") - } - .tag(SettingsSidebar.positionConfig) - - } - Section("module.configuration") { - - NavigationLink { - CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - - Image(systemName: "list.bullet.rectangle.fill") - .symbolRenderingMode(.hierarchical) - - Text("canned.messages") - } - .tag(SettingsSidebar.cannedMessagesConfig) - - NavigationLink { - ExternalNotificationConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - Image(systemName: "megaphone") - .symbolRenderingMode(.hierarchical) - Text("external.notification") - } - .tag(SettingsSidebar.externalNotificationConfig) - - NavigationLink { - MQTTConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - Image(systemName: "dot.radiowaves.right") - .symbolRenderingMode(.hierarchical) - Text("mqtt") - } - .tag(SettingsSidebar.mqttConfig) - - NavigationLink { - RangeTestConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - Image(systemName: "point.3.connected.trianglepath.dotted") - .symbolRenderingMode(.hierarchical) - Text("range.test") - } - NavigationLink { - RtttlConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - Image(systemName: "music.note.list") - .symbolRenderingMode(.hierarchical) - Text("ringtone") - } - .tag(SettingsSidebar.ringtoneConfig) - - NavigationLink { - SerialConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - Image(systemName: "terminal") - .symbolRenderingMode(.hierarchical) - Text("serial") - } - .tag(SettingsSidebar.serialConfig) - - NavigationLink { - TelemetryConfig(node: nodes.first(where: { $0.num == selectedNode })) - } label: { - Image(systemName: "chart.xyaxis.line") - .symbolRenderingMode(.hierarchical) - Text("telemetry") - } - .tag(SettingsSidebar.telemetryConfig) - } - Section(header: Text("logging")) { - NavigationLink { - MeshLog() - } label: { - Image(systemName: "list.bullet.rectangle") - .symbolRenderingMode(.hierarchical) - Text("mesh.log") - } - .tag(SettingsSidebar.meshLog) - - NavigationLink { - let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) - AdminMessageList(user: connectedNode?.user) - } label: { - Image(systemName: "building.columns") - .symbolRenderingMode(.hierarchical) - Text("admin.log") - } - .tag(SettingsSidebar.adminMessageLog) - } - Section(header: Text("Firmware")) { - NavigationLink { - Firmware(node: nodes.first(where: { $0.num == connectedNodeNum })) - } label: { - Image(systemName: "arrow.up.arrow.down.square") - .symbolRenderingMode(.hierarchical) - - Text("Firmware Updates") - } - .tag(SettingsSidebar.about) - .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) } } .onAppear { @@ -297,11 +301,11 @@ struct Settings: View { .listStyle(GroupedListStyle()) .navigationTitle("settings") .navigationBarItems(leading: - MeshtasticLogo() + MeshtasticLogo() ) } - detail: { - Text("select.menu.item") - } + detail: { + Text("select.menu.item") + } } } diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 80820174..040aa124 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -139,7 +139,7 @@ "map"="Mesh Map"; "map.type"="Default Type"; "map.centering"="Centering Mode"; -"map.tiles.delete"="Delete Map Tiles"; +"map.tiles.delete"="Delete All Map Tiles"; "map.recentering"="Automatic Re-centering"; "map.usertrackingmode"="User tracking mode"; "map.usertrackingmode.follow"="Follow"; From 0c852a52024968f5ee0e068c422c8d038193d6b1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 14 May 2023 00:16:55 -0700 Subject: [PATCH 5/7] Weather layer --- Meshtastic/Enums/AppSettingsEnums.swift | 89 +++++++++++++++++- Meshtastic/Extensions/UserDefaults.swift | 23 ++++- .../Helpers/Map/OfflineTileManager.swift | 6 +- .../Views/Map/Custom/MapViewSwiftUI.swift | 14 ++- Meshtastic/Views/Nodes/NodeMap.swift | 94 +++++++++++++------ Meshtastic/Views/Settings/AppSettings.swift | 52 +++++----- 6 files changed, 212 insertions(+), 66 deletions(-) diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index 8044cde8..5c964f0c 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -137,7 +137,7 @@ enum MapLayer: String, CaseIterable, Equatable { var localized: String { self.rawValue.localized } } -enum MapTileServerLinks: String, CaseIterable, Identifiable { +enum MapTileServer: String, CaseIterable, Identifiable { case openStreetMap case openStreetMapDE @@ -269,7 +269,86 @@ enum MapTileServerLinks: String, CaseIterable, Identifiable { } } -//enum MapOverlayServerLinks: String, CaseIterable, Identifiable { -// -// -//} +enum MapOverlayServer: String, CaseIterable, Identifiable { + + case baseReReflectivityCurrent + case baseReReflectivityOneHourAgo + case echoTopsEetCurrent + case echoTopsEetOneHourAgo + case q2OneHourPrecipitation + case q2TwentyFourHourPrecipitation + case q2FortyEightHourPrecipitation + case q2SeventyTwoHourPrecipitation + case mrmsHybridScanReflectivityComposite + + var id: String { self.rawValue } + var attribution: String { + return "Weather layers via Iowa State University Iowa Environmental Mesonet [OGC Web Services](https://mesonet.agron.iastate.edu/ogc/)" + } + var description: String { + switch self { + case .baseReReflectivityCurrent: + return "NEXRAD Base Reflectivity current" + case .baseReReflectivityOneHourAgo: + return "NEXRAD Base Reflectivity one hour ago" + case .echoTopsEetCurrent: + return "NEXRAD Echo Tops EET current" + case .echoTopsEetOneHourAgo: + return "NEXRAD Echo Tops EET one hour ago" + case .q2OneHourPrecipitation: + return "Q2 1 Hour Precipitation" + case .q2TwentyFourHourPrecipitation: + return "Q2 24 Hour Precipitation" + case .q2FortyEightHourPrecipitation: + return "Q2 48 Hour Precipitation" + case .q2SeventyTwoHourPrecipitation: + return "Q2 72 Hour Precipitation" + case .mrmsHybridScanReflectivityComposite: + return "MRMS Hybrid-Scan Reflectivity Composite" + } + } + var tileUrl: String { + switch self { + case .baseReReflectivityCurrent: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-900913/{z}/{x}/{y}" + case .baseReReflectivityOneHourAgo: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-900913-m55m/{z}/{x}/{y}" + case .echoTopsEetCurrent: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-eet-900913/{z}/{x}/{y}" + case .echoTopsEetOneHourAgo: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-eet-900913-m55m/{z}/{x}/{y}" + case .q2OneHourPrecipitation: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-n1p-900913/{z}/{x}/{y}" + case .q2TwentyFourHourPrecipitation: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-p24h-900913/{z}/{x}/{y}" + case .q2FortyEightHourPrecipitation: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-p48h-900913/{z}/{x}/{y}" + case .q2SeventyTwoHourPrecipitation: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-p72h-900913/{z}/{x}/{y}" + case .mrmsHybridScanReflectivityComposite: + return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-hsr-900913/{z}/{x}/{y}" + } + } + var zoomRange: [Int] { + switch self { + case .baseReReflectivityCurrent: + return [Int](0...18) + case .baseReReflectivityOneHourAgo: + return [Int](0...18) + case .echoTopsEetCurrent: + return [Int](0...18) + case .echoTopsEetOneHourAgo: + return [Int](0...18) + case .q2OneHourPrecipitation: + return [Int](0...18) + case .q2TwentyFourHourPrecipitation: + return [Int](0...18) + case .q2FortyEightHourPrecipitation: + return [Int](0...18) + case .q2SeventyTwoHourPrecipitation: + return [Int](0...18) + case .mrmsHybridScanReflectivityComposite: + return [Int](0...18) + } + } +} diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 94764f97..b711f89c 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -115,16 +115,35 @@ extension UserDefaults { } } - static var mapTileServer: MapTileServerLinks { + static var mapTileServer: MapTileServer { get { - MapTileServerLinks(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServerLinks.openStreetMap.rawValue) ?? MapTileServerLinks.openStreetMap + MapTileServer(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServer.openStreetMap.rawValue) ?? MapTileServer.openStreetMap } set { UserDefaults.standard.set(newValue.rawValue, forKey: "mapTileServer") } } + static var enableOverlayServer: Bool { + get { + UserDefaults.standard.bool(forKey: "enableOverlayServer") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableOverlayServer") + } + } + + static var mapOverlayServer: MapOverlayServer { + get { + + MapOverlayServer(rawValue: UserDefaults.standard.string(forKey: "mapOverlayServer") ?? MapOverlayServer.baseReReflectivityCurrent.rawValue) ?? MapOverlayServer.baseReReflectivityCurrent + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: "mapOverlayServer") + } + } + static var mapTilesAboveLabels: Bool { get { UserDefaults.standard.bool(forKey: "mapTilesAboveLabels") diff --git a/Meshtastic/Helpers/Map/OfflineTileManager.swift b/Meshtastic/Helpers/Map/OfflineTileManager.swift index 640fa078..fe6e4508 100644 --- a/Meshtastic/Helpers/Map/OfflineTileManager.swift +++ b/Meshtastic/Helpers/Map/OfflineTileManager.swift @@ -22,7 +22,7 @@ class OfflineTileManager: ObservableObject { } // MARK: - Private properties - private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.tileUrl.count > 1 ? UserDefaults.mapTileServer.tileUrl : MapTileServerLinks.openStreetMap.tileUrl) } + private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.tileUrl.count > 1 ? UserDefaults.mapTileServer.tileUrl : MapTileServer.openStreetMap.tileUrl) } private var documentsDirectory: URL { fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! } @@ -48,7 +48,7 @@ class OfflineTileManager: ObservableObject { return Double(count) * size } - func getDownloadedSize(for mapTileLink: MapTileServerLinks) -> Double { + func getDownloadedSize(for mapTileLink: MapTileServer) -> Double { var accumulatedSize: UInt64 = 0 let mapTiles = try! fileManager.contentsOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"), includingPropertiesForKeys: []) @@ -90,7 +90,7 @@ class OfflineTileManager: ObservableObject { createDirectoriesIfNecessary() } - func remove(for mapTileLink: MapTileServerLinks) { + func remove(for mapTileLink: MapTileServer) { let mapTiles = try! fileManager.contentsOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"), includingPropertiesForKeys: []) let matchingTiles = mapTiles.filter { fileName in diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index bd4e9e36..76a1a840 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -19,7 +19,8 @@ struct MapViewSwiftUI: UIViewRepresentable { let mapView = MKMapView() // Parameters - var selectedMapLayer: MapLayer + let selectedMapLayer: MapLayer + let selectedWeatherLayer: MapOverlayServer = UserDefaults.mapOverlayServer let positions: [PositionEntity] let waypoints: [WaypointEntity] @@ -116,6 +117,17 @@ struct MapViewSwiftUI: UIViewRepresentable { default: mapView.mapType = .standard } + // Weather radar + if UserDefaults.enableOverlayServer { + let locale = Locale.current + if locale.region?.identifier ?? "no locale" == "US" { + let overlay = MKTileOverlay(urlTemplate: selectedWeatherLayer.tileUrl) + overlay.canReplaceMapContent = false + overlay.minimumZ = selectedWeatherLayer.zoomRange.startIndex + overlay.maximumZ = selectedWeatherLayer.zoomRange.endIndex + mapView.addOverlay(overlay, level: .aboveLabels) + } + } } func makeUIView(context: Context) -> MKMapView { diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 30e76b4b..9ebe55ee 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -11,62 +11,64 @@ import CoreLocation import CoreData struct NodeMap: View { - + @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @ObservedObject var tileManager = OfflineTileManager.shared - + @State var selectedMapLayer: MapLayer = UserDefaults.mapLayer @State var enableMapRecentering: Bool = UserDefaults.enableMapRecentering @State var enableMapRouteLines: Bool = UserDefaults.enableMapRouteLines @State var enableMapNodeHistoryPins: Bool = UserDefaults.enableMapNodeHistoryPins @State var enableOfflineMaps: Bool = UserDefaults.enableOfflineMaps - @State var selectedTileServer: MapTileServerLinks = UserDefaults.mapTileServer + @State var selectedTileServer: MapTileServer = UserDefaults.mapTileServer @State var enableOfflineMapsMBTiles: Bool = UserDefaults.enableOfflineMapsMBTiles + @State var enableOverlayServer: Bool = UserDefaults.enableOverlayServer + @State var selectedOverlayServer: MapOverlayServer = UserDefaults.mapOverlayServer @State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels - + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .none) private var positions: FetchedResults - + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], predicate: NSPredicate( format: "expire == nil || expire >= %@", Date() as NSDate ), animation: .none) private var waypoints: FetchedResults @State var waypointCoordinate: WaypointCoordinate? - + @State var selectedTracking: UserTrackingModes = .none @State var isPresentingInfoSheet: Bool = false @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( - mapName: "offlinemap", - tileType: "png", - canReplaceMapContent: true - ) - + mapName: "offlinemap", + tileType: "png", + canReplaceMapContent: true + ) + var body: some View { - + NavigationStack { ZStack { - + MapViewSwiftUI( - onLongPress: { coord in + onLongPress: { coord in waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0) }, onWaypointEdit: { wpId in if wpId > 0 { waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId)) } }, - selectedMapLayer: selectedMapLayer, - positions: Array(positions), - waypoints: Array(waypoints), - userTrackingMode: selectedTracking.MKUserTrackingModeValue(), - showNodeHistory: enableMapNodeHistoryPins, - showRouteLines: enableMapRouteLines, - customMapOverlay: self.customMapOverlay + selectedMapLayer: selectedMapLayer, + positions: Array(positions), + waypoints: Array(waypoints), + userTrackingMode: selectedTracking.MKUserTrackingModeValue(), + showNodeHistory: enableMapNodeHistoryPins, + showRouteLines: enableMapRouteLines, + customMapOverlay: self.customMapOverlay ) VStack(alignment: .trailing) { @@ -84,9 +86,9 @@ struct NodeMap: View { .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) .frame(maxHeight: .infinity) .sheet(item: $waypointCoordinate, content: { wpc in - WaypointFormView(coordinate: wpc) - .presentationDetents([.medium, .large]) - .presentationDragIndicator(.automatic) + WaypointFormView(coordinate: wpc) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.automatic) }) .sheet(isPresented: $isPresentingInfoSheet) { VStack { @@ -137,6 +139,38 @@ struct NodeMap: View { self.enableMapRouteLines.toggle() UserDefaults.enableMapRouteLines = self.enableMapRouteLines } + + let locale = Locale.current + if locale.region?.identifier ?? "no locale" == "US" { + + Toggle(isOn: $enableOverlayServer) { + + Label("Show Weather", systemImage: "cloud.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.enableOverlayServer.toggle() + UserDefaults.enableOverlayServer = self.enableOverlayServer + } + + if enableOverlayServer { + Picker(selection: $selectedOverlayServer, + label: Text("Radar")) { + ForEach(MapOverlayServer.allCases, id: \.self) { mos in + Text(mos.description) + .font(.footnote) + } + } + .pickerStyle(DefaultPickerStyle()) + .onChange(of: (selectedOverlayServer)) { newSelectedOverlayServer in + UserDefaults.mapOverlayServer = newSelectedOverlayServer + } + Text(LocalizedStringKey(selectedOverlayServer.attribution)) + .font(.footnote) + .foregroundColor(.gray) + .padding(0) + } + } } Section(header: Text("Offline Maps")) { Toggle(isOn: $enableOfflineMaps) { @@ -158,14 +192,14 @@ struct NodeMap: View { Picker(selection: $selectedTileServer, label: Text("Tile Server")) { - ForEach(MapTileServerLinks.allCases, id: \.self) { tsl in + ForEach(MapTileServer.allCases, id: \.self) { tsl in Text(tsl.description) } } - .pickerStyle(DefaultPickerStyle()) - .onChange(of: (selectedTileServer)) { newSelectedTileServer in - UserDefaults.mapTileServer = newSelectedTileServer - } + .pickerStyle(DefaultPickerStyle()) + .onChange(of: (selectedTileServer)) { newSelectedTileServer in + UserDefaults.mapTileServer = newSelectedTileServer + } Text("Attribution:") .fontWeight(.semibold) .font(.footnote) @@ -212,7 +246,7 @@ struct NodeMap: View { .padding(.bottom) #endif } - .presentationDetents([UserDefaults.enableOfflineMaps ? .large : .medium]) + .presentationDetents([UserDefaults.enableOfflineMaps || UserDefaults.enableOverlayServer ? .large : .medium]) .presentationDragIndicator(.visible) } } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 4310d105..4ad86d70 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -16,7 +16,6 @@ struct AppSettings: View { @State var provideLocationInterval: Int = UserDefaults.provideLocationInterval @State private var isPresentingCoreDataResetConfirm = false @State private var isPresentingDeleteMapTilesConfirm = false - @State var selectedTileServer: MapTileServerLinks? = nil var body: some View { VStack { @@ -106,33 +105,36 @@ struct AppSettings: View { } } } - Section(header: Text("Map Tile Data")) { - Button { - isPresentingDeleteMapTilesConfirm = true - } label: { - Label("\("map.tiles.delete".localized) (\(totalDownloadedTileSize))", systemImage: "trash") - .foregroundColor(.red) - } - .confirmationDialog( - "are.you.sure", - isPresented: $isPresentingDeleteMapTilesConfirm, - titleVisibility: .visible - ) { - Button("Delete all map tiles?", role: .destructive) { - tileManager.removeAll() - totalDownloadedTileSize = tileManager.getAllDownloadedSize() - print("delete all tiles") - } - } - ForEach(MapTileServerLinks.allCases, id: \.self) { tsl in - + if totalDownloadedTileSize != "0MB" { + Section(header: Text("Map Tile Data")) { Button { - tileManager.remove(for: tsl) - totalDownloadedTileSize = tileManager.getAllDownloadedSize() + isPresentingDeleteMapTilesConfirm = true } label: { - Label("Delete \(tsl.description) Tiles", systemImage: "trash") + Label("\("map.tiles.delete".localized) (\(totalDownloadedTileSize))", systemImage: "trash") .foregroundColor(.red) - .font(.footnote) + } + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingDeleteMapTilesConfirm, + titleVisibility: .visible + ) { + Button("Delete all map tiles?", role: .destructive) { + tileManager.removeAll() + totalDownloadedTileSize = tileManager.getAllDownloadedSize() + print("delete all tiles") + } + } + + ForEach(MapTileServer.allCases, id: \.self) { tsl in + + Button { + tileManager.remove(for: tsl) + totalDownloadedTileSize = tileManager.getAllDownloadedSize() + } label: { + Label("Delete \(tsl.description) Tiles", systemImage: "trash") + .foregroundColor(.red) + .font(.footnote) + } } } } From 26b6ee3735d0573ac42793dd32b024afbc01c892 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 14 May 2023 00:19:42 -0700 Subject: [PATCH 6/7] Attribution update --- Meshtastic/Enums/AppSettingsEnums.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index 5c964f0c..98e2b3b9 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -283,18 +283,18 @@ enum MapOverlayServer: String, CaseIterable, Identifiable { var id: String { self.rawValue } var attribution: String { - return "Weather layers via Iowa State University Iowa Environmental Mesonet [OGC Web Services](https://mesonet.agron.iastate.edu/ogc/)" + return "NEXRAD Weather tiles from Iowa State University Environmental Mesonet [OGC Web Services](https://mesonet.agron.iastate.edu/ogc/)." } var description: String { switch self { case .baseReReflectivityCurrent: - return "NEXRAD Base Reflectivity current" + return "Base Reflectivity current" case .baseReReflectivityOneHourAgo: - return "NEXRAD Base Reflectivity one hour ago" + return "Base Reflectivity one hour ago" case .echoTopsEetCurrent: - return "NEXRAD Echo Tops EET current" + return "Echo Tops EET current" case .echoTopsEetOneHourAgo: - return "NEXRAD Echo Tops EET one hour ago" + return "Echo Tops EET one hour ago" case .q2OneHourPrecipitation: return "Q2 1 Hour Precipitation" case .q2TwentyFourHourPrecipitation: From d27123a49a5695b4161ed02a6ef26d06a3986e39 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 14 May 2023 09:40:36 -0700 Subject: [PATCH 7/7] Delete unused code --- Meshtastic/Helpers/Map/OfflineTileManager.swift | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Meshtastic/Helpers/Map/OfflineTileManager.swift b/Meshtastic/Helpers/Map/OfflineTileManager.swift index fe6e4508..fe346d18 100644 --- a/Meshtastic/Helpers/Map/OfflineTileManager.swift +++ b/Meshtastic/Helpers/Map/OfflineTileManager.swift @@ -63,15 +63,6 @@ class OfflineTileManager: ObservableObject { } return Double(accumulatedSize) - -// let paths = self.computeTileOverlayPaths(boundingBox: boundingBox) -// -// for path in paths { -// let file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png" -// let url = documentsDirectory.appendingPathComponent(file) -// accumulatedSize += (try? url.regularFileAllocatedSize()) ?? 0 -// } -// return Double(accumulatedSize) } func getDownloadedSize(for boundingBox: MKMapRect) -> Double {