diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 9581b975..b01e0ee0 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -100,6 +100,8 @@ DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA0B6B1294CDC55001356EC /* Channels.swift */; }; DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */; }; DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */; }; + DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAB580C2B0DAA9E00147258 /* Routes.swift */; }; + DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */; }; DDAD49ED2AFB39DC00B4425D /* MeshMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAD49EC2AFB39DC00B4425D /* MeshMap.swift */; }; DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAF8C5226EB1DF10058C060 /* BLEManager.swift */; }; DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */; }; @@ -314,6 +316,9 @@ DDA0B6B1294CDC55001356EC /* Channels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channels.swift; sourceTree = ""; }; DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelRoles.swift; sourceTree = ""; }; DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPackets.swift; sourceTree = ""; }; + DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV20.xcdatamodel; sourceTree = ""; }; + DDAB580C2B0DAA9E00147258 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Routes.swift; sourceTree = ""; }; + DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEntityExtension.swift; sourceTree = ""; }; DDAD49EC2AFB39DC00B4425D /* MeshMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMap.swift; sourceTree = ""; }; DDAF8C5226EB1DF10058C060 /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = ""; }; DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothConfig.swift; sourceTree = ""; }; @@ -472,10 +477,26 @@ DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */, DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */, DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */, + DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */, ); path = CoreData; sourceTree = ""; }; + DD2100802B0E676E00F2F116 /* Routes */ = { + isa = PBXGroup; + children = ( + DD2100832B0E67AD00F2F116 /* RouteMap */, + ); + path = Routes; + sourceTree = ""; + }; + DD2100832B0E67AD00F2F116 /* RouteMap */ = { + isa = PBXGroup; + children = ( + ); + path = RouteMap; + sourceTree = ""; + }; DD47E3CA26F0E50300029299 /* Nodes */ = { isa = PBXGroup; children = ( @@ -503,9 +524,11 @@ DD4A911C2708C57100501B7E /* Settings */ = { isa = PBXGroup; children = ( + DD2100802B0E676E00F2F116 /* Routes */, DD97E96728EFE9A00056DDA4 /* About.swift */, DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */, DD4A911D2708C65400501B7E /* AppSettings.swift */, + DDAB580C2B0DAA9E00147258 /* Routes.swift */, DDA0B6B1294CDC55001356EC /* Channels.swift */, DDD6EEAE29BC024700383354 /* Firmware.swift */, DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */, @@ -1208,6 +1231,7 @@ DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */, DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */, DD5E523A298EFA5300D21B61 /* TelemetryWeather.swift in Sources */, + DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */, C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */, DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */, DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */, @@ -1223,6 +1247,7 @@ DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */, DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */, DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */, + DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */, DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */, DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */, DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */, @@ -1435,7 +1460,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.12; + MARKETING_VERSION = 2.2.13; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1469,7 +1494,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.12; + MARKETING_VERSION = 2.2.13; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1591,7 +1616,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.12; + MARKETING_VERSION = 2.2.13; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1624,7 +1649,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.12; + MARKETING_VERSION = 2.2.13; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1735,6 +1760,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */, DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */, DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */, DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */, @@ -1755,7 +1781,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */; + currentVersion = DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index d2d13979..dd0a94ed 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -85,7 +85,6 @@ enum UserTrackingModes: Int, CaseIterable, Identifiable { } enum LocationUpdateInterval: Int, CaseIterable, Identifiable { - case fiveSeconds = 5 case tenSeconds = 10 case fifteenSeconds = 15 case thirtySeconds = 30 @@ -97,8 +96,6 @@ enum LocationUpdateInterval: Int, CaseIterable, Identifiable { var id: Int { self.rawValue } var description: String { switch self { - case .fiveSeconds: - return "interval.five.seconds".localized case .tenSeconds: return "interval.ten.seconds".localized case .fifteenSeconds: diff --git a/Meshtastic/Enums/DisplayEnums.swift b/Meshtastic/Enums/DisplayEnums.swift index 42eb6339..afecb6ec 100644 --- a/Meshtastic/Enums/DisplayEnums.swift +++ b/Meshtastic/Enums/DisplayEnums.swift @@ -73,6 +73,7 @@ enum ScreenOnIntervals: Int, CaseIterable, Identifiable { enum ScreenCarouselIntervals: Int, CaseIterable, Identifiable { case off = 0 + case fifteenSeconds = 15 case thirtySeconds = 30 case oneMinute = 60 case fiveMinutes = 300 @@ -84,6 +85,8 @@ enum ScreenCarouselIntervals: Int, CaseIterable, Identifiable { switch self { case .off: return "off".localized + case .fifteenSeconds: + return "interval.fifteen.seconds".localized case .thirtySeconds: return "interval.thirty.seconds".localized case .oneMinute: @@ -168,3 +171,29 @@ enum DisplayModes: Int, CaseIterable, Identifiable { } } } + +// Default of 0 is metric +enum Units: Int, CaseIterable, Identifiable { + + case metric = 0 + case imperial = 1 + + var id: Int { self.rawValue } + var description: String { + switch self { + case .metric: + return "Metric" + case .imperial: + return "Imperial" + } + } + func protoEnumValue() -> Config.DisplayConfig.DisplayUnits { + + switch self { + case .metric: + return Config.DisplayConfig.DisplayUnits.metric + case .imperial: + return Config.DisplayConfig.DisplayUnits.imperial + } + } +} diff --git a/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift b/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift new file mode 100644 index 00000000..310fe626 --- /dev/null +++ b/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift @@ -0,0 +1,42 @@ +// +// LocationEntityExtension.swift +// Meshtastic +// +// Copyright (c) Garth Vander Houwen 11/21/23. +// + +import CoreData +import CoreLocation +import MapKit +import SwiftUI + +extension LocationEntity { + + var latitude: Double? { + + let d = Double(latitudeI) + if d == 0 { + return 0 + } + return d / 1e7 + } + + var longitude: Double? { + + let d = Double(longitudeI) + if d == 0 { + return 0 + } + return d / 1e7 + } + + var locationCoordinate: CLLocationCoordinate2D? { + if latitudeI != 0 && longitudeI != 0 { + let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!) + return coord + } else { + return nil + } + } +} + diff --git a/Meshtastic/Extensions/UIColor.swift b/Meshtastic/Extensions/UIColor.swift index 6a8909b9..d9160015 100644 --- a/Meshtastic/Extensions/UIColor.swift +++ b/Meshtastic/Extensions/UIColor.swift @@ -37,5 +37,13 @@ extension UIColor { private func add(_ value: CGFloat, toComponent: CGFloat) -> CGFloat { return max(0, min(1, toComponent + value)) } - + + static var random: UIColor { + return UIColor( + red: .random(in: 0...1), + green: .random(in: 0...1), + blue: .random(in: 0...1), + alpha: 1.0 + ) + } } diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 07c230ad..1c9e21fd 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -12,6 +12,7 @@ extension UserDefaults { case enableRangeTest case meshtasticUsername case preferredPeripheralId + case preferredPeripheralNum case provideLocation case provideLocationInterval case mapLayer @@ -53,6 +54,14 @@ extension UserDefaults { UserDefaults.standard.set(newValue, forKey: "preferredPeripheralId") } } + static var preferredPeripheralNum: Int { + get { + UserDefaults.standard.integer(forKey: "preferredPeripheralNum") + } + set { + UserDefaults.standard.set(newValue, forKey: "preferredPeripheralNum") + } + } static var provideLocation: Bool { get { UserDefaults.standard.bool(forKey: "provideLocation") diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index aa88a530..f49dcc78 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -486,6 +486,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let myInfo = myInfoPacket(myInfo: decodedInfo.myInfo, peripheralId: self.connectedPeripheral.id, context: context!) if myInfo != nil { + UserDefaults.preferredPeripheralNum = Int(myInfo!.myNodeNum) connectedPeripheral.num = myInfo!.myNodeNum connectedPeripheral.name = myInfo?.bleName ?? "unknown".localized connectedPeripheral.longName = myInfo?.bleName ?? "unknown".localized @@ -611,7 +612,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } case .neighborinfoApp: - MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + if let neighborInfo = try? NeighborInfo(serializedData: decodedInfo.packet.decoded.payload) { + MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App App UNHANDLED \(neighborInfo)") + } case .UNRECOGNIZED: MeshLogger.log("🕸️ MESH PACKET received for Other App UNHANDLED \(try! decodedInfo.packet.jsonString())") case .max: diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 074f016d..fbcba258 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV19.xcdatamodel + MeshtasticDataModelV20.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents index 0dc699d5..08699813 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -290,7 +290,7 @@ - + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents new file mode 100644 index 00000000..fc942300 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents @@ -0,0 +1,378 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 8588483c..a02837c3 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -256,10 +256,10 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) guard let mutablePositions = fetchedNode[0].positions!.mutableCopy() as? NSMutableOrderedSet else { return } - /// Don't save the same position over and over. + /// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one. if mutablePositions.count > 0 { let mostRecent = mutablePositions.lastObject as! PositionEntity - if mostRecent.latitudeI == position.latitudeI && mostRecent.longitudeI == position.longitudeI { + if mostRecent.coordinate.distance(from: position.coordinate) < 10 { mutablePositions.remove(mostRecent) } } @@ -413,6 +413,7 @@ func upsertDisplayConfigPacket(config: Meshtastic.Config.DisplayConfig, nodeNum: newDisplayConfig.flipScreen = config.flipScreen newDisplayConfig.oledType = Int32(config.oled.rawValue) newDisplayConfig.displayMode = Int32(config.displaymode.rawValue) + newDisplayConfig.units = Int32(config.units.rawValue) newDisplayConfig.headingBold = config.headingBold fetchedNode[0].displayConfig = newDisplayConfig @@ -425,6 +426,7 @@ func upsertDisplayConfigPacket(config: Meshtastic.Config.DisplayConfig, nodeNum: fetchedNode[0].displayConfig?.flipScreen = config.flipScreen fetchedNode[0].displayConfig?.oledType = Int32(config.oled.rawValue) fetchedNode[0].displayConfig?.displayMode = Int32(config.displaymode.rawValue) + fetchedNode[0].displayConfig?.units = Int32(config.units.rawValue) fetchedNode[0].displayConfig?.headingBold = config.headingBold } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 49281d65..aa1b1a20 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -30,6 +30,7 @@ struct ChannelMessageList: View { @State private var deleteMessageId: Int64 = 0 @State private var replyMessageId: Int64 = 0 @State private var sendPositionWithMessage: Bool = false + @AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1 var body: some View { VStack { @@ -39,7 +40,7 @@ struct ChannelMessageList: View { ScrollView { LazyVStack { ForEach( channel.allPrivateMessages ) { (message: MessageEntity) in - let currentUser: Bool = (bleManager.connectedPeripheral?.num ?? -1 == message.fromUser?.num ? true : false) + let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false) if message.replyID > 0 { let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID }) HStack { diff --git a/Meshtastic/Views/Messages/Messages.swift b/Meshtastic/Views/Messages/Messages.swift index 6c261dfd..879a361c 100644 --- a/Meshtastic/Views/Messages/Messages.swift +++ b/Meshtastic/Views/Messages/Messages.swift @@ -72,7 +72,7 @@ struct Messages: View { } if UserDefaults.preferredPeripheralId.count > 0 { let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(bleManager.connectedPeripheral?.num ?? -1)) + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(UserDefaults.preferredPeripheralNum)) do { guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else { return diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 43cc4b72..d35f6592 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -67,7 +67,7 @@ struct UserList: View { Spacer() if user.vip { Image(systemName: "star.fill") - .foregroundColor(.secondary) + .foregroundColor(.yellow) } if user.messageList.count > 0 { if lastMessageDay == currentDay { diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index c2458d71..39207c6b 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -38,7 +38,7 @@ struct UserMessageList: View { LazyVStack { ForEach( user.messageList ) { (message: MessageEntity) in if user.num != bleManager.connectedPeripheral?.num ?? -1 { - let currentUser: Bool = (bleManager.connectedPeripheral?.num ?? 0 == message.fromUser?.num ?? -1 ? true : false) + let currentUser: Bool = (Int64(UserDefaults.preferredPeripheralNum) == message.fromUser?.num ?? -1 ? true : false) if message.replyID > 0 { let messageReply = user.messageList.first(where: { $0.messageId == message.replyID }) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index fdf4cc4a..f094d5e8 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -22,7 +22,7 @@ struct MapSettingsForm: View { var body: some View { - VStack { + NavigationStack { Form { Section(header: Text("Map Options")) { Picker(selection: $mapLayer, label: Text("")) { @@ -80,6 +80,7 @@ struct MapSettingsForm: View { } } } + #if targetEnvironment(macCatalyst) Spacer() Button { @@ -95,5 +96,6 @@ Spacer() } .presentationDetents([.fraction(0.45), .fraction(0.65)]) .presentationDragIndicator(.visible) + } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 0e07c8fd..48ff4ba1 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -61,18 +61,16 @@ struct NodeMapSwiftUI: View { let nodeColor = UIColor(hex: UInt32(node.num)) /// Route Lines if showRouteLines { - if showRouteLines { - let gradient = LinearGradient( - colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)], - startPoint: .leading, endPoint: .trailing - ) - let dashed = StrokeStyle( - lineWidth: 3, - lineCap: .round, lineJoin: .round, dash: [10, 10] - ) - MapPolyline(coordinates: lineCoords) - .stroke(gradient, style: dashed) - } + let gradient = LinearGradient( + colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)], + startPoint: .leading, endPoint: .trailing + ) + let dashed = StrokeStyle( + lineWidth: 3, + lineCap: .round, lineJoin: .round, dash: [10, 10] + ) + MapPolyline(coordinates: lineCoords) + .stroke(gradient, style: dashed) } /// Convex Hull if showConvexHull { @@ -125,7 +123,7 @@ struct NodeMapSwiftUI: View { .opacity(0.8) .presentationCompactAdaptation(.popover) } - + } else { Image(systemName: "flipphone") .symbolEffect(.pulse.byLayer) @@ -142,7 +140,7 @@ struct NodeMapSwiftUI: View { .opacity(0.8) .presentationCompactAdaptation(.popover) } - + } } else { if showNodeHistory { @@ -155,7 +153,7 @@ struct NodeMapSwiftUI: View { .clipShape(Circle()) .rotationEffect(headingDegrees) .frame(width: 16, height: 16) - + } else { Circle() .fill(Color(UIColor(hex: UInt32(node.num)))) @@ -225,6 +223,8 @@ struct NodeMapSwiftUI: View { } } .onChange(of: node) { + isLookingAround = false + isShowingAltitude = false mostRecent = node.positions?.lastObject as? PositionEntity if node.positions?.count ?? 0 > 1 { position = .automatic @@ -282,8 +282,8 @@ struct NodeMapSwiftUI: View { showWaypoints = !showWaypoints } }) { - Image(systemName: showWaypoints ? "signpost.right.and.left.fill" : "signpost.right.and.left") - .padding(.vertical, 5) + Image(systemName: showWaypoints ? "signpost.right.and.left.fill" : "signpost.right.and.left") + .padding(.vertical, 5) } .tint(Color(UIColor.secondarySystemBackground)) .foregroundColor(.accentColor) @@ -319,13 +319,13 @@ struct NodeMapSwiftUI: View { .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) } - #if targetEnvironment(macCatalyst) +#if targetEnvironment(macCatalyst) /// Hide non fuctional catalyst controls -// MapZoomStepper(scope: mapScope) -// .mapControlVisibility(.visible) -// MapPitchSlider(scope: mapScope) -// .mapControlVisibility(.visible) - #endif + // MapZoomStepper(scope: mapScope) + // .mapControlVisibility(.visible) + // MapPitchSlider(scope: mapScope) + // .mapControlVisibility(.visible) +#endif } .controlSize(.regular) .padding(5) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift index de10f6a5..eba287e7 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift @@ -25,7 +25,8 @@ struct PositionAltitudeChart: View { var body: some View { let nodePositions = Array(node.positions!) as! [PositionEntity] let data = nodePositions.map { PositionAltitude(time: $0.time ?? Date(), altitude: Measurement(value: Double($0.altitude), unit: .meters) ) } - HStack { + GroupBox(label: Label("Altitude", systemImage: "mountain.2")) { + Chart(data, id: \.time) { LineMark( x: .value("Time", $0.time), @@ -56,7 +57,6 @@ struct PositionAltitudeChart: View { } .chartXAxis(.visible) } - .padding() .background(Color(UIColor.secondarySystemBackground)) .opacity(/*@START_MENU_TOKEN@*/0.8/*@END_MENU_TOKEN@*/) } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index 461da979..d92d4b35 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -15,11 +15,32 @@ struct PositionPopover: View { var position: PositionEntity var popover: Bool = true let distanceFormatter = MKDistanceFormatter() + var delay: Double = 0 + @State private var scale: CGFloat = 0.5 var body: some View { + // Node Color from node.num + let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) VStack { HStack { - CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(position.nodePosition?.user?.num ?? 0))), circleSize: 65) - .padding(.trailing, 5) + ZStack { + + if position.nodePosition?.isOnline ?? false { + Circle() + .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) + .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) + .scaleEffect(scale) + .animation( + Animation.easeInOut(duration: 0.6) + .repeatForever().delay(delay), value: scale + ) + .onAppear { + self.scale = 1 + } + .frame(width: 90, height: 90) + } + CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 65) + } + Text(position.nodePosition?.user?.longName ?? "Unknown") .font(.largeTitle) } @@ -129,7 +150,7 @@ struct PositionPopover: View { if position.nodePosition != nil { if position.nodePosition?.user?.vip ?? false { Image(systemName: "star.fill") - .foregroundColor(.accentColor) + .foregroundColor(.yellow) .symbolRenderingMode(.hierarchical) .font(.largeTitle) .padding(.bottom, 5) @@ -137,7 +158,7 @@ struct PositionPopover: View { if position.nodePosition?.hasEnvironmentMetrics ?? false { Image(systemName: "cloud.sun.rain") .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) + .symbolRenderingMode(.multicolor) .font(.largeTitle) .padding(.bottom) } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index f68ed469..54aa5b29 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -11,7 +11,7 @@ import MapKit import CoreLocation struct WaypointForm: View { - + @EnvironmentObject var bleManager: BLEManager @Environment(\.dismiss) private var dismiss @State var waypoint: WaypointEntity @@ -27,9 +27,9 @@ struct WaypointForm: View { @State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours @State private var locked: Bool = false @State private var lockedTo: Int64 = 0 - + var body: some View { - VStack { + NavigationStack { if editMode { Text((waypoint.id > 0) ? "Editing Waypoint" : "Create Waypoint") .font(.largeTitle) @@ -341,7 +341,7 @@ struct WaypointForm: View { } } .padding(.top) - #if targetEnvironment(macCatalyst) +#if targetEnvironment(macCatalyst) Spacer() Button { dismiss() @@ -352,7 +352,7 @@ struct WaypointForm: View { .buttonBorderShape(.capsule) .controlSize(.large) .padding() - #endif +#endif } } } @@ -381,9 +381,11 @@ struct WaypointForm: View { expires = false expire = Date.now.addingTimeInterval(60 * 480) icon = "📍" - latitude = waypoint.coordinate.latitude + latitude = waypoint.coordinate.latitude longitude = waypoint.coordinate.longitude } } + .presentationDetents([.fraction(0.75)]) + .presentationDragIndicator(.visible) } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 58096955..b644f2b6 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -72,7 +72,7 @@ struct NodeDetail: View { NavigationLink { EnvironmentMetricsLog(node: node) } label: { - Image(systemName: "chart.xyaxis.line") + Image(systemName: "cloud.sun.rain") .symbolRenderingMode(.hierarchical) .font(.title) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 8ba12835..a5b5a3db 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -23,7 +23,6 @@ struct NodeListItem: View { VStack(alignment: .leading) { CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65) .padding(.trailing, 5) - BatteryLevelCompact(node: node, font: .caption, iconFont: .callout, color: .accentColor) } VStack(alignment: .leading) { HStack { @@ -33,7 +32,7 @@ struct NodeListItem: View { if node.user?.vip ?? false { Spacer() Image(systemName: "star.fill") - .foregroundColor(.secondary) + .foregroundColor(.yellow) } } if connected { @@ -81,8 +80,30 @@ struct NodeListItem: View { HStack { let preset = ModemPresets(rawValue: Int(modemPreset)) LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true) + .padding(.top, 2) } } + HStack { + BatteryLevelCompact(node: node, font: .caption, iconFont: .callout, color: .accentColor) + + if node.hasPositions { + Image(systemName: "mappin.and.ellipse") + .symbolRenderingMode(.hierarchical) + .font(.callout) + + } + if node.hasEnvironmentMetrics { + Image(systemName: "cloud.sun.rain") + .symbolRenderingMode(.hierarchical) + .font(.callout) + } + if node.hasDetectionSensorMetrics { + Image(systemName: "sensor") + .symbolRenderingMode(.hierarchical) + .font(.callout) + } + } + .padding(.top, 3) } .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 520fbbab..621e9f08 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -44,8 +44,9 @@ struct MeshMap: View { var delay: Double = 0 @State private var scale: CGFloat = 0.5 + /// "time >= %@ && nodePosition != nil && latest == true" @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], - predicate: NSPredicate(format: "time >= %@ && nodePosition != nil && latest == true", Calendar.current.date(byAdding: .day, value: -30, to: Date())! as NSDate), animation: .none) + predicate: NSPredicate(format: "nodePosition != nil && latest == true", Calendar.current.date(byAdding: .day, value: -30, to: Date())! as NSDate), animation: .none) private var positions: FetchedResults @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], @@ -53,6 +54,10 @@ struct MeshMap: View { format: "expire == nil || expire >= %@", Date() as NSDate ), animation: .none) private var waypoints: FetchedResults + + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)], + predicate: NSPredicate(format: "enabled == true", ""), animation: .none) + private var routes: FetchedResults var body: some View { @@ -122,7 +127,39 @@ struct MeshMap: View { selectedPosition = (selectedPosition == position ? nil : position) } } - /// Route Lines + /// Routes + ForEach(Array(routes), id: \.id) { route in + let routeLocations = Array(route.locations!) as! [LocationEntity] + let routeCoords = routeLocations.compactMap({(loc) -> CLLocationCoordinate2D in + return loc.locationCoordinate ?? LocationHelper.DefaultLocation + }) + Annotation("Start", coordinate: routeCoords.first ?? LocationHelper.DefaultLocation) { + ZStack { + Circle() + .fill(Color(.green)) + .strokeBorder(.white, lineWidth: 3) + .frame(width: 15, height: 15) + } + } + .annotationTitles(.automatic) + Annotation("Finish", coordinate: routeCoords.last ?? LocationHelper.DefaultLocation) { + ZStack { + Circle() + .fill(Color(.black)) + .strokeBorder(.white, lineWidth: 3) + .frame(width: 15, height: 15) + } + } + .annotationTitles(.automatic) + let dashed = StrokeStyle( + lineWidth: 3, + lineCap: .round, lineJoin: .round, dash: [7, 10] + ) + MapPolyline(coordinates: routeCoords) + .stroke(Color(UIColor(hex: UInt32(route.color))), style: dashed) + + } + /// Node Route Lines if showRouteLines { let nodePositions = Array(position.nodePosition!.positions!) as! [PositionEntity] let routeCoords = nodePositions.compactMap({(pos) -> CLLocationCoordinate2D in @@ -172,6 +209,19 @@ struct MeshMap: View { } } } + .mapScope(mapScope) + .mapStyle(mapStyle) + .mapControls { + MapScaleView(scope: mapScope) + .mapControlVisibility(.automatic) + MapUserLocationButton(scope: mapScope) + .mapControlVisibility(showUserLocation ? .visible : .hidden) + MapPitchToggle(scope: mapScope) + .mapControlVisibility(.automatic) + MapCompass(scope: mapScope) + .mapControlVisibility(.automatic) + } + .controlSize(.regular) .onTapGesture(count: 1, perform: { location in newWaypointCoord = reader.convert(location , from: .local) }) @@ -184,23 +234,10 @@ struct MeshMap: View { editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480) editingWaypoint!.id = 0 } + } } - .mapScope(mapScope) - .mapStyle(mapStyle) - .mapControls { - MapScaleView(scope: mapScope) - .mapControlVisibility(.visible) - if showUserLocation { - MapUserLocationButton(scope: mapScope) - .mapControlVisibility(.visible) - } - MapPitchToggle(scope: mapScope) - .mapControlVisibility(.visible) - MapCompass(scope: mapScope) - .mapControlVisibility(.visible) - } - .controlSize(.regular) + .sheet(item: $selectedPosition) { selection in PositionPopover(position: selection, popover: false) .padding() diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 03f48f56..737a90d3 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -142,7 +142,7 @@ struct Channels: View { Picker("Key Size", selection: $channelKeySize) { Text("Empty").tag(0) Text("Default").tag(-1) - Text("1 bit").tag(1) + Text("1 byte").tag(1) Text("128 bit").tag(16) Text("192 bit").tag(24) Text("256 bit").tag(32) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index f7d97eb5..682581f4 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -143,8 +143,11 @@ struct DeviceConfig: View { ) { Button("Erase all device and app data?", role: .destructive) { if bleManager.sendNodeDBReset(fromUser: node!.user!, toUser: node!.user!) { - bleManager.disconnectPeripheral() - clearCoreDataDatabase(context: context) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + bleManager.disconnectPeripheral() + clearCoreDataDatabase(context: context) + } + } else { print("NodeDB Reset Failed") } @@ -165,8 +168,10 @@ struct DeviceConfig: View { ) { Button("Factory reset your device and app? ", role: .destructive) { if bleManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!) { - bleManager.disconnectPeripheral() - clearCoreDataDatabase(context: context) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + bleManager.disconnectPeripheral() + clearCoreDataDatabase(context: context) + } } else { print("Factory Reset Failed") } diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index d3d95b7d..f600dda3 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -26,6 +26,7 @@ struct DisplayConfig: View { @State var flipScreen = false @State var oledType = 0 @State var displayMode = 0 + @State var units = 0 var body: some View { @@ -125,6 +126,15 @@ struct DisplayConfig: View { Text("The format used to display GPS coordinates on the device screen.") .font(.caption) .listRowSeparator(.visible) + + Picker("Display Units", selection: $units ) { + ForEach(Units.allCases) { un in + Text(un.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("Units displayed on the device screen") + .font(.caption) } } .disabled(self.bleManager.connectedPeripheral == nil || node?.displayConfig == nil) @@ -160,6 +170,7 @@ struct DisplayConfig: View { dc.flipScreen = flipScreen dc.oled = OledTypes(rawValue: oledType)!.protoEnumValue() dc.displaymode = DisplayModes(rawValue: displayMode)!.protoEnumValue() + dc.units = Units(rawValue: units)!.protoEnumValue() let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { @@ -233,6 +244,11 @@ struct DisplayConfig: View { if newDisplayMode != node!.displayConfig!.displayMode { hasChanges = true } } } + .onChange(of: units) { newUnits in + if node != nil && node!.displayConfig != nil { + if newUnits != node!.displayConfig!.units { hasChanges = true } + } + } } func setDisplayValues() { self.gpsFormat = Int(node?.displayConfig?.gpsFormat ?? 0) @@ -243,6 +259,7 @@ struct DisplayConfig: View { self.flipScreen = node?.displayConfig?.flipScreen ?? false self.oledType = Int(node?.displayConfig?.oledType ?? 0) self.displayMode = Int(node?.displayConfig?.displayMode ?? 0) + self.units = Int(node?.displayConfig?.units ?? 0) self.hasChanges = false } } diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index db176fb5..dccef435 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -184,6 +184,12 @@ struct LoRaConfig: View { .scrollDismissesKeyboard(.immediately) .focused($focusedField, equals: .frequencyOverride) } + HStack { + Image(systemName: "antenna.radiowaves.left.and.right") + .foregroundColor(.accentColor) + Stepper("\(txPower)db Transmit Power", value: $txPower, in: 1...30, step: 1) + .padding(5) + } } } .disabled(self.bleManager.connectedPeripheral == nil || node?.loRaConfig == nil) @@ -214,6 +220,7 @@ struct LoRaConfig: View { lc.modemPreset = ModemPresets(rawValue: modemPreset)!.protoEnumValue() lc.usePreset = usePreset lc.txEnabled = txEnabled + lc.txPower = Int32(txPower) lc.channelNum = UInt32(channelNum) lc.bandwidth = UInt32(bandwidth) lc.codingRate = UInt32(codingRate) @@ -302,6 +309,11 @@ struct LoRaConfig: View { if newOverrideFrequency != node!.loRaConfig!.overrideFrequency { hasChanges = true } } } + .onChange(of: txPower) { newTxPower in + if node != nil && node!.loRaConfig != nil { + if newTxPower != node!.loRaConfig!.txPower { hasChanges = true } + } + } } func setLoRaValues() { self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 3) diff --git a/Meshtastic/Views/Settings/Routes.swift b/Meshtastic/Views/Settings/Routes.swift new file mode 100644 index 00000000..1c3d52c7 --- /dev/null +++ b/Meshtastic/Views/Settings/Routes.swift @@ -0,0 +1,195 @@ +// +// Routes.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 11/21/23. +// + +import SwiftUI +import CoreData +import MapKit + +@available(iOS 17.0, macOS 14.0, *) +struct Routes: View { + + @State private var columnVisibility = NavigationSplitViewVisibility.doubleColumn + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + @State private var selectedRoute: RouteEntity? + @State private var importing = false + @State private var isShowingBadFileAlert = false + + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "enabled", ascending: false), NSSortDescriptor(key: "name", ascending: true), NSSortDescriptor(key: "date", ascending: false)], animation: .default) + + var routes: FetchedResults + var body: some View { + //NavigationSplitView(columnVisibility: $columnVisibility) { + NavigationStack { + Button("Import Route") { + importing = true + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + + .alert(isPresented: $isShowingBadFileAlert) { + Alert(title: Text("Not a valid route file"), message: Text("Your route file must have both Latitude and Longitude."), dismissButton: .default(Text("OK"))) + } + .fileImporter( + isPresented: $importing, + allowedContentTypes: [.commaSeparatedText], + allowsMultipleSelection: false + ) { result in + do { + guard let selectedFile: URL = try result.get().first else { return } + guard selectedFile.startAccessingSecurityScopedResource() else { + return + } + + do { + guard let fileContent = String(data: try Data(contentsOf: selectedFile), encoding: .utf8) else { return } + let routeName = selectedFile.lastPathComponent.dropLast(4) + let lines = fileContent.components(separatedBy: "\n") + let headers = lines.first?.components(separatedBy: ",") + var latIndex = -1 + var longIndex = -1 + for index in headers!.indices { + print("\(index): \( headers![index])") + if headers![index].trimmingCharacters(in: .whitespaces) == "Latitude" { + latIndex = index + } else if headers![index].trimmingCharacters(in: .whitespaces) == "Longitude" { + longIndex = index + } + } + if latIndex >= 0 && longIndex >= 0 { + let newRoute = RouteEntity(context: context) + newRoute.name = String(routeName) + newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max) + newRoute.color = Int64(UIColor.random.hex) + newRoute.date = Date() + newRoute.enabled = true + var newLocations = [LocationEntity]() + lines.dropFirst().forEach { line in + let data = line.components(separatedBy: ",") + if data.count > 1 { + let latitude = latIndex >= 0 ? data[latIndex].trimmingCharacters(in: .whitespaces) : "0" + let longitude = longIndex >= 0 ? data[longIndex].trimmingCharacters(in: .whitespaces) : "0" + let loc = LocationEntity(context: context) + loc.latitudeI = Int32((Double(latitude) ?? 0) * 1e7) + loc.longitudeI = Int32((Double(longitude) ?? 0) * 1e7) + newLocations.append(loc) + print("Longitude: \(longitude) Latitude: \(latitude)") + } + } + newRoute.locations? = NSOrderedSet(array: newLocations) + do { + try context.save() + } catch let error as NSError { + print("Error: \(error.localizedDescription)") + isShowingBadFileAlert = true + } + } else { + isShowingBadFileAlert = true + } + + } catch { + print("error: \(error)") // to do deal with errors + } + + } catch { + print("CSV Import Error") + } + } + + VStack { + List(routes, id: \.self, selection: $selectedRoute) { route in + let routeColor = Color(UIColor(hex: route.color >= 0 ? UInt32(route.color) : 0)) + Label { + VStack (alignment: .leading) { + Text("\(route.name ?? "No Name Route")") + .padding(.top) + .foregroundStyle(.primary) + + Text("\(route.date?.formatted() ?? "Unknown Time")") + .padding(.bottom) + .font(.callout) + .foregroundColor(.gray) + + if route.notes?.count ?? 0 > 0 { + Text("\(route.notes ?? "")") + .padding(.bottom) + .font(.callout) + .foregroundColor(.gray) + } + } + } icon: { + ZStack { + Circle() + .fill(routeColor) + .frame(width: 40, height: 40) + .padding(.top) + if route.enabled { + Image(systemName: "checkmark.circle.fill") + .padding(.top) + .foregroundColor(routeColor.isLight() ? .black : .white) + } + } + } + .swipeActions { + Button(role: .destructive) { + context.delete(route) + do { + try context.save() + } catch let error as NSError { + print("Error: \(error.localizedDescription)") + } + } label: { + Label("delete", systemImage: "trash") + } + } + } + .listStyle(.plain) + } + .navigationTitle("Route List") +// } detail: { + + VStack { + if selectedRoute != nil { + let locationArray = selectedRoute?.locations?.array as? [LocationEntity] ?? [] + let lineCoords = locationArray.compactMap({(location) -> CLLocationCoordinate2D in + return location.locationCoordinate ?? LocationHelper.DefaultLocation + }) + + Map() { + Annotation("Start", coordinate: lineCoords.first ?? LocationHelper.DefaultLocation) { + ZStack { + Circle() + .fill(Color(.green)) + .strokeBorder(.white, lineWidth: 3) + .frame(width: 15, height: 15) + } + } + .annotationTitles(.automatic) + Annotation("Finish", coordinate: lineCoords.last ?? LocationHelper.DefaultLocation) { + ZStack { + Circle() + .fill(Color(.black)) + .strokeBorder(.white, lineWidth: 3) + .frame(width: 15, height: 15) + } + } + .annotationTitles(.automatic) + let dashed = StrokeStyle( + lineWidth: 3, + lineCap: .round, lineJoin: .round, dash: [7, 10] + ) + MapPolyline(coordinates: lineCoords) + .stroke(Color(UIColor(hex: UInt32(selectedRoute?.color ?? 0))), style: dashed) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + }.navigationTitle(" \(selectedRoute?.name ?? "Unknown Route") \(selectedRoute?.locations?.count ?? 0) points") + } + } +} diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index a4972a90..7fb37da8 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -13,10 +13,11 @@ struct Settings: View { @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "user.longName", ascending: true)], animation: .default) private var nodes: FetchedResults @State private var selectedNode: Int = 0 - @State private var connectedNodeNum: Int = 0 + @State private var preferredNodeNum: Int = 0 @State private var selection: SettingsSidebar = .about enum SettingsSidebar { case appSettings + case routes case shareChannels case userConfig case loraConfig @@ -57,7 +58,18 @@ struct Settings: View { Text("app.settings") } .tag(SettingsSidebar.appSettings) - let node = nodes.first(where: { $0.num == connectedNodeNum }) + if #available(iOS 17.0, macOS 14.0, *) { + NavigationLink { + Routes() + } label: { + Image(systemName: "road.lanes.curved.right") + .symbolRenderingMode(.hierarchical) + Text("routes") + } + .tag(SettingsSidebar.routes) + } + + let node = nodes.first(where: { $0.num == preferredNodeNum }) let hasAdmin = node?.myInfo?.adminIndex ?? 0 > 0 ? true : false if !(node?.deviceConfig?.isManaged ?? false) { Section("Configure") { @@ -84,8 +96,8 @@ struct Settings: View { .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) + let connectedNode = nodes.first(where: { $0.num == preferredNodeNum }) + preferredNodeNum = Int(connectedNode?.num ?? 0)// Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil && node?.metadata == nil { let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) if adminMessageId > 0 { @@ -100,14 +112,14 @@ struct Settings: View { } Section("radio.configuration") { NavigationLink { - ShareChannels(node: nodes.first(where: { $0.num == connectedNodeNum })) + ShareChannels(node: nodes.first(where: { $0.num == preferredNodeNum })) } label: { Image(systemName: "qrcode") .symbolRenderingMode(.hierarchical) Text("share.channels") } .tag(SettingsSidebar.shareChannels) - .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) + .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) NavigationLink { UserConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { @@ -125,14 +137,14 @@ struct Settings: View { } .tag(SettingsSidebar.loraConfig) NavigationLink { - Channels(node: nodes.first(where: { $0.num == connectedNodeNum })) + Channels(node: nodes.first(where: { $0.num == preferredNodeNum })) } label: { Image(systemName: "fibrechannel") .symbolRenderingMode(.hierarchical) Text("channels") } .tag(SettingsSidebar.channelConfig) - .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) + .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) NavigationLink { BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { @@ -257,7 +269,7 @@ struct Settings: View { } .tag(SettingsSidebar.meshLog) NavigationLink { - let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) + let connectedNode = nodes.first(where: { $0.num == preferredNodeNum }) AdminMessageList(user: connectedNode?.user) } label: { Image(systemName: "building.columns") @@ -266,24 +278,24 @@ struct Settings: View { } .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) - } +// Section(header: Text("Firmware")) { +// NavigationLink { +// Firmware(node: nodes.first(where: { $0.num == preferredNodeNum })) +// } label: { +// Image(systemName: "arrow.up.arrow.down.square") +// .symbolRenderingMode(.hierarchical) +// Text("Firmware Updates") +// } +// .tag(SettingsSidebar.about) +// .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) +// } } } .onAppear { if self.bleManager.context == nil { self.bleManager.context = context } - self.connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) + self.preferredNodeNum = UserDefaults.preferredPeripheralNum// Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) selectedNode = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) } .listStyle(GroupedListStyle()) diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index e58c38b6..60d9ff6b 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -225,6 +225,7 @@ "received.ack.real"="Recipient Ack"; "ringtone"="Ringtone"; "ringtone.config"="Ringtone Config"; +"routes"="Routes"; "routing.acknowledged"="Bestätigt"; "routing.noroute"="Keine Route"; "routing.gotnak"="Negative Empfangsbestätigung empfangen"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index ae374695..23c19d6a 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -228,6 +228,7 @@ "received.ack.real"="Recipient Ack"; "ringtone"="Ringtone"; "ringtone.config"="Ringtone Config"; +"routes"="Routes"; "routing.acknowledged"="Acknowledged"; "routing.noroute"="No Route"; "routing.gotnak"="Received a negative acknowledgment"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index ad7bc540..489db06a 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -226,6 +226,7 @@ "received.ack.real"="Odbiorca potwierdzenia"; "ringtone"="Dzwonek"; "ringtone.config"="Konfiguracja dzwonka"; +"routes"="Routes"; "routing.acknowledged"="Potwierdzono"; "routing.noroute"="Brak trasy"; "routing.gotnak"="Otrzymano negatywne potwierdzenie"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index d1b7dcc8..cd4a522b 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -225,6 +225,7 @@ "received.ack.real"="收件人确认"; "ringtone"="铃声"; "ringtone.config"="铃声设置"; +"routes"="Routes"; "routing.acknowledged"="确认"; "routing.noroute"="找不到目标"; "routing.gotnak"="收到否认";