From 41fc4574ba1a4080c6cd863651e134c9e42830ea Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 17 Sep 2023 13:47:38 -0700 Subject: [PATCH 01/11] Bump version --- Meshtastic/Views/Nodes/Helpers/PositionPopover.swift | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Meshtastic/Views/Nodes/Helpers/PositionPopover.swift diff --git a/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift new file mode 100644 index 00000000..68ec2b29 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift @@ -0,0 +1,8 @@ +// +// PositionPopover.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 9/17/23. +// + +import Foundation From 3a5f192ac10664533bd74a03e51ee63f1b26c2e2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 17 Sep 2023 19:42:03 -0700 Subject: [PATCH 02/11] Map annotation popover and convex hull! --- Meshtastic.xcodeproj/project.pbxproj | 12 +- Meshtastic/Extensions/UserDefaults.swift | 9 ++ Meshtastic/Helpers/BLEManager.swift | 12 +- Meshtastic/Tips/BluetoothTips.swift | 2 +- Meshtastic/Tips/ChannelTips.swift | 2 +- Meshtastic/Tips/MessagesTips.swift | 21 ++- Meshtastic/Views/Messages/UserList.swift | 78 +++++------ .../Views/Nodes/Helpers/NodeListItem.swift | 13 +- .../Views/Nodes/Helpers/NodeMapSwiftUI.swift | 121 +++++++++++++++--- .../Views/Nodes/Helpers/PositionPopover.swift | 113 +++++++++++++++- Meshtastic/Views/Nodes/NodeList.swift | 36 +++++- Meshtastic/Views/Settings/ShareChannels.swift | 12 +- en.lproj/Localizable.strings | 2 +- 13 files changed, 344 insertions(+), 89 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 181d0bd8..27c5a4f9 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */; }; DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = DD0D3D212A55CEB10066DB71 /* CocoaMQTT */; }; DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */; }; + DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */; }; DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */; }; DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B828CDA93900720036 /* SerialConfigEnums.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; @@ -218,6 +219,7 @@ DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = ""; }; DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = ""; }; DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminMessageList.swift; sourceTree = ""; }; + DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionPopover.swift; sourceTree = ""; }; DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV15.xcdatamodel; sourceTree = ""; }; DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfigEnums.swift; sourceTree = ""; }; DD1925B828CDA93900720036 /* SerialConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfigEnums.swift; sourceTree = ""; }; @@ -819,6 +821,7 @@ DDDB26412AABF655003AFCB7 /* NodeListItem.swift */, DDDB26472AACD6D1003AFCB7 /* NodeMapControl.swift */, DDB6CCFA2AAF805100945AF6 /* NodeMapSwiftUI.swift */, + DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */, ); path = Helpers; sourceTree = ""; @@ -1106,6 +1109,7 @@ DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, DD5E5209298EE33B00D21B61 /* module_config.pb.swift in Sources */, DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, + DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */, 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */, DDDB444229F8A88700EE2349 /* Double.swift in Sources */, DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */, @@ -1410,7 +1414,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.6; + MARKETING_VERSION = 2.2.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1444,7 +1448,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.6; + MARKETING_VERSION = 2.2.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1566,7 +1570,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.6; + MARKETING_VERSION = 2.2.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1599,7 +1603,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.6; + MARKETING_VERSION = 2.2.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index d453094e..48305399 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -18,6 +18,7 @@ extension UserDefaults { case meshMapRecentering case meshMapShowNodeHistory case meshMapShowRouteLines + case enableMapConvexHull case enableMapTraffic case enableMapPointsOfInterest case enableOfflineMaps @@ -98,6 +99,14 @@ extension UserDefaults { UserDefaults.standard.set(newValue, forKey: "meshMapShowRouteLines") } } + static var enableMapConvexHull: Bool { + get { + UserDefaults.standard.bool(forKey: "enableMapConvexHull") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableMapConvexHull") + } + } static var enableMapTraffic: Bool { get { UserDefaults.standard.bool(forKey: "enableMapTraffic") diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index ab010d52..1547fe77 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -69,7 +69,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Scan for nearby BLE devices using the Meshtastic BLE service ID func startScanning() { if isSwitchedOn { - centralManager.scanForPeripherals(withServices: [meshtasticServiceCBUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]) + centralManager.scanForPeripherals(withServices: [meshtasticServiceCBUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) print("✅ Scanning Started") } } @@ -486,18 +486,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } // Config if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil { - nowKnown = true localConfig(config: decodedInfo.config, context: context!, nodeNum: self.connectedPeripheral.num, nodeLongName: self.connectedPeripheral.longName) } // Module Config if decodedInfo.moduleConfig.isInitialized && !invalidVersion { - nowKnown = true moduleConfig(config: decodedInfo.moduleConfig, context: context!, nodeNum: self.connectedPeripheral.num, nodeLongName: self.connectedPeripheral.longName) - if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { - if decodedInfo.moduleConfig.cannedMessage.enabled { _ = self.getCannedMessageModuleMessages(destNum: self.connectedPeripheral.num, wantResponse: true) } @@ -508,9 +504,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate nowKnown = true deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: connectedPeripheral.num, context: context!) connectedPeripheral.firmwareVersion = decodedInfo.metadata.firmwareVersion - let lastDotIndex = decodedInfo.metadata.firmwareVersion.lastIndex(of: ".") - if lastDotIndex == nil { invalidVersion = true connectedVersion = "0.0.0" @@ -520,19 +514,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate connectedVersion = String(version.dropLast()) appState.firmwareVersion = connectedVersion } - let supportedVersion = connectedVersion == "0.0.0" || self.minimumVersion.compare(connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(connectedVersion, options: .numeric) == .orderedSame - if !supportedVersion { invalidVersion = true lastConnectionError = "🚨" + "update.firmware".localized return - } } // Log any other unknownApp calls if !nowKnown { MeshLogger.log("🕸️ MESH PACKET received for Unknown App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") } - case .textMessageApp, .detectionSensorApp: textMessageAppPacket(packet: decodedInfo.packet, blockRangeTest: UserDefaults.blockRangeTest, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) case .remoteHardwareApp: diff --git a/Meshtastic/Tips/BluetoothTips.swift b/Meshtastic/Tips/BluetoothTips.swift index 36fe7583..f7540b66 100644 --- a/Meshtastic/Tips/BluetoothTips.swift +++ b/Meshtastic/Tips/BluetoothTips.swift @@ -22,6 +22,6 @@ struct BluetoothConnectionTip: Tip { Text("tip.bluetooth.connect.message") } var image: Image? { - Image(systemName: "questionmark.circle") + Image(systemName: "flipphone") } } diff --git a/Meshtastic/Tips/ChannelTips.swift b/Meshtastic/Tips/ChannelTips.swift index b98e4032..cdd5d9c7 100644 --- a/Meshtastic/Tips/ChannelTips.swift +++ b/Meshtastic/Tips/ChannelTips.swift @@ -22,6 +22,6 @@ Text("tip.channels.share.message") } var image: Image? { - Image(systemName: "questionmark.circle") + Image(systemName: "qrcode") } } diff --git a/Meshtastic/Tips/MessagesTips.swift b/Meshtastic/Tips/MessagesTips.swift index ce655583..b2bdf295 100644 --- a/Meshtastic/Tips/MessagesTips.swift +++ b/Meshtastic/Tips/MessagesTips.swift @@ -22,6 +22,25 @@ struct MessagesTip: Tip { Text("tip.messages.message") } var image: Image? { - Image(systemName: "questionmark.circle") + Image(systemName: "bubble.left.and.bubble.right") + } +} + +@available(iOS 17.0, macOS 14.0, *) +struct ContactsTip: Tip { + + var id: String { + return "tip.messages.contacts" + } + var title: Text { + //Text("tip.messages.contacts.title") + Text("Contacts") + } + var message: Text? { + //Text("tip.messages.contacts.message") + Text("Each node shows as an available contact. Nodes with recent messages and favorites show up at the top of the list. Select a node to send or view messages. Long press to favorite or mute the node, send a trace route or delete the conversation.") + } + var image: Image? { + Image(systemName: "person.circle") } } diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 4cf0f135..9fa4e2b8 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -7,6 +7,9 @@ import SwiftUI import CoreData +#if canImport(TipKit) +import TipKit +#endif struct UserList: View { @@ -37,6 +40,9 @@ struct UserList: View { let dateFormatString = (localeDateFormat ?? "MM/dd/YY") VStack { List { + if #available(iOS 17.0, macOS 14.0, *) { + TipView(ContactsTip(), arrowEdge: .bottom) + } ForEach(users) { (user: UserEntity) in let mostRecent = user.messageList.last let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) @@ -96,47 +102,47 @@ struct UserList: View { } } } - .frame(height: 62) - .contextMenu { - Button { - user.vip = !user.vip - do { - try context.save() - } catch { - context.rollback() - print("💥 Save User VIP Error") - } - } label: { - Label(user.vip ? "Un-Favorite" : "Favorite", systemImage: user.vip ? "star.slash.fill" : "star.fill") + .frame(height: 62) + .contextMenu { + Button { + user.vip = !user.vip + do { + try context.save() + } catch { + context.rollback() + print("💥 Save User VIP Error") } - Button { - user.mute = !user.mute - do { - try context.save() - } catch { - context.rollback() - print("💥 Save User Mute Error") - } - } label: { - Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") + } label: { + Label(user.vip ? "Un-Favorite" : "Favorite", systemImage: user.vip ? "star.slash.fill" : "star.fill") + } + Button { + user.mute = !user.mute + do { + try context.save() + } catch { + context.rollback() + print("💥 Save User Mute Error") } - Button { - let success = bleManager.sendTraceRouteRequest(destNum: user.num, wantResponse: true) - if success { - isPresentingTraceRouteSentAlert = true - } - } label: { - Label("Trace Route", systemImage: "signpost.right.and.left") + } label: { + Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") + } + Button { + let success = bleManager.sendTraceRouteRequest(destNum: user.num, wantResponse: true) + if success { + isPresentingTraceRouteSentAlert = true } - if user.messageList.count > 0 { - Button(role: .destructive) { - isPresentingDeleteUserMessagesConfirm = true - userSelection = user - } label: { - Label("Delete Messages", systemImage: "trash") - } + } label: { + Label("Trace Route", systemImage: "signpost.right.and.left") + } + if user.messageList.count > 0 { + Button(role: .destructive) { + isPresentingDeleteUserMessagesConfirm = true + userSelection = user + } label: { + Label("Delete Messages", systemImage: "trash") } } + } .alert( "Trace Route Sent", isPresented: $isPresentingTraceRouteSentAlert diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index f2397f26..7f4afcdb 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -30,9 +30,16 @@ struct NodeListItem: View { } } VStack(alignment: .leading) { - Text(node.user?.longName ?? "unknown".localized) - .fontWeight(.medium) - .font(.callout) + HStack { + Text(node.user?.longName ?? "unknown".localized) + .fontWeight(.medium) + .font(.callout) + if node.user?.vip ?? false { + Spacer() + Image(systemName: "star.fill") + .foregroundColor(.secondary) + } + } if connected { HStack { Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift index 26c45a0a..0545e686 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift @@ -11,6 +11,7 @@ import MapKit import WeatherKit @available(iOS 17.0, macOS 14.0, *) + struct NodeMapSwiftUI: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -27,6 +28,8 @@ struct NodeMapSwiftUI: View { @State private var isLookingAround = false @State private var isEditingSettings = false @State private var showUserLocation: Bool = false + + @State private var showConvexHull = true @State var selected: PositionEntity? /// Data @ObservedObject var node: NodeInfoEntity @@ -36,6 +39,8 @@ struct NodeMapSwiftUI: View { ), animation: .none) private var waypoints: FetchedResults + @State private var showingPopover = false + var body: some View { let nodeColor = UIColor(hex: UInt32(node.num)) let positionArray = node.positions?.array as? [PositionEntity] ?? [] @@ -48,17 +53,26 @@ struct NodeMapSwiftUI: View { ZStack { Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { /// Route Lines - if showRouteLines { - let gradient = LinearGradient( - colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)], - startPoint: .leading, endPoint: .trailing - ) - let stroke = StrokeStyle( - lineWidth: 5, - lineCap: .round, lineJoin: .round, dash: [10, 10] - ) - MapPolyline(coordinates: lineCoords) - .stroke(gradient, style: stroke) + 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: 5, + lineCap: .round, lineJoin: .round, dash: [10, 10] + ) + MapPolyline(coordinates: lineCoords) + .stroke(gradient, style: dashed) + } + } + /// Convex Hull + if showConvexHull { + let hull = getConvexHull(input: lineCoords) + MapPolygon(coordinates: hull) + .stroke(Color(nodeColor.darker()), lineWidth: 5) + .foregroundStyle(Color(nodeColor).opacity(0.4)) } /// Node Annotations ForEach(positionArray.reversed(), id: \.id) { position in @@ -79,10 +93,16 @@ struct NodeMapSwiftUI: View { .background(Color(UIColor(hex: UInt32(node.num)).darker())) .clipShape(Circle()) .rotationEffect(.degrees(Double(position.heading))) -// .onTapGesture { -// selected = (selected == position ? nil : position) // <-- here -// print("tapity tap tap \(position.time)") -// } + .onTapGesture { + showingPopover = true + selected = (selected == position ? nil : position) // <-- here + } + .popover(isPresented: $showingPopover, arrowEdge: .bottom) { + PositionPopover(position: position) + .padding() + .opacity(0.8) + .presentationCompactAdaptation(.popover) + } } else { Image(systemName: "flipphone") .symbolEffect(.pulse.byLayer) @@ -90,10 +110,16 @@ struct NodeMapSwiftUI: View { .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) .background(Color(UIColor(hex: UInt32(node.num)).darker())) .clipShape(Circle()) -// .onTapGesture { -// selected = (selected == position ? nil : position) // <-- here -// print("tapity tap tap \(position.time)") -// } + .onTapGesture { + showingPopover = true + selected = (selected == position ? nil : position) // <-- here + } + .popover(isPresented: $showingPopover, arrowEdge: .bottom) { + PositionPopover(position: position) + .padding() + .opacity(0.8) + .presentationCompactAdaptation(.popover) + } } } else { if showNodeHistory { @@ -186,6 +212,14 @@ struct NodeMapSwiftUI: View { self.showRouteLines.toggle() UserDefaults.enableMapRouteLines = self.showRouteLines } + Toggle(isOn: $showConvexHull) { + Label("Convex Hull", systemImage: "button.angledbottom.horizontal.right") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.showConvexHull.toggle() + UserDefaults.enableMapConvexHull = self.showConvexHull + } Toggle(isOn: $showTraffic) { Label("Traffic", systemImage: "car") } @@ -216,13 +250,13 @@ struct NodeMapSwiftUI: View { .padding() #endif } - //.presentationDetents([.fraction(0.4)]) + //.presentationDetents([.fraction(0.5)]) .presentationDetents([.medium]) .presentationDragIndicator(.visible) } .onChange(of: node) { let mostRecent = node.positions?.lastObject as? PositionEntity - position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 1500, heading: 0, pitch: 0)) + position = MapCameraPosition.automatic//.camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 1500, heading: 0, pitch: 0)) if let mostRecent { Task { scene = try? await fetchScene(for: mostRecent.coordinate) @@ -306,4 +340,49 @@ struct NodeMapSwiftUI: View { let lookAroundScene = MKLookAroundSceneRequest(coordinate: coordinate) return try await lookAroundScene.scene } + + func getConvexHull(input: [CLLocationCoordinate2D]) -> [CLLocationCoordinate2D] { + + // X = longitude + // Y = latitudeß + + // 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product. + // Returns a positive value, if OAB makes a counter-clockwise turn, + // negative for clockwise turn, and zero if the points are collinear. + func cross(P: CLLocationCoordinate2D, A: CLLocationCoordinate2D, B: CLLocationCoordinate2D) -> Double { + let part1 = (A.longitude - P.longitude) * (B.latitude - P.latitude) + let part2 = (A.latitude - P.latitude) * (B.longitude - P.longitude) + return part1 - part2; + } + + // Sort points lexicographically + let points = input.sorted() { + $0.longitude == $1.longitude ? $0.latitude < $1.latitude : $0.longitude < $1.longitude + } + + // Build the lower hull + var lower: [CLLocationCoordinate2D] = [] + for p in points { + while lower.count >= 2 && cross(P: lower[lower.count - 2], A: lower[lower.count - 1], B: p) <= 0 { + lower.removeLast() + } + lower.append(p) + } + + // Build upper hull + var upper: [CLLocationCoordinate2D] = [] + for p in points.reversed() { + while upper.count >= 2 && cross(P: upper[upper.count-2], A: upper[upper.count-1], B: p) <= 0 { + upper.removeLast() + } + upper.append(p) + } + + // Last point of upper list is omitted because it is repeated at the + // beginning of the lower list. + upper.removeLast() + + // Concatenation of the lower and upper hulls gives the convex hull. + return (upper + lower) + } } diff --git a/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift index 68ec2b29..58928cde 100644 --- a/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift @@ -2,7 +2,116 @@ // PositionPopover.swift // Meshtastic // -// Created by Garth Vander Houwen on 9/17/23. +// Copyright(c) Garth Vander Houwen 9/17/23. // -import Foundation +import SwiftUI +import MapKit + +struct PositionPopover: View { + var position: PositionEntity + let distanceFormatter = MKDistanceFormatter() + var body: some View { + VStack { + HStack { + CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(position.nodePosition?.user?.num ?? 0)))) + Text(position.nodePosition?.user?.longName ?? "Unknown") + .font(.title3) + } + Divider() + VStack (alignment: .leading) { + /// Time + Label { + Text(position.time?.formatted() ?? "Unknown") + .foregroundColor(.primary) + } icon: { + Image(systemName: "clock.badge.checkmark") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + /// Coordinate + Label { + Text("\(String(format: "%.6f", position.coordinate.latitude)), \(String(format: "%.6f", position.coordinate.longitude))") + .font(.footnote) + .textSelection(.enabled) + .foregroundColor(.primary) + } icon: { + Image(systemName: "mappin.and.ellipse") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + /// Altitude + Label { + Text("Altitude: \(distanceFormatter.string(fromDistance: Double(position.altitude)))") + // .font(.footnote) + .foregroundColor(.primary) + } icon: { + Image(systemName: "mountain.2.fill") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 3)) + /// Sats in view + if pf.contains(.Satsinview) { + Label { + Text("Sats in view: \(String(position.satsInView))") + // .font(.footnote) + .foregroundColor(.primary) + } icon: { + Image(systemName: "sparkles") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + } + /// Sequence Number + if pf.contains(.SeqNo) { + Label { + Text("Sequence: \(String(position.seqNo))") + // .font(.footnote) + .foregroundColor(.primary) + } icon: { + Image(systemName: "number") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + } + /// Heading +// if pf.contains(.Heading) { +// Text("Heading: \(Int32(position.heading))") +// } + /// Speed + if pf.contains(.Speed) { + let formatter = MeasurementFormatter() + Label { + Text("Speed: \(formatter.string(from: Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour)))") + // .font(.footnote) + .foregroundColor(.primary) + } icon: { + Image(systemName: "gauge.with.dots.needle.33percent") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + } + /// Distance + if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 { + let metersAway = position.coordinate.distance(from: LocationHelper.currentLocation) + Label { + Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") + // .font(.footnote) + .foregroundColor(.primary) + } icon: { + Image(systemName: "lines.measurement.horizontal") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + } + } + } + } +} diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 7a36fe44..7e6f30ec 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -31,7 +31,7 @@ struct NodeList: View { sortDescriptors: [NSSortDescriptor(key: "user.vip", ascending: false), NSSortDescriptor(key: "lastHeard", ascending: false)], animation: .default) - private var nodes: FetchedResults + var nodes: FetchedResults @@ -42,7 +42,39 @@ struct NodeList: View { let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) List(nodes, id: \.self, selection: $selectedNode) { node in - NodeListItem(node: node, connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num, connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1), modemPreset: Int(connectedNode?.loRaConfig?.modemPreset ?? 0)) + NodeListItem(node: node, + connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num, + connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1), + modemPreset: Int(connectedNode?.loRaConfig?.modemPreset ?? 0)) + .contextMenu { + if node.user != nil { + Button { + node.user!.vip = !node.user!.vip + context.refresh(node, mergeChanges: true) + do { + try context.save() + } catch { + context.rollback() + print("💥 Save User VIP Error") + } + } label: { + Label(node.user?.vip ?? false ? "Un-Favorite" : "Favorite", systemImage: node.user?.vip ?? false ? "star.slash.fill" : "star.fill") + } + Button { + node.user!.mute = !node.user!.mute + context.refresh(node, mergeChanges: true) + do { + try context.save() + } catch { + context.rollback() + print("💥 Save User Mute Error") + } + } label: { + Label(node.user!.mute ? "Show Alerts" : "Hide Alerts", systemImage: node.user!.mute ? "bell" : "bell.slash") + } + } + + } } .searchable(text: nodesQuery, prompt: "Find a node") .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index ebc6e98e..e1b45f14 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -51,16 +51,16 @@ struct ShareChannels: View { var qrCodeImage = QrCodeImage() var body: some View { + + if #available(iOS 17.0, macOS 14.0, *) { + VStack { + TipView(ShareChannelsTip(), arrowEdge: .bottom) + } + } GeometryReader { bounds in let smallest = min(bounds.size.width, bounds.size.height) ScrollView { if node != nil && node?.myInfo != nil { - - if #available(iOS 17.0, macOS 14.0, *) { - VStack { - TipView(ShareChannelsTip(), arrowEdge: .top) - } - } Grid { GridRow { Spacer() diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index ffc87d28..dda1dade 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -276,7 +276,7 @@ "telemetry.config"="Telemetry Config"; "timeout"="Timeout"; "timestamp"="Timestamp"; -"tip.bluetooth.connect.title"="Connected LoRa Radio"; +"tip.bluetooth.connect.title"="Connected Radio"; "tip.bluetooth.connect.message"="Shows information for the Lora radio currently connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; "tip.channels.share.title"="Sharing Meshtastic Channels"; "tip.channels.share.message"="In a Meshtastic LoRa Mesh there are up to 8 channels. The first one is the Primary channel where most activity happens and is required. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. It talks on its primary and your secondary channel. A channel with the name 'admin' controls nodes remotely. Other channels are for private groups, each with its own key."; From 13451323bdc4f8bb378f1e48d7ecfe439b588d07 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 18 Sep 2023 16:53:02 -0700 Subject: [PATCH 03/11] Clean up latest pin details popover --- Meshtastic/Views/Nodes/Helpers/NodeDetail.swift | 2 +- .../Views/Nodes/Helpers/NodeMapSwiftUI.swift | 11 +++++------ .../Views/Nodes/Helpers/PositionPopover.swift | 17 +++++++++++++---- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 4c889031..f60337cb 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -42,7 +42,7 @@ struct NodeDetail: View { Divider() NavigationLink { if #available (iOS 17, macOS 14, *) { - NodeMapSwiftUI(node: node) + NodeMapSwiftUI(node: node, showUserLocation: connectedNode?.num ?? 0 == node.num) } else { NodeMapControl(node: node) } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift index 0545e686..43f991f1 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift @@ -15,6 +15,9 @@ import WeatherKit struct NodeMapSwiftUI: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager + /// Parameters + @ObservedObject var node: NodeInfoEntity + @State var showUserLocation: Bool = false /// Map State @Namespace var mapScope @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @@ -27,20 +30,16 @@ struct NodeMapSwiftUI: View { @State private var scene: MKLookAroundScene? @State private var isLookingAround = false @State private var isEditingSettings = false - @State private var showUserLocation: Bool = false - @State private var showConvexHull = true - @State var selected: PositionEntity? + @State private var selected: PositionEntity? + @State private var showingPopover = false /// Data - @ObservedObject var node: NodeInfoEntity @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], predicate: NSPredicate( format: "expire == nil || expire >= %@", Date() as NSDate ), animation: .none) private var waypoints: FetchedResults - @State private var showingPopover = false - var body: some View { let nodeColor = UIColor(hex: UInt32(node.num)) let positionArray = node.positions?.array as? [PositionEntity] ?? [] diff --git a/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift index 58928cde..f1070518 100644 --- a/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift @@ -17,6 +17,7 @@ struct PositionPopover: View { CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(position.nodePosition?.user?.num ?? 0)))) Text(position.nodePosition?.user?.longName ?? "Unknown") .font(.title3) + let degrees = Angle.degrees(Double(position.heading)) } Divider() VStack (alignment: .leading) { @@ -45,7 +46,6 @@ struct PositionPopover: View { /// Altitude Label { Text("Altitude: \(distanceFormatter.string(fromDistance: Double(position.altitude)))") - // .font(.footnote) .foregroundColor(.primary) } icon: { Image(systemName: "mountain.2.fill") @@ -58,7 +58,6 @@ struct PositionPopover: View { if pf.contains(.Satsinview) { Label { Text("Sats in view: \(String(position.satsInView))") - // .font(.footnote) .foregroundColor(.primary) } icon: { Image(systemName: "sparkles") @@ -71,7 +70,6 @@ struct PositionPopover: View { if pf.contains(.SeqNo) { Label { Text("Sequence: \(String(position.seqNo))") - // .font(.footnote) .foregroundColor(.primary) } icon: { Image(systemName: "number") @@ -82,7 +80,18 @@ struct PositionPopover: View { } /// Heading // if pf.contains(.Heading) { -// Text("Heading: \(Int32(position.heading))") +// let degrees = Angle.degrees(Double(position.heading)) +// Label { +// let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees) +// Text("Heading: \(heading.formatted())") +// .foregroundColor(.primary) +// } icon: { +// Image(systemName: "location.north") +// .symbolRenderingMode(.hierarchical) +// .frame(width: 35) +// .rotationEffect(degrees) +// } +// .padding(.bottom, 5) // } /// Speed if pf.contains(.Speed) { From 0b11f8ed7d18b434ccbf9ba3b6d7a2e436376988 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 19 Sep 2023 17:06:47 -0700 Subject: [PATCH 04/11] Assorted cleanup --- Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/Enums/AppSettingsEnums.swift | 3 - .../Extensions/CLLocationCoordinate2D.swift | 42 +++++++++ Meshtastic/Helpers/BLEManager.swift | 32 +++---- Meshtastic/Helpers/LocationHelper.swift | 10 +++ .../Views/Messages/ChannelMessageList.swift | 4 +- .../Views/Messages/UserMessageList.swift | 4 +- .../Views/Nodes/Helpers/NodeMapSwiftUI.swift | 86 +++++-------------- .../Views/Nodes/Helpers/PositionPopover.swift | 28 +++--- Meshtastic/Views/Nodes/PositionLog.swift | 3 +- .../Settings/Config/PositionConfig.swift | 2 +- 11 files changed, 115 insertions(+), 103 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 27c5a4f9..e7d6a2eb 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -166,6 +166,7 @@ DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; DDDE5A1429AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */; }; + DDE179302ABA2482005777A8 /* LocationDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1792F2ABA2482005777A8 /* LocationDataManager.swift */; }; DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */; }; DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; }; @@ -389,6 +390,7 @@ DDDE5A1229AFEAB900490C6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = ""; }; DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = ""; }; + DDE1792F2ABA2482005777A8 /* LocationDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataManager.swift; sourceTree = ""; }; DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV17.xcdatamodel; sourceTree = ""; }; DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreForward.swift; sourceTree = ""; }; @@ -791,6 +793,7 @@ DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */, DDDB443C29F6592F00EE2349 /* NetworkManager.swift */, + DDE1792F2ABA2482005777A8 /* LocationDataManager.swift */, ); path = Helpers; sourceTree = ""; @@ -1180,6 +1183,7 @@ DD5E520E298EE33B00D21B61 /* mqtt.pb.swift in Sources */, DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */, DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */, + DDE179302ABA2482005777A8 /* LocationDataManager.swift in Sources */, DDDB443D29F6592F00EE2349 /* NetworkManager.swift in Sources */, DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */, DD73FD1128750779000852D6 /* PositionLog.swift in Sources */, 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/Extensions/CLLocationCoordinate2D.swift b/Meshtastic/Extensions/CLLocationCoordinate2D.swift index 32a47774..58287add 100644 --- a/Meshtastic/Extensions/CLLocationCoordinate2D.swift +++ b/Meshtastic/Extensions/CLLocationCoordinate2D.swift @@ -18,3 +18,45 @@ extension CLLocationCoordinate2D { return from.distance(from: to) } } + +extension [CLLocationCoordinate2D] { + /// Get Convex Hull For an array of CLLocationCoordinate2D positions + /// - Returns: A smaller CLLocationCoordinate2D array containing only the points necessary to create a convex hull polygon + func getConvexHull() -> [CLLocationCoordinate2D] { + /// X = longitude + /// Y = latitude + /// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product. + /// Returns a positive value, if OAB makes a counter-clockwise turn, + /// negative for clockwise turn, and zero if the points are collinear. + func cross(P: CLLocationCoordinate2D, A: CLLocationCoordinate2D, B: CLLocationCoordinate2D) -> Double { + let part1 = (A.longitude - P.longitude) * (B.latitude - P.latitude) + let part2 = (A.latitude - P.latitude) * (B.longitude - P.longitude) + return part1 - part2; + } + // Sort points lexicographically + let points = self.sorted() { + $0.longitude == $1.longitude ? $0.latitude < $1.latitude : $0.longitude < $1.longitude + } + // Build the lower hull + var lower: [CLLocationCoordinate2D] = [] + for p in points { + while lower.count >= 2 && cross(P: lower[lower.count - 2], A: lower[lower.count - 1], B: p) <= 0 { + lower.removeLast() + } + lower.append(p) + } + // Build upper hull + var upper: [CLLocationCoordinate2D] = [] + for p in points.reversed() { + while upper.count >= 2 && cross(P: upper[upper.count-2], A: upper[upper.count-1], B: p) <= 0 { + upper.removeLast() + } + upper.append(p) + } + // Last point of upper list is omitted because it is repeated at the + // beginning of the lower list. + upper.removeLast() + // Concatenation of the lower and upper hulls gives the convex hull. + return (upper + lower) + } +} diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 1547fe77..0251a62a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -42,6 +42,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var lastPosition: CLLocationCoordinate2D? let emptyNodeNum: UInt32 = 4294967295 let mqttManager = MqttClientProxyManager.shared + let locationHelper = LocationHelper.shared var wantRangeTestPackets = false /* Meshtastic Service Details */ var TORADIO_characteristic: CBCharacteristic! @@ -834,27 +835,28 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return success } - public func sendPosition(destNum: Int64, wantResponse: Bool, smartPosition: Bool) -> Bool { + public func sendPosition(destNum: Int64, wantResponse: Bool) -> Bool { var success = false let fromNodeNum = connectedPeripheral.num if fromNodeNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 { return false } - if smartPosition { - if lastPosition != nil { - let connectedNode = getNodeInfo(id: connectedPeripheral?.num ?? 0, context: context!) - if connectedNode?.positionConfig?.smartPositionEnabled ?? false { - if lastPosition!.distance(from: LocationHelper.currentLocation) < Double(connectedNode?.positionConfig?.broadcastSmartMinimumDistance ?? 50) { - return false - } - } - } - } - lastPosition = LocationHelper.currentLocation +// if smartPosition { +// if lastPosition != nil { +// let connectedNode = getNodeInfo(id: connectedPeripheral?.num ?? 0, context: context!) +// if connectedNode?.positionConfig?.smartPositionEnabled ?? false { +// if lastPosition!.distance(from: LocationHelper.currentLocation) < Double(connectedNode?.positionConfig?.broadcastSmartMinimumDistance ?? 50) { +// return false +// } +// } +// } +// } +// lastPosition = LocationHelper.currentLocation +// var locationHelper = LocationHelper() var positionPacket = Position() - positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) - positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) + positionPacket.latitudeI = Int32((locationFetcher.lastKnownLocation?.latitude ?? 0) * 1e7) + positionPacket.longitudeI = Int32((locationFetcher.manager.location?.coordinate.longitude ?? 0) * 1e7) positionPacket.time = UInt32(LocationHelper.currentTimestamp.timeIntervalSince1970) positionPacket.timestamp = UInt32(LocationHelper.currentTimestamp.timeIntervalSince1970) positionPacket.altitude = Int32(LocationHelper.currentAltitude) @@ -892,7 +894,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if connectedPeripheral != nil { // Send a position out to the mesh if "share location with the mesh" is enabled in settings if UserDefaults.provideLocation { - let _ = sendPosition(destNum: connectedPeripheral.num, wantResponse: false, smartPosition: true) + let _ = sendPosition(destNum: connectedPeripheral.num, wantResponse: false) } } } diff --git a/Meshtastic/Helpers/LocationHelper.swift b/Meshtastic/Helpers/LocationHelper.swift index 14e1af70..118623b8 100644 --- a/Meshtastic/Helpers/LocationHelper.swift +++ b/Meshtastic/Helpers/LocationHelper.swift @@ -1,9 +1,12 @@ import Foundation import CoreLocation +import MapKit class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate { static let shared = LocationHelper() var locationManager = CLLocationManager() + + //@Published var region = MKCoordinateRegion() @Published var authorizationStatus: CLAuthorizationStatus? override init() { super.init() @@ -89,6 +92,13 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate { } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { +// locationManager.stopUpdatingLocation() +// locations.last.map { +// region = MKCoordinateRegion( +// center: $0.coordinate, +// span: .init(latitudeDelta: 0.01, longitudeDelta: 0.01) +// ) +// } } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print("Location manager error: \(error.localizedDescription)") diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 57b27edd..f6211355 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -384,7 +384,7 @@ struct ChannelMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false, smartPosition: false) { + if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) { print("Location Sent") } } @@ -401,7 +401,7 @@ struct ChannelMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false, smartPosition: false) { + if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) { print("Location Sent") } } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 12d28783..4b9898ff 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -335,7 +335,7 @@ struct UserMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: user.num, wantResponse: true, smartPosition: false) { + if bleManager.sendPosition(destNum: user.num, wantResponse: true) { print("Location Sent") } } @@ -352,7 +352,7 @@ struct UserMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: user.num, wantResponse: true, smartPosition: false) { + if bleManager.sendPosition(destNum: user.num, wantResponse: true) { print("Location Sent") } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift index 43f991f1..bbef8357 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift @@ -18,30 +18,26 @@ struct NodeMapSwiftUI: View { /// Parameters @ObservedObject var node: NodeInfoEntity @State var showUserLocation: Bool = false - /// Map State - @Namespace var mapScope + @State var positions: [PositionEntity] = [] + @State var waypoints: [WaypointEntity] = [] + /// Map State User Defaults @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @AppStorage("meshMapShowRouteLines") private var showRouteLines = false + @AppStorage("meshMapShowConvexHull") private var showConvexHull = true @AppStorage("enableMapTraffic") private var showTraffic: Bool = true @AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = true @AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid + // Map Configuration + @Namespace var mapScope @State private var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) @State private var position = MapCameraPosition.automatic @State private var scene: MKLookAroundScene? @State private var isLookingAround = false @State private var isEditingSettings = false - @State private var showConvexHull = true @State private var selected: PositionEntity? - @State private var showingPopover = false - /// Data - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], - predicate: NSPredicate( - format: "expire == nil || expire >= %@", Date() as NSDate - ), animation: .none) - private var waypoints: FetchedResults + @State private var showingPositionPopover = false var body: some View { - let nodeColor = UIColor(hex: UInt32(node.num)) let positionArray = node.positions?.array as? [PositionEntity] ?? [] let mostRecent = node.positions?.lastObject as? PositionEntity let lineCoords = positionArray.compactMap({(position) -> CLLocationCoordinate2D in @@ -51,6 +47,8 @@ struct NodeMapSwiftUI: View { if node.hasPositions { ZStack { Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { + /// Node Color from node.num + let nodeColor = UIColor(hex: UInt32(node.num)) /// Route Lines if showRouteLines { if showRouteLines { @@ -68,7 +66,7 @@ struct NodeMapSwiftUI: View { } /// Convex Hull if showConvexHull { - let hull = getConvexHull(input: lineCoords) + let hull = lineCoords.getConvexHull() MapPolygon(coordinates: hull) .stroke(Color(nodeColor.darker()), lineWidth: 5) .foregroundStyle(Color(nodeColor).opacity(0.4)) @@ -78,6 +76,7 @@ struct NodeMapSwiftUI: View { let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 3)) let formatter = MeasurementFormatter() let speedText = formatter.string(from: Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour)) + let headingDegrees = Angle.degrees(Double(position.heading)) Annotation(position.latest ? node.user?.shortName ?? "?" : (pf.contains(.Speed) && position.speed > 2) ? speedText : "", coordinate: position.coordinate) { ZStack { if position.latest { @@ -85,18 +84,18 @@ struct NodeMapSwiftUI: View { .foregroundStyle(Color(nodeColor.lighter()).opacity(0.4)) .frame(width: 60, height: 60) if pf.contains(.Heading) { - Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north.fill" : "location.north") + Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north" : "hexagon") .symbolEffect(.pulse.byLayer) .padding(5) .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) .background(Color(UIColor(hex: UInt32(node.num)).darker())) .clipShape(Circle()) - .rotationEffect(.degrees(Double(position.heading))) + .rotationEffect(headingDegrees) .onTapGesture { - showingPopover = true + showingPositionPopover = true selected = (selected == position ? nil : position) // <-- here } - .popover(isPresented: $showingPopover, arrowEdge: .bottom) { + .popover(isPresented: $showingPositionPopover, arrowEdge: .bottom) { PositionPopover(position: position) .padding() .opacity(0.8) @@ -110,10 +109,10 @@ struct NodeMapSwiftUI: View { .background(Color(UIColor(hex: UInt32(node.num)).darker())) .clipShape(Circle()) .onTapGesture { - showingPopover = true + showingPositionPopover = true selected = (selected == position ? nil : position) // <-- here } - .popover(isPresented: $showingPopover, arrowEdge: .bottom) { + .popover(isPresented: $showingPositionPopover, arrowEdge: .bottom) { PositionPopover(position: position) .padding() .opacity(0.8) @@ -123,12 +122,12 @@ struct NodeMapSwiftUI: View { } else { if showNodeHistory { if pf.contains(.Heading) { - Image(systemName: pf.contains(.Speed) && position.speed > 0 ? "location.north.fill" : "hexagon") + Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north" : "hexagon") .padding(2) .foregroundStyle(Color(UIColor(hex: UInt32(node.num)).lighter()).isLight() ? .black : .white) .background(Color(UIColor(hex: UInt32(node.num)).lighter())) .clipShape(Circle()) - .rotationEffect(.degrees(Double(position.heading))) + .rotationEffect(headingDegrees) } else { Image(systemName: "mappin.circle") .padding(2) @@ -141,6 +140,8 @@ struct NodeMapSwiftUI: View { } } .tag(position.time) + .annotationTitles(.automatic) + .annotationSubtitles(.automatic) } } .mapScope(mapScope) @@ -339,49 +340,4 @@ struct NodeMapSwiftUI: View { let lookAroundScene = MKLookAroundSceneRequest(coordinate: coordinate) return try await lookAroundScene.scene } - - func getConvexHull(input: [CLLocationCoordinate2D]) -> [CLLocationCoordinate2D] { - - // X = longitude - // Y = latitudeß - - // 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product. - // Returns a positive value, if OAB makes a counter-clockwise turn, - // negative for clockwise turn, and zero if the points are collinear. - func cross(P: CLLocationCoordinate2D, A: CLLocationCoordinate2D, B: CLLocationCoordinate2D) -> Double { - let part1 = (A.longitude - P.longitude) * (B.latitude - P.latitude) - let part2 = (A.latitude - P.latitude) * (B.longitude - P.longitude) - return part1 - part2; - } - - // Sort points lexicographically - let points = input.sorted() { - $0.longitude == $1.longitude ? $0.latitude < $1.latitude : $0.longitude < $1.longitude - } - - // Build the lower hull - var lower: [CLLocationCoordinate2D] = [] - for p in points { - while lower.count >= 2 && cross(P: lower[lower.count - 2], A: lower[lower.count - 1], B: p) <= 0 { - lower.removeLast() - } - lower.append(p) - } - - // Build upper hull - var upper: [CLLocationCoordinate2D] = [] - for p in points.reversed() { - while upper.count >= 2 && cross(P: upper[upper.count-2], A: upper[upper.count-1], B: p) <= 0 { - upper.removeLast() - } - upper.append(p) - } - - // Last point of upper list is omitted because it is repeated at the - // beginning of the lower list. - upper.removeLast() - - // Concatenation of the lower and upper hulls gives the convex hull. - return (upper + lower) - } } diff --git a/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift index f1070518..acac8ddf 100644 --- a/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/PositionPopover.swift @@ -79,20 +79,20 @@ struct PositionPopover: View { .padding(.bottom, 5) } /// Heading -// if pf.contains(.Heading) { -// let degrees = Angle.degrees(Double(position.heading)) -// Label { -// let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees) -// Text("Heading: \(heading.formatted())") -// .foregroundColor(.primary) -// } icon: { -// Image(systemName: "location.north") -// .symbolRenderingMode(.hierarchical) -// .frame(width: 35) -// .rotationEffect(degrees) -// } -// .padding(.bottom, 5) -// } + if pf.contains(.Heading) { + let degrees = Angle.degrees(Double(position.heading)) + Label { + let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees) + Text("Heading: \(heading.formatted())") + .foregroundColor(.primary) + } icon: { + Image(systemName: "location.north") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + .rotationEffect(degrees) + } + .padding(.bottom, 5) + } /// Speed if pf.contains(.Speed) { let formatter = MeasurementFormatter() diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 0c8c034a..d8d0e5e0 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -50,7 +50,8 @@ struct PositionLog: View { Text(speed.formatted()) } TableColumn("Heading") { position in - Text("\(position.heading)°") + let heading = Measurement(value: Double(position.heading), unit: UnitAngle.degrees) + Text("\(heading.formatted())") } TableColumn("SNR") { position in Text("\(String(format: "%.2f", position.snr)) dB") diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 2ed55fe2..8ad8397c 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -277,7 +277,7 @@ struct PositionConfig: View { Button(buttonText) { if fixedPosition { - _ = bleManager.sendPosition(destNum: node!.num, wantResponse: true, smartPosition: false) + _ = bleManager.sendPosition(destNum: node!.num, wantResponse: true) } let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) From a0e8caf6fbabbe6b2b6db28e272e9fb923a95233 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 20 Sep 2023 22:26:21 -0700 Subject: [PATCH 05/11] Janky waypoint popovers --- Meshtastic.xcodeproj/project.pbxproj | 8 +- Meshtastic/Helpers/BLEManager.swift | 17 +--- .../Views/Nodes/Helpers/NodeMapSwiftUI.swift | 51 ++++++++-- .../Views/Nodes/Helpers/WaypointPopover.swift | 98 +++++++++++++++++++ 4 files changed, 147 insertions(+), 27 deletions(-) create mode 100644 Meshtastic/Views/Nodes/Helpers/WaypointPopover.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e7d6a2eb..968271ec 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -68,6 +68,7 @@ DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; }; DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; }; DD73FD1128750779000852D6 /* PositionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FD1028750779000852D6 /* PositionLog.swift */; }; + DD760AAE2ABAC706002C022E /* WaypointPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD760AAD2ABAC706002C022E /* WaypointPopover.swift */; }; DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */; }; DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */; }; DD77093D2AA1AFA3007A8BF0 /* ChannelTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */; }; @@ -166,7 +167,6 @@ DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; DDDE5A1429AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */; }; - DDE179302ABA2482005777A8 /* LocationDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1792F2ABA2482005777A8 /* LocationDataManager.swift */; }; DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */; }; DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; }; @@ -277,6 +277,7 @@ DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = ""; }; DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = ""; }; DD73FD1028750779000852D6 /* PositionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionLog.swift; sourceTree = ""; }; + DD760AAD2ABAC706002C022E /* WaypointPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointPopover.swift; sourceTree = ""; }; DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetricsLog.swift; sourceTree = ""; }; DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothTips.swift; sourceTree = ""; }; DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelTips.swift; sourceTree = ""; }; @@ -390,7 +391,6 @@ DDDE5A1229AFEAB900490C6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = ""; }; DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = ""; }; - DDE1792F2ABA2482005777A8 /* LocationDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataManager.swift; sourceTree = ""; }; DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV17.xcdatamodel; sourceTree = ""; }; DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreForward.swift; sourceTree = ""; }; @@ -793,7 +793,6 @@ DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */, DDDB443C29F6592F00EE2349 /* NetworkManager.swift */, - DDE1792F2ABA2482005777A8 /* LocationDataManager.swift */, ); path = Helpers; sourceTree = ""; @@ -825,6 +824,7 @@ DDDB26472AACD6D1003AFCB7 /* NodeMapControl.swift */, DDB6CCFA2AAF805100945AF6 /* NodeMapSwiftUI.swift */, DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */, + DD760AAD2ABAC706002C022E /* WaypointPopover.swift */, ); path = Helpers; sourceTree = ""; @@ -1075,6 +1075,7 @@ DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */, DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */, 6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */, + DD760AAE2ABAC706002C022E /* WaypointPopover.swift in Sources */, DD5E5203298EE33B00D21B61 /* config.pb.swift in Sources */, DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */, DDA6B2EB28420A7B003E8C16 /* NodeAnnotation.swift in Sources */, @@ -1183,7 +1184,6 @@ DD5E520E298EE33B00D21B61 /* mqtt.pb.swift in Sources */, DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */, DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */, - DDE179302ABA2482005777A8 /* LocationDataManager.swift in Sources */, DDDB443D29F6592F00EE2349 /* NetworkManager.swift in Sources */, DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */, DD73FD1128750779000852D6 /* PositionLog.swift in Sources */, diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 0251a62a..cc309864 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -841,22 +841,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if fromNodeNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 { return false } - -// if smartPosition { -// if lastPosition != nil { -// let connectedNode = getNodeInfo(id: connectedPeripheral?.num ?? 0, context: context!) -// if connectedNode?.positionConfig?.smartPositionEnabled ?? false { -// if lastPosition!.distance(from: LocationHelper.currentLocation) < Double(connectedNode?.positionConfig?.broadcastSmartMinimumDistance ?? 50) { -// return false -// } -// } -// } -// } -// lastPosition = LocationHelper.currentLocation -// var locationHelper = LocationHelper() var positionPacket = Position() - positionPacket.latitudeI = Int32((locationFetcher.lastKnownLocation?.latitude ?? 0) * 1e7) - positionPacket.longitudeI = Int32((locationFetcher.manager.location?.coordinate.longitude ?? 0) * 1e7) + positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) + positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) positionPacket.time = UInt32(LocationHelper.currentTimestamp.timeIntervalSince1970) positionPacket.timestamp = UInt32(LocationHelper.currentTimestamp.timeIntervalSince1970) positionPacket.altitude = Int32(LocationHelper.currentAltitude) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift index bbef8357..f7fc6eed 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift @@ -11,7 +11,6 @@ import MapKit import WeatherKit @available(iOS 17.0, macOS 14.0, *) - struct NodeMapSwiftUI: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -19,7 +18,7 @@ struct NodeMapSwiftUI: View { @ObservedObject var node: NodeInfoEntity @State var showUserLocation: Bool = false @State var positions: [PositionEntity] = [] - @State var waypoints: [WaypointEntity] = [] + //@State var waypoints: [WaypointEntity] = [] /// Map State User Defaults @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @AppStorage("meshMapShowRouteLines") private var showRouteLines = false @@ -35,7 +34,18 @@ struct NodeMapSwiftUI: View { @State private var isLookingAround = false @State private var isEditingSettings = false @State private var selected: PositionEntity? + @State private var selectedWaypoint: WaypointEntity? + @State private var selectedWaypointRect: CGRect = .zero + @State private var selectedWaypointPoint: CGPoint = .zero @State private var showingPositionPopover = false + @State private var showingWaypointPopover = false + + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], + predicate: NSPredicate( + format: "expire == nil || expire >= %@", Date() as NSDate + ), animation: .none) + private var waypoints: FetchedResults + @State var waypoiintSelectionRect: CGRect = .zero var body: some View { let positionArray = node.positions?.array as? [PositionEntity] ?? [] @@ -44,6 +54,7 @@ struct NodeMapSwiftUI: View { return position.nodeCoordinate ?? LocationHelper.DefaultLocation }) + if node.hasPositions { ZStack { Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { @@ -71,13 +82,29 @@ struct NodeMapSwiftUI: View { .stroke(Color(nodeColor.darker()), lineWidth: 5) .foregroundStyle(Color(nodeColor).opacity(0.4)) } + /// Waypoint Annotations + ForEach(Array(waypoints), id: \.id) { waypoint in + Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { + ZStack { + CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 35) + .onTapGesture(coordinateSpace: .global) { location in + print("Tapped at \(location)") + let size = CGSize(width: 1, height: 1) + let rect = CGRect(origin: location, size: size) + selectedWaypointRect = rect + selectedWaypointPoint = location + showingWaypointPopover = true + selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) + } + } + } + } /// Node Annotations - ForEach(positionArray.reversed(), id: \.id) { position in + ForEach(positionArray, id: \.id) { position in let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 3)) let formatter = MeasurementFormatter() - let speedText = formatter.string(from: Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour)) let headingDegrees = Angle.degrees(Double(position.heading)) - Annotation(position.latest ? node.user?.shortName ?? "?" : (pf.contains(.Speed) && position.speed > 2) ? speedText : "", coordinate: position.coordinate) { + Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) { ZStack { if position.latest { Circle() @@ -95,7 +122,7 @@ struct NodeMapSwiftUI: View { showingPositionPopover = true selected = (selected == position ? nil : position) // <-- here } - .popover(isPresented: $showingPositionPopover, arrowEdge: .bottom) { + .popover(isPresented: $showingPositionPopover) { PositionPopover(position: position) .padding() .opacity(0.8) @@ -114,6 +141,7 @@ struct NodeMapSwiftUI: View { } .popover(isPresented: $showingPositionPopover, arrowEdge: .bottom) { PositionPopover(position: position) + .tag(position.id) .padding() .opacity(0.8) .presentationCompactAdaptation(.popover) @@ -167,6 +195,13 @@ struct NodeMapSwiftUI: View { .padding(.horizontal, 20) } } + .popover(item: $selectedWaypoint, attachmentAnchor: .rect(.rect(selectedWaypointRect)), arrowEdge: .bottom) { selection in + //.popover(isPresented: $showingWaypointPopover, arrowEdge: .bottom) { + WaypointPopover(waypoint: selection) + .padding() + .opacity(0.8) + .presentationCompactAdaptation(.popover) + } .sheet(isPresented: $isEditingSettings) { VStack { Form { @@ -250,8 +285,8 @@ struct NodeMapSwiftUI: View { .padding() #endif } - //.presentationDetents([.fraction(0.5)]) - .presentationDetents([.medium]) + .presentationDetents([.fraction(0.46)]) + //.presentationDetents([.medium]) .presentationDragIndicator(.visible) } .onChange(of: node) { diff --git a/Meshtastic/Views/Nodes/Helpers/WaypointPopover.swift b/Meshtastic/Views/Nodes/Helpers/WaypointPopover.swift new file mode 100644 index 00000000..1c4d8f8d --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/WaypointPopover.swift @@ -0,0 +1,98 @@ +// +// WaypointPopover.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen on 9/19/23. +// + +import SwiftUI +import MapKit + +struct WaypointPopover: View { + var waypoint: WaypointEntity + let distanceFormatter = MKDistanceFormatter() + var body: some View { + VStack { + HStack { + CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.blue) + Text(waypoint.name ?? "?") + .font(.title3) + if waypoint.locked > 0 { + Image(systemName: "lock.fill" ) + .font(.title2) + } else { + // Edit Button + } + } + Divider() + VStack (alignment: .leading) { + // Description + if (waypoint.longDescription ?? "").count > 0 { + Label { + Text(waypoint.longDescription ?? "") + .foregroundColor(.primary) + .font(.footnote) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + } icon: { + Image(systemName: "doc.plaintext") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + } + /// Created + Label { + Text("Created: \(waypoint.created?.formatted() ?? "?")") + .foregroundColor(.primary) + .font(.footnote) + } icon: { + Image(systemName: "clock.badge.checkmark") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + /// Updated + if waypoint.lastUpdated != nil { + Label { + Text("Updated: \(waypoint.lastUpdated?.formatted() ?? "?")") + .foregroundColor(.primary) + .font(.footnote) + } icon: { + Image(systemName: "clock.arrow.circlepath") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + } + /// Updated + if waypoint.expire != nil { + Label { + Text("Expires: \(waypoint.expire?.formatted() ?? "?")") + .foregroundColor(.primary) + .font(.footnote) + } icon: { + Image(systemName: "clock.badge.xmark") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + } + /// Distance + if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 { + let metersAway = waypoint.coordinate.distance(from: LocationHelper.currentLocation) + Label { + Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") + .foregroundColor(.primary) + .font(.footnote) + } icon: { + Image(systemName: "lines.measurement.horizontal") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + } + } + } + .tag(waypoint.id) + } +} From 4130e7ca0076f5ed21d71ef64c21ff60ac988c06 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 21 Sep 2023 08:54:02 -0700 Subject: [PATCH 06/11] Use PWD abbreviation on live activity battery gauge --- Widgets/WidgetsLiveActivity.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 95230354..3f998b53 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -52,7 +52,7 @@ struct WidgetsLiveActivity: Widget { .foregroundColor(.gray) .fixedSize() } else { - Text("Plugged In") + Text("PWD") .font(.title3) .foregroundColor(.gray) } @@ -101,7 +101,6 @@ struct WidgetsLiveActivity: Widget { } } -@available(iOS 16.2, *) struct WidgetsLiveActivity_Previews: PreviewProvider { static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") static let state = MeshActivityAttributes.ContentState( @@ -123,7 +122,6 @@ struct WidgetsLiveActivity_Previews: PreviewProvider { } } -@available(iOS 16.2, *) struct LiveActivityView: View { @Environment(\.colorScheme) private var colorScheme @Environment(\.isLuminanceReduced) var isLuminanceReduced From 945aaa9eafc42aa01ef9e5fd4295249b446a6e32 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 21 Sep 2023 13:25:38 -0700 Subject: [PATCH 07/11] Update image, remove unnesary version checks in widgets --- Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift | 2 +- Widgets/WidgetsLiveActivity.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift index f7fc6eed..ca3ea5eb 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift @@ -150,7 +150,7 @@ struct NodeMapSwiftUI: View { } else { if showNodeHistory { if pf.contains(.Heading) { - Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north" : "hexagon") + Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north.circle" : "hexagon") .padding(2) .foregroundStyle(Color(UIColor(hex: UInt32(node.num)).lighter()).isLight() ? .black : .white) .background(Color(UIColor(hex: UInt32(node.num)).lighter())) diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 3f998b53..80d1fb50 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -9,7 +9,6 @@ import ActivityKit import WidgetKit import SwiftUI -@available(iOS 16.2, *) struct WidgetsLiveActivity: Widget { var body: some WidgetConfiguration { From 80afbcacd3cebb0433fc4ff40f8be9df171442a1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 24 Sep 2023 18:44:04 -0700 Subject: [PATCH 08/11] Log packets being sent to and coming back from the phone --- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Helpers/BLEManager.swift | 4 +- .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 4 +- .../contents | 366 +++++++++++++ Meshtastic/Persistence/UpdateCoreData.swift | 2 +- .../Views/Nodes/Helpers/NodeMapSwiftUI.swift | 513 +++++++++--------- 7 files changed, 634 insertions(+), 261 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 968271ec..b1d8fa0a 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -230,6 +230,7 @@ DD2553562855B02500E55709 /* LoRaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaConfig.swift; sourceTree = ""; }; DD2553582855B52700E55709 /* PositionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionConfig.swift; sourceTree = ""; }; DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = ""; }; + DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = ""; }; DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardwareModels.swift; sourceTree = ""; }; DD2E65252767A01F00E45FC5 /* NodeDetailOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetailOld.swift; sourceTree = ""; }; DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; @@ -1718,6 +1719,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */, DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */, DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */, DDC4CA012A8DAA3800CE201C /* MeshtasticDataModelV16.xcdatamodel */, @@ -1737,7 +1739,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */; + currentVersion = DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index cc309864..6d99e61a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -30,6 +30,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate @Published var mqttProxyConnected: Bool = false @StateObject var appState = AppState.shared + //public var locationHelper = LocationHelper.shared public var minimumVersion = "2.0.0" public var connectedVersion: String public var isConnecting: Bool = false @@ -42,7 +43,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var lastPosition: CLLocationCoordinate2D? let emptyNodeNum: UInt32 = 4294967295 let mqttManager = MqttClientProxyManager.shared - let locationHelper = LocationHelper.shared + //var locationHelper = LocationHelper.shared var wantRangeTestPackets = false /* Meshtastic Service Details */ var TORADIO_characteristic: CBCharacteristic! @@ -872,6 +873,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) success = true let logString = String.localizedStringWithFormat("mesh.log.sharelocation %@".localized, String(fromNodeNum)) + print(positionPacket) MeshLogger.log("📍 \(logString)") } return success diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 348d9e99..074f016d 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV18.xcdatamodel + MeshtasticDataModelV19.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV18.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV18.xcdatamodel/contents index c30e2f4b..ef4d9a8d 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV18.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV18.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -345,7 +345,7 @@ - + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents new file mode 100644 index 00000000..72bc0d71 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents @@ -0,0 +1,366 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 17310c61..16d253a7 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -218,7 +218,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) position.latest = false } } - + print("Incoming position message: \n \(positionMessage)") let position = PositionEntity(context: context) position.latest = true position.snr = packet.rxSnr diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift index ca3ea5eb..ade7d39a 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift @@ -48,232 +48,235 @@ struct NodeMapSwiftUI: View { @State var waypoiintSelectionRect: CGRect = .zero var body: some View { + let positionArray = node.positions?.array as? [PositionEntity] ?? [] let mostRecent = node.positions?.lastObject as? PositionEntity let lineCoords = positionArray.compactMap({(position) -> CLLocationCoordinate2D in return position.nodeCoordinate ?? LocationHelper.DefaultLocation }) - if node.hasPositions { ZStack { - Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { - /// Node Color from node.num - 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: 5, - lineCap: .round, lineJoin: .round, dash: [10, 10] - ) - MapPolyline(coordinates: lineCoords) - .stroke(gradient, style: dashed) + MapReader { reader in + Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { + /// Node Color from node.num + 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: 5, + lineCap: .round, lineJoin: .round, dash: [10, 10] + ) + MapPolyline(coordinates: lineCoords) + .stroke(gradient, style: dashed) + } } - } - /// Convex Hull - if showConvexHull { - let hull = lineCoords.getConvexHull() - MapPolygon(coordinates: hull) - .stroke(Color(nodeColor.darker()), lineWidth: 5) - .foregroundStyle(Color(nodeColor).opacity(0.4)) - } - /// Waypoint Annotations - ForEach(Array(waypoints), id: \.id) { waypoint in - Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { - ZStack { - CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 35) - .onTapGesture(coordinateSpace: .global) { location in - print("Tapped at \(location)") - let size = CGSize(width: 1, height: 1) - let rect = CGRect(origin: location, size: size) - selectedWaypointRect = rect - selectedWaypointPoint = location - showingWaypointPopover = true - selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) + /// Convex Hull + if showConvexHull { + let hull = lineCoords.getConvexHull() + MapPolygon(coordinates: hull) + .stroke(Color(nodeColor.darker()), lineWidth: 5) + .foregroundStyle(Color(nodeColor).opacity(0.4)) + } + /// Waypoint Annotations + ForEach(Array(waypoints), id: \.id) { waypoint in + Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { + ZStack { + CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 35) + .onTapGesture(coordinateSpace: .global) { location in + print("Tapped at \(location)") + let pinLocation = reader.convert(location, from: .local) + print(pinLocation) + let size = CGSize(width: 1, height: 50) + let rect = CGRect(origin: location, size: size) + selectedWaypointRect = rect + selectedWaypointPoint = location + showingWaypointPopover = true + selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) + } } } } - } - /// Node Annotations - ForEach(positionArray, id: \.id) { position in - let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 3)) - let formatter = MeasurementFormatter() - let headingDegrees = Angle.degrees(Double(position.heading)) - Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) { - ZStack { - if position.latest { - Circle() - .foregroundStyle(Color(nodeColor.lighter()).opacity(0.4)) - .frame(width: 60, height: 60) - if pf.contains(.Heading) { - Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north" : "hexagon") - .symbolEffect(.pulse.byLayer) - .padding(5) - .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) - .background(Color(UIColor(hex: UInt32(node.num)).darker())) - .clipShape(Circle()) - .rotationEffect(headingDegrees) - .onTapGesture { - showingPositionPopover = true - selected = (selected == position ? nil : position) // <-- here - } - .popover(isPresented: $showingPositionPopover) { - PositionPopover(position: position) - .padding() - .opacity(0.8) - .presentationCompactAdaptation(.popover) - } - } else { - Image(systemName: "flipphone") - .symbolEffect(.pulse.byLayer) - .padding(5) - .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) - .background(Color(UIColor(hex: UInt32(node.num)).darker())) - .clipShape(Circle()) - .onTapGesture { - showingPositionPopover = true - selected = (selected == position ? nil : position) // <-- here - } - .popover(isPresented: $showingPositionPopover, arrowEdge: .bottom) { - PositionPopover(position: position) - .tag(position.id) - .padding() - .opacity(0.8) - .presentationCompactAdaptation(.popover) - } - } - } else { - if showNodeHistory { + /// Node Annotations + ForEach(positionArray, id: \.id) { position in + let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 3)) + let formatter = MeasurementFormatter() + let headingDegrees = Angle.degrees(Double(position.heading)) + Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) { + ZStack { + if position.latest { + Circle() + .foregroundStyle(Color(nodeColor.lighter()).opacity(0.4)) + .frame(width: 60, height: 60) if pf.contains(.Heading) { - Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north.circle" : "hexagon") - .padding(2) - .foregroundStyle(Color(UIColor(hex: UInt32(node.num)).lighter()).isLight() ? .black : .white) - .background(Color(UIColor(hex: UInt32(node.num)).lighter())) + Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north" : "hexagon") + .symbolEffect(.pulse.byLayer) + .padding(5) + .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) + .background(Color(UIColor(hex: UInt32(node.num)).darker())) .clipShape(Circle()) .rotationEffect(headingDegrees) + .onTapGesture { + showingPositionPopover = true + selected = (selected == position ? nil : position) // <-- here + } + .popover(isPresented: $showingPositionPopover) { + PositionPopover(position: position) + .padding() + .opacity(0.8) + .presentationCompactAdaptation(.popover) + } } else { - Image(systemName: "mappin.circle") - .padding(2) - .foregroundStyle(Color(UIColor(hex: UInt32(node.num)).lighter()).isLight() ? .black : .white) - .background(Color(UIColor(hex: UInt32(node.num)).lighter())) + Image(systemName: "flipphone") + .symbolEffect(.pulse.byLayer) + .padding(5) + .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) + .background(Color(UIColor(hex: UInt32(node.num)).darker())) .clipShape(Circle()) + .onTapGesture { + showingPositionPopover = true + selected = (selected == position ? nil : position) // <-- here + } + .popover(isPresented: $showingPositionPopover, arrowEdge: .bottom) { + PositionPopover(position: position) + .tag(position.id) + .padding() + .opacity(0.8) + .presentationCompactAdaptation(.popover) + } + } + } else { + if showNodeHistory { + if pf.contains(.Heading) { + Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north.circle" : "hexagon") + .padding(2) + .foregroundStyle(Color(UIColor(hex: UInt32(node.num)).lighter()).isLight() ? .black : .white) + .background(Color(UIColor(hex: UInt32(node.num)).lighter())) + .clipShape(Circle()) + .rotationEffect(headingDegrees) + } else { + Image(systemName: "mappin.circle") + .padding(2) + .foregroundStyle(Color(UIColor(hex: UInt32(node.num)).lighter()).isLight() ? .black : .white) + .background(Color(UIColor(hex: UInt32(node.num)).lighter())) + .clipShape(Circle()) + } } } } } + .tag(position.time) + .annotationTitles(.automatic) + .annotationSubtitles(.automatic) } - .tag(position.time) - .annotationTitles(.automatic) - .annotationSubtitles(.automatic) } - } - .mapScope(mapScope) - .mapStyle(mapStyle) - .mapControls { - MapScaleView(scope: mapScope) - .mapControlVisibility(.visible) - if showUserLocation { - MapUserLocationButton(scope: mapScope) + .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) } - MapPitchToggle(scope: mapScope) - .mapControlVisibility(.visible) - MapCompass(scope: mapScope) - .mapControlVisibility(.visible) - } - .controlSize(.regular) - .overlay(alignment: .bottom) { - if scene != nil && isLookingAround { - LookAroundPreview(initialScene: scene) - .frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 250 : 400) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .padding(.horizontal, 20) + .controlSize(.regular) + .overlay(alignment: .bottom) { + if scene != nil && isLookingAround { + LookAroundPreview(initialScene: scene) + .frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 250 : 400) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .padding(.horizontal, 20) + } } - } - .popover(item: $selectedWaypoint, attachmentAnchor: .rect(.rect(selectedWaypointRect)), arrowEdge: .bottom) { selection in - //.popover(isPresented: $showingWaypointPopover, arrowEdge: .bottom) { - WaypointPopover(waypoint: selection) - .padding() - .opacity(0.8) - .presentationCompactAdaptation(.popover) - } - .sheet(isPresented: $isEditingSettings) { - VStack { - Form { - Section(header: Text("Map Options")) { - Picker(selection: $selectedMapLayer, label: Text("")) { - ForEach(MapLayer.allCases, id: \.self) { layer in - if layer != MapLayer.offline { - Text(layer.localized) + .popover(item: $selectedWaypoint, attachmentAnchor: .rect(.rect(selectedWaypointRect)), arrowEdge: .bottom) { selection in + //.popover(isPresented: $showingWaypointPopover, arrowEdge: .bottom) { + WaypointPopover(waypoint: selection) + .padding() + .opacity(0.8) + .presentationCompactAdaptation(.popover) + } + .sheet(isPresented: $isEditingSettings) { + VStack { + Form { + Section(header: Text("Map Options")) { + Picker(selection: $selectedMapLayer, label: Text("")) { + ForEach(MapLayer.allCases, id: \.self) { layer in + if layer != MapLayer.offline { + Text(layer.localized) + } } } - } - .pickerStyle(SegmentedPickerStyle()) - .onChange(of: (selectedMapLayer)) { newMapLayer in - switch selectedMapLayer { - case .standard: - UserDefaults.mapLayer = newMapLayer - mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) - case .hybrid: - UserDefaults.mapLayer = newMapLayer - mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) - case .satellite: - UserDefaults.mapLayer = newMapLayer - mapStyle = MapStyle.imagery(elevation: .realistic) - case .offline: - return + .pickerStyle(SegmentedPickerStyle()) + .onChange(of: (selectedMapLayer)) { newMapLayer in + switch selectedMapLayer { + case .standard: + UserDefaults.mapLayer = newMapLayer + mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + case .hybrid: + UserDefaults.mapLayer = newMapLayer + mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + case .satellite: + UserDefaults.mapLayer = newMapLayer + mapStyle = MapStyle.imagery(elevation: .realistic) + case .offline: + return + } + } + .padding(.top, 5) + .padding(.bottom, 5) + Toggle(isOn: $showNodeHistory) { + Label("Node History", systemImage: "building.columns.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.showNodeHistory.toggle() + UserDefaults.enableMapNodeHistoryPins = self.showNodeHistory + } + Toggle(isOn: $showRouteLines) { + Label("Route Lines", systemImage: "road.lanes") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.showRouteLines.toggle() + UserDefaults.enableMapRouteLines = self.showRouteLines + } + Toggle(isOn: $showConvexHull) { + Label("Convex Hull", systemImage: "button.angledbottom.horizontal.right") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.showConvexHull.toggle() + UserDefaults.enableMapConvexHull = self.showConvexHull + } + Toggle(isOn: $showTraffic) { + Label("Traffic", systemImage: "car") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.showTraffic.toggle() + UserDefaults.enableMapTraffic = self.showTraffic + } + Toggle(isOn: $showPointsOfInterest) { + Label("Points of Interest", systemImage: "mappin.and.ellipse") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onTapGesture { + self.showPointsOfInterest.toggle() + UserDefaults.enableMapPointsOfInterest = self.showPointsOfInterest } } - .padding(.top, 5) - .padding(.bottom, 5) - Toggle(isOn: $showNodeHistory) { - Label("Node History", systemImage: "building.columns.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.showNodeHistory.toggle() - UserDefaults.enableMapNodeHistoryPins = self.showNodeHistory - } - Toggle(isOn: $showRouteLines) { - Label("Route Lines", systemImage: "road.lanes") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.showRouteLines.toggle() - UserDefaults.enableMapRouteLines = self.showRouteLines - } - Toggle(isOn: $showConvexHull) { - Label("Convex Hull", systemImage: "button.angledbottom.horizontal.right") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.showConvexHull.toggle() - UserDefaults.enableMapConvexHull = self.showConvexHull - } - Toggle(isOn: $showTraffic) { - Label("Traffic", systemImage: "car") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.showTraffic.toggle() - UserDefaults.enableMapTraffic = self.showTraffic - } - Toggle(isOn: $showPointsOfInterest) { - Label("Points of Interest", systemImage: "mappin.and.ellipse") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.showPointsOfInterest.toggle() - UserDefaults.enableMapPointsOfInterest = self.showPointsOfInterest - } } - } - #if targetEnvironment(macCatalyst) +#if targetEnvironment(macCatalyst) Button { isEditingSettings = false } label: { @@ -283,84 +286,84 @@ struct NodeMapSwiftUI: View { .buttonBorderShape(.capsule) .controlSize(.large) .padding() - #endif - } - .presentationDetents([.fraction(0.46)]) - //.presentationDetents([.medium]) - .presentationDragIndicator(.visible) - } - .onChange(of: node) { - let mostRecent = node.positions?.lastObject as? PositionEntity - position = MapCameraPosition.automatic//.camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 1500, heading: 0, pitch: 0)) - if let mostRecent { - Task { - scene = try? await fetchScene(for: mostRecent.coordinate) +#endif } + .presentationDetents([.fraction(0.46)]) + //.presentationDetents([.medium]) + .presentationDragIndicator(.visible) } - } - .onAppear { - UIApplication.shared.isIdleTimerDisabled = true - switch selectedMapLayer { - case .standard: - mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) - case .hybrid: - mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) - case .satellite: - mapStyle = MapStyle.imagery(elevation: .realistic) - case .offline: - mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) - } - if self.scene == nil { - Task { - scene = try? await fetchScene(for: mostRecent!.coordinate) - } - } - } - .safeAreaInset(edge: .bottom, alignment: UIDevice.current.userInterfaceIdiom == .phone ? .leading : .trailing) { - HStack { - Button(action: { - withAnimation { - isEditingSettings = !isEditingSettings + .onChange(of: node) { + let mostRecent = node.positions?.lastObject as? PositionEntity + position = MapCameraPosition.automatic//.camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 1500, heading: 0, pitch: 0)) + if let mostRecent { + Task { + scene = try? await fetchScene(for: mostRecent.coordinate) } - }) { - Image(systemName: isEditingSettings ? "info.circle.fill" : "info.circle") - .padding(.vertical, 5) } - .tint(Color(UIColor.secondarySystemBackground)) - .foregroundColor(.accentColor) - .buttonStyle(.borderedProminent) - /// Look Around Button - if self.scene != nil { + } + .onAppear { + UIApplication.shared.isIdleTimerDisabled = true + switch selectedMapLayer { + case .standard: + mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + case .hybrid: + mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + case .satellite: + mapStyle = MapStyle.imagery(elevation: .realistic) + case .offline: + mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + } + if self.scene == nil { + Task { + scene = try? await fetchScene(for: mostRecent!.coordinate) + } + } + } + .safeAreaInset(edge: .bottom, alignment: UIDevice.current.userInterfaceIdiom == .phone ? .leading : .trailing) { + HStack { Button(action: { withAnimation { - isLookingAround = !isLookingAround + isEditingSettings = !isEditingSettings } }) { - Image(systemName: isLookingAround ? "binoculars.fill" : "binoculars") + Image(systemName: isEditingSettings ? "info.circle.fill" : "info.circle") .padding(.vertical, 5) } .tint(Color(UIColor.secondarySystemBackground)) .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) - } - - #if targetEnvironment(macCatalyst) + /// Look Around Button + if self.scene != nil { + Button(action: { + withAnimation { + isLookingAround = !isLookingAround + } + }) { + Image(systemName: isLookingAround ? "binoculars.fill" : "binoculars") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } + +#if targetEnvironment(macCatalyst) MapZoomStepper(scope: mapScope) .mapControlVisibility(.visible) MapPitchSlider(scope: mapScope) .mapControlVisibility(.visible) - #endif +#endif + } + .controlSize(.regular) + .padding(5) } - .controlSize(.regular) - .padding(5) - } - .onDisappear { - UIApplication.shared.isIdleTimerDisabled = false - } - } + .onDisappear { + UIApplication.shared.isIdleTimerDisabled = false + } + }} .navigationBarTitle(String((node.user?.shortName ?? "unknown".localized) + (" \(node.positions?.count ?? 0) points")), displayMode: .inline) .navigationBarItems(trailing: - ZStack { + ZStack { ConnectedDevice( bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, @@ -372,7 +375,7 @@ struct NodeMapSwiftUI: View { } private func fetchScene(for coordinate: CLLocationCoordinate2D) async throws -> MKLookAroundScene? { - let lookAroundScene = MKLookAroundSceneRequest(coordinate: coordinate) - return try await lookAroundScene.scene + let lookAroundScene = MKLookAroundSceneRequest(coordinate: coordinate) + return try await lookAroundScene.scene } } From 0210f0e7602f02a52b35a6cd9d9ec8e1ac4fd52a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 25 Sep 2023 09:44:57 -0700 Subject: [PATCH 09/11] Check for nil context on message views --- Meshtastic/Views/Messages/ChannelList.swift | 5 ++++ .../Views/Messages/ChannelMessageList.swift | 5 +++- Meshtastic/Views/Messages/Messages.swift | 4 ++- Meshtastic/Views/Messages/UserList.swift | 26 +++++++++---------- .../Views/Messages/UserMessageList.swift | 8 +++--- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index 5df1a487..8349abf3 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -123,6 +123,11 @@ struct ChannelList: View { Text("delete") } } + .onAppear { + if self.bleManager.context == nil { + self.bleManager.context = context + } + } } } .padding([.top, .bottom]) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index f6211355..74a92b70 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -259,7 +259,9 @@ struct ChannelMessageList: View { .padding([.top]) .scrollDismissesKeyboard(.immediately) .onAppear(perform: { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } if channel.allPrivateMessages.count > 0 { scrollView.scrollTo(channel.allPrivateMessages.last!.messageId) } @@ -409,6 +411,7 @@ struct ChannelMessageList: View { }) { Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.accentColor) } + } .padding(.all, 15) } diff --git a/Meshtastic/Views/Messages/Messages.swift b/Meshtastic/Views/Messages/Messages.swift index c4bd1f89..f22414cd 100644 --- a/Meshtastic/Views/Messages/Messages.swift +++ b/Meshtastic/Views/Messages/Messages.swift @@ -63,7 +63,9 @@ struct Messages: View { .navigationBarTitleDisplayMode(.large) .navigationBarItems(leading: MeshtasticLogo()) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } if UserDefaults.preferredPeripheralId.count > 0 { let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(bleManager.connectedPeripheral?.num ?? -1)) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 9fa4e2b8..8959407e 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -143,19 +143,19 @@ struct UserList: View { } } } - .alert( - "Trace Route Sent", - isPresented: $isPresentingTraceRouteSentAlert - ) { - Button("OK", role: .cancel) { } - } message: { - Text("This could take a while, response will appear in the mesh log.") - } - .confirmationDialog( - "This conversation will be deleted.", - isPresented: $isPresentingDeleteUserMessagesConfirm, - titleVisibility: .visible - ) { + .alert( + "Trace Route Sent", + isPresented: $isPresentingTraceRouteSentAlert + ) { + Button("OK", role: .cancel) { } + } message: { + Text("This could take a while, response will appear in the mesh log.") + } + .confirmationDialog( + "This conversation will be deleted.", + isPresented: $isPresentingDeleteUserMessagesConfirm, + titleVisibility: .visible + ) { Button(role: .destructive) { deleteUserMessages(user: userSelection!, context: context) context.refresh(node!.user!, mergeChanges: true) diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 4b9898ff..c2458d71 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -237,12 +237,14 @@ struct UserMessageList: View { } .padding([.top]) .scrollDismissesKeyboard(.immediately) - .onAppear(perform: { - self.bleManager.context = context + .onAppear { + if self.bleManager.context == nil { + self.bleManager.context = context + } if user.messageList.count > 0 { scrollView.scrollTo(user.messageList.last!.messageId) } - }) + } .onChange(of: user.messageList, perform: { _ in if user.messageList.count > 0 { scrollView.scrollTo(user.messageList.last!.messageId) From 805b149294474ebd5c9933f67bb47a1acd9dcfe6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 25 Sep 2023 10:38:32 -0700 Subject: [PATCH 10/11] Update protos and core data --- .../contents | 10 +++++++++ .../Protobufs/meshtastic/config.pb.swift | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents index 72bc0d71..0dc699d5 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV19.xcdatamodel/contents @@ -1,5 +1,13 @@ + + + + + + + + @@ -54,6 +62,7 @@ + @@ -214,6 +223,7 @@ + diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index eeee9214..6cb98a73 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -186,6 +186,10 @@ struct Config { /// Clients should then limit available configuration and administrative options inside the user interface var isManaged: Bool = false + /// + /// Disables the triple-press of user button to enable or disable GPS + var disableTripleClick: Bool = false + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -371,6 +375,10 @@ struct Config { /// The minimum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled var broadcastSmartMinimumIntervalSecs: UInt32 = 0 + /// + /// (Re)define PIN_GPS_EN for your board. + var gpsEnGpio: UInt32 = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -951,6 +959,7 @@ struct Config { /// /// Maximum number of hops. This can't be greater than 7. /// Default of 3 + /// Attempting to set a value > 7 results in the default var hopLimit: UInt32 = 0 /// @@ -1596,6 +1605,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl 7: .standard(proto: "node_info_broadcast_secs"), 8: .standard(proto: "double_tap_as_button_press"), 9: .standard(proto: "is_managed"), + 10: .standard(proto: "disable_triple_click"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1613,6 +1623,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl 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) }() + case 10: try { try decoder.decodeSingularBoolField(value: &self.disableTripleClick) }() default: break } } @@ -1646,6 +1657,9 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if self.isManaged != false { try visitor.visitSingularBoolField(value: self.isManaged, fieldNumber: 9) } + if self.disableTripleClick != false { + try visitor.visitSingularBoolField(value: self.disableTripleClick, fieldNumber: 10) + } try unknownFields.traverse(visitor: &visitor) } @@ -1659,6 +1673,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if lhs.nodeInfoBroadcastSecs != rhs.nodeInfoBroadcastSecs {return false} if lhs.doubleTapAsButtonPress != rhs.doubleTapAsButtonPress {return false} if lhs.isManaged != rhs.isManaged {return false} + if lhs.disableTripleClick != rhs.disableTripleClick {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1698,6 +1713,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm 9: .standard(proto: "tx_gpio"), 10: .standard(proto: "broadcast_smart_minimum_distance"), 11: .standard(proto: "broadcast_smart_minimum_interval_secs"), + 12: .standard(proto: "gps_en_gpio"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1717,6 +1733,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm case 9: try { try decoder.decodeSingularUInt32Field(value: &self.txGpio) }() case 10: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumDistance) }() case 11: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumIntervalSecs) }() + case 12: try { try decoder.decodeSingularUInt32Field(value: &self.gpsEnGpio) }() default: break } } @@ -1756,6 +1773,9 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if self.broadcastSmartMinimumIntervalSecs != 0 { try visitor.visitSingularUInt32Field(value: self.broadcastSmartMinimumIntervalSecs, fieldNumber: 11) } + if self.gpsEnGpio != 0 { + try visitor.visitSingularUInt32Field(value: self.gpsEnGpio, fieldNumber: 12) + } try unknownFields.traverse(visitor: &visitor) } @@ -1771,6 +1791,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if lhs.txGpio != rhs.txGpio {return false} if lhs.broadcastSmartMinimumDistance != rhs.broadcastSmartMinimumDistance {return false} if lhs.broadcastSmartMinimumIntervalSecs != rhs.broadcastSmartMinimumIntervalSecs {return false} + if lhs.gpsEnGpio != rhs.gpsEnGpio {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } From 1181c69ff12a2a9c3b0817684566619314a2751b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 25 Sep 2023 12:09:26 -0700 Subject: [PATCH 11/11] Comment out waypoint popover --- .../Views/Nodes/Helpers/NodeMapSwiftUI.swift | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift index ade7d39a..aaa8732c 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift @@ -88,17 +88,17 @@ struct NodeMapSwiftUI: View { Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { ZStack { CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 35) - .onTapGesture(coordinateSpace: .global) { location in - print("Tapped at \(location)") - let pinLocation = reader.convert(location, from: .local) - print(pinLocation) - let size = CGSize(width: 1, height: 50) - let rect = CGRect(origin: location, size: size) - selectedWaypointRect = rect - selectedWaypointPoint = location - showingWaypointPopover = true - selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) - } +// .onTapGesture(coordinateSpace: .global) { location in +// print("Tapped at \(location)") +// let pinLocation = reader.convert(location, from: .local) +// print(pinLocation) +// let size = CGSize(width: 1, height: 50) +// let rect = CGRect(origin: location, size: size) +// selectedWaypointRect = rect +// selectedWaypointPoint = location +// showingWaypointPopover = true +// selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) +// } } } } @@ -198,13 +198,13 @@ struct NodeMapSwiftUI: View { .padding(.horizontal, 20) } } - .popover(item: $selectedWaypoint, attachmentAnchor: .rect(.rect(selectedWaypointRect)), arrowEdge: .bottom) { selection in - //.popover(isPresented: $showingWaypointPopover, arrowEdge: .bottom) { - WaypointPopover(waypoint: selection) - .padding() - .opacity(0.8) - .presentationCompactAdaptation(.popover) - } +// .popover(item: $selectedWaypoint, attachmentAnchor: .rect(.rect(selectedWaypointRect)), arrowEdge: .bottom) { selection in +// //.popover(isPresented: $showingWaypointPopover, arrowEdge: .bottom) { +// WaypointPopover(waypoint: selection) +// .padding() +// .opacity(0.8) +// .presentationCompactAdaptation(.popover) +// } .sheet(isPresented: $isEditingSettings) { VStack { Form {