From a30fc18d73180b20850166a308abe691138b67f5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 9 Dec 2023 15:01:01 -0800 Subject: [PATCH] Trace route cleanup --- Meshtastic/Views/Messages/UserList.swift | 17 -- Meshtastic/Views/Nodes/NodeList.swift | 21 ++- Meshtastic/Views/Nodes/TraceRouteLog.swift | 171 ++++++++++++------ Meshtastic/Views/Settings/RouteRecorder.swift | 16 +- 4 files changed, 146 insertions(+), 79 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index d35f6592..f102b6bd 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -33,7 +33,6 @@ struct UserList: View { @State var node: NodeInfoEntity? @State private var userSelection: UserEntity? // Nothing selected by default. @State private var isPresentingDeleteUserMessagesConfirm: Bool = false - @State private var isPresentingTraceRouteSentAlert = false var body: some View { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current) @@ -126,14 +125,6 @@ struct UserList: View { } 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 - } - } label: { - Label("Trace Route", systemImage: "signpost.right.and.left") - } if user.messageList.count > 0 { Button(role: .destructive) { isPresentingDeleteUserMessagesConfirm = true @@ -143,14 +134,6 @@ 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, diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 7e6f30ec..c0df0167 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -11,6 +11,7 @@ struct NodeList: View { @State private var columnVisibility = NavigationSplitViewVisibility.all @State private var selectedNode: NodeInfoEntity? + @State private var isPresentingTraceRouteSentAlert = false @SceneStorage("selectedDetailView") var selectedDetailView: String? @@ -72,13 +73,31 @@ struct NodeList: View { } label: { Label(node.user!.mute ? "Show Alerts" : "Hide Alerts", systemImage: node.user!.mute ? "bell" : "bell.slash") } + if connectedNodeNum != node.num { + Button { + let success = bleManager.sendTraceRouteRequest(destNum: node.user?.num ?? 0, wantResponse: true) + if success { + isPresentingTraceRouteSentAlert = true + } + } label: { + Label("Trace Route", systemImage: "signpost.right.and.left") + } + } } - + } + .alert( + "Trace Route Sent", + isPresented: $isPresentingTraceRouteSentAlert + ) { + Button("OK", role: .cancel) { } + } message: { + Text("This could take a while, response will appear in the trace route log for the node it was sent to.") } } .searchable(text: nodesQuery, prompt: "Find a node") .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) .listStyle(.plain) + .navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500) .navigationBarItems(leading: MeshtasticLogo(), diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 8f139f77..7b525932 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -12,6 +12,7 @@ import MapKit @available(iOS 17.0, macOS 14.0, *) struct TraceRouteLog: View { + @ObservedObject var locationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -20,84 +21,136 @@ struct TraceRouteLog: View { @State var exportString = "" @ObservedObject var node: NodeInfoEntity @State private var selectedRoute: TraceRouteEntity? - + // Map Configuration + @Namespace var mapScope + @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .all, showsTraffic: true) + @State var position = MapCameraPosition.automatic + let distanceFormatter = MKDistanceFormatter() + var body: some View { - VStack { + HStack (alignment: .top) { VStack { - List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in - Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "Other") : "No Response")") - } - .listStyle(.plain) - } - .navigationTitle("Trace Route List") - VStack { - if selectedRoute != nil { - Divider() - if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { - Text("Trace Route received by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") - Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)") - .font(.title) - } else if selectedRoute?.response ?? false { - Text("Trace Route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") - .font(.title) + VStack { + List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in + + Label { + Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count == 0) Hops") : "No Response")") + } icon: { + Image(systemName: route.response ? (route.hops?.count == 0 && route.response ? "person.line.dotted.person" : "point.3.connected.trianglepath.dotted") : "person.slash") + .symbolRenderingMode(.hierarchical) + } } - let hopsArray = selectedRoute?.hops?.array as? [TraceRouteHopEntity] ?? [] - let lineCoords = hopsArray.compactMap({(hop) -> CLLocationCoordinate2D in - return hop.coordinate ?? LocationHelper.DefaultLocation - }) - if selectedRoute?.response ?? false { - Map() { - Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) { - ZStack { - Circle() - .fill(Color(.green)) - .strokeBorder(.white, lineWidth: 3) - .frame(width: 15, height: 15) - } + .listStyle(.plain) + } + .frame(minHeight: 200, maxHeight: 230) + VStack { + if selectedRoute != nil { + if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { + Text("Received by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)") + .font(.title3) + } else if selectedRoute?.response ?? false { + Label { + Text("Trace route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) } - .annotationTitles(.automatic) - // Direct Trace Route - if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 { - if selectedRoute?.node?.positions?.count ?? 0 > 0 { - let mostRecent = selectedRoute?.node?.positions?.lastObject as! PositionEntity - var traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate] - Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) { + .font(.title3) + } + + let hopsArray = selectedRoute?.hops?.array as? [TraceRouteHopEntity] ?? [] + let lineCoords = hopsArray.compactMap({(hop) -> CLLocationCoordinate2D in + return hop.coordinate ?? LocationHelper.DefaultLocation + }) + if selectedRoute?.response ?? false { + if selectedRoute?.coordinate != nil && (selectedRoute?.node?.positions?.count ?? 0 > 0 || false ) { + Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { + Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) { ZStack { Circle() - .fill(Color(.black)) + .fill(Color(.green)) .strokeBorder(.white, lineWidth: 3) .frame(width: 15, height: 15) } } - let dashed = StrokeStyle( - lineWidth: 3, - lineCap: .round, lineJoin: .round, dash: [7, 10] - ) - MapPolyline(coordinates: traceRouteCoords) - .stroke(.blue, style: dashed) + .annotationTitles(.automatic) + // Direct Trace Route + if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 { + if selectedRoute?.node?.positions?.count ?? 0 > 0 { + let mostRecent = selectedRoute?.node?.positions?.lastObject as! PositionEntity + var traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate] + Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) { + ZStack { + Circle() + .fill(Color(.black)) + .strokeBorder(.white, lineWidth: 3) + .frame(width: 15, height: 15) + } + } + let dashed = StrokeStyle( + lineWidth: 2, + lineCap: .round, lineJoin: .round, dash: [7, 10] + ) + MapPolyline(coordinates: traceRouteCoords) + .stroke(.blue, style: dashed) + } + } else if selectedRoute?.hops?.count ?? 0 == 0 { + + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + VStack { + /// Distance + if selectedRoute?.node?.positions?.count ?? 0 > 0 && selectedRoute?.coordinate != nil { + let mostRecent = selectedRoute?.node?.positions?.lastObject as! PositionEntity + let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude) + + if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { + let metersAway = selectedRoute?.coordinate?.distance(from:CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude)) + Label { + Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway ?? 0)))") + .foregroundColor(.primary) + } icon: { + Image(systemName: "lines.measurement.horizontal") + .symbolRenderingMode(.hierarchical) + } + } } } + } else { + VStack { + Label { + Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + } + .font(.title3) + Divider() + Label { + Text("\(selectedRoute?.time?.formatted() ?? "") - No response") + + } icon: { + Image(systemName: "person.slash") + .symbolRenderingMode(.hierarchical) + } + .font(.callout) + Spacer() + } } - .frame(maxWidth: .infinity, maxHeight: .infinity) } else { - Text("Trace Route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") - .font(.title) - .padding(.top) - Spacer() - Text("\(selectedRoute?.time?.formatted() ?? "")") - .font(.title3) - Spacer() - Text("No response") - .font(.title2) - Spacer() + ContentUnavailableView("Select a Trace Route", systemImage: "signpost.right.and.left") } } + Spacer() } - .navigationTitle("Route Details") + .navigationTitle("Trace Route Log") } .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { if self.bleManager.context == nil { diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index df5ca511..eb9ba679 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -144,14 +144,26 @@ struct RouteRecorder: View { Label("Speed \(speed.formatted())", systemImage: "speedometer") } if locationsHandler.lastLocation.courseAccuracy > 0 { - Label("Heading \(String(format: "%.2f", locationsHandler.lastLocation.course))°", systemImage: "location.circle") + /// Heading + let degrees = Angle.degrees(Double(locationsHandler.lastLocation.course)) + Label { + let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees) + /// Text("Heading: \(heading.formatted())") + Text("Heading \(String(format: "%.2f", locationsHandler.lastLocation.course))°") + .foregroundColor(.primary) + } icon: { + Image(systemName: "location.circle") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + .rotationEffect(degrees) + } } } .listStyle(.plain) } } } - .presentationDetents([.fraction(0.5)]) + .presentationDetents([.fraction(0.6)]) .presentationDragIndicator(.visible) } }