From b80b9e65b303d1fcdd9321d089c2c2a27024dc7d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Sep 2023 15:41:03 -0700 Subject: [PATCH] Add small weather widget back to node map --- Meshtastic.xcodeproj/project.pbxproj | 4 - Meshtastic/Views/ContentView.swift | 2 +- Meshtastic/Views/Nodes/Helpers/NodeInfo.swift | 93 +++++++ .../Views/Nodes/Helpers/NodeMapControl.swift | 54 ++++ Meshtastic/Views/Nodes/NodeInfoViewOld.swift | 261 ------------------ Meshtastic/Views/Nodes/NodeList.swift | 2 +- 6 files changed, 149 insertions(+), 267 deletions(-) create mode 100644 Meshtastic/Views/Nodes/Helpers/NodeInfo.swift delete mode 100644 Meshtastic/Views/Nodes/NodeInfoViewOld.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index b4c4f63b..2049f791 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -162,7 +162,6 @@ DDDE5A1129AFE69700490C6C /* MeshActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */; }; DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; DDDE5A1429AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; - DDDEE5E129DA3E1100A8E078 /* NodeInfoViewOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDEE5E029DA3E1100A8E078 /* NodeInfoViewOld.swift */; }; DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */; }; DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */; }; DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; @@ -382,7 +381,6 @@ DDDE5A0429AF163E00490C6C /* WidgetsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetsExtension.entitlements; sourceTree = ""; }; DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshActivityAttributes.swift; sourceTree = ""; }; DDDE5A1229AFEAB900490C6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - DDDEE5E029DA3E1100A8E078 /* NodeInfoViewOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoViewOld.swift; 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 = ""; }; DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; @@ -474,7 +472,6 @@ DD73FD1028750779000852D6 /* PositionLog.swift */, DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */, 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */, - DDDEE5E029DA3E1100A8E078 /* NodeInfoViewOld.swift */, DD2E65252767A01F00E45FC5 /* NodeDetailOld.swift */, DD47E3CD26F103C600029299 /* NodeListOld.swift */, ); @@ -1130,7 +1127,6 @@ 6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */, DD47E3CE26F103C600029299 /* NodeListOld.swift in Sources */, DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */, - DDDEE5E129DA3E1100A8E078 /* NodeInfoViewOld.swift in Sources */, DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */, DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */, DD47E3D626F17ED900029299 /* CircleText.swift in Sources */, diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index 5f2340fe..35b2a9ea 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -19,7 +19,7 @@ struct ContentView: View { Label("bluetooth", systemImage: "antenna.radiowaves.left.and.right") } .tag(Tab.ble) - NodeListSplit() + NodeList() .tabItem { Label("nodes", systemImage: "flipphone") } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfo.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfo.swift new file mode 100644 index 00000000..3c230e27 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfo.swift @@ -0,0 +1,93 @@ +// +// NodeInfoItem.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 9/9/23. +// + +import SwiftUI +import CoreLocation +import MapKit + +struct NodeInfoItem: View { + + @ObservedObject var node: NodeInfoEntity + + var body: some View { + + Divider() + + HStack { + + VStack(alignment: .center) { + CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65) + } + if node.user != nil { + Divider() + VStack { + Image(node.user!.hwModel ?? "unset".localized) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 75, height: 75) + .cornerRadius(5) + Text(String(node.user!.hwModel ?? "unset".localized)) + .font(.caption2).fixedSize() + } + } + if node.snr != 0 { + Divider() + VStack(alignment: .center) { + let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate) + LoRaSignalStrengthIndicator(signalStrength: signalStrength) + Text("Signal \(signalStrength.description)").font(.footnote) + Text("SNR \(String(format: "%.2f", node.snr))dB") + .foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate)) + .font(.caption2) + Text("RSSI \(node.rssi)dB") + .foregroundColor(getRssiColor(rssi: node.rssi)) + .font(.caption2) + } + } + let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) + if deviceMetrics?.count ?? 0 >= 1 { + Divider() + let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity + VStack(alignment: .center) { + BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) + if mostRecent?.voltage ?? 0 > 0 { + + Text(String(format: "%.2f", mostRecent?.voltage ?? 0) + " V") + .font(.callout) + .foregroundColor(.gray) + .fixedSize() + } + } + } + } + Divider() + HStack(alignment: .center) { + VStack { + HStack { + Image(systemName: "number") + .font(.title2) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("Node Number:").font(.title2) + } + Text(String(node.num)).font(.title3).foregroundColor(.gray) + } + Divider() + VStack { + HStack { + Image(systemName: "person") + .font(.title2) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("User Id:").font(.title2) + } + Text(node.user?.userId ?? "?").font(.title3).foregroundColor(.gray) + } + } + Divider() + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift index 1a816892..de1e8eff 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift @@ -7,11 +7,21 @@ import SwiftUI import CoreLocation import MapKit +import WeatherKit struct NodeMapControl: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager + /// Weather + /// The current weather condition for the city. + @State private var condition: WeatherCondition? + @State private var temperature: Measurement? + @State private var humidity: Int? + @State private var symbolName: String = "cloud.fill" + @State private var attributionLink: URL? + @State private var attributionLogo: URL? + @Environment(\.colorScheme) var colorScheme: ColorScheme @AppStorage("meshMapType") private var meshMapType = 0 @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false @@ -76,6 +86,50 @@ struct NodeMapControl: View { .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) .pickerStyle(.menu) .padding(5) + VStack { + Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName) + .font(.caption) + + Label("\(humidity ?? 0)%", systemImage: "humidity") + .font(.caption2) + } + .padding(10) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + .padding(5) + VStack { + AsyncImage(url: attributionLogo) { image in + image + .resizable() + .scaledToFit() + } placeholder: { + ProgressView() + .controlSize(.mini) + } + .frame(height: 15) + + Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!) + } + .font(.footnote) + .padding(.bottom, 5) + .task { + do { + if node.hasPositions { + let mostRecent = node.positions?.lastObject as? PositionEntity + let weather = try await WeatherService.shared.weather(for: mostRecent?.nodeLocation ?? CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)) + condition = weather.currentWeather.condition + temperature = weather.currentWeather.temperature + humidity = Int(weather.currentWeather.humidity * 100) + symbolName = weather.currentWeather.symbolName + let attribution = try await WeatherService.shared.attribution + attributionLink = attribution.legalPageURL + attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL + } + } catch { + print("Could not gather weather information...", error.localizedDescription) + condition = .clear + symbolName = "cloud.fill" + } + } } } } diff --git a/Meshtastic/Views/Nodes/NodeInfoViewOld.swift b/Meshtastic/Views/Nodes/NodeInfoViewOld.swift deleted file mode 100644 index 94dfdf4f..00000000 --- a/Meshtastic/Views/Nodes/NodeInfoViewOld.swift +++ /dev/null @@ -1,261 +0,0 @@ -//// -//// NodeInfoView.swift -//// Meshtastic -//// -//// Created by Garth Vander Houwen on 4/2/23. -//// -// -//// -//// DistanceText.swift -//// Meshtastic -//// -//// Copyright(c) Garth Vander Houwen 8/19/22. -//// -// -//import SwiftUI -//import CoreLocation -//import MapKit -// -//struct NodeInfoView: View { -// -// var node: NodeInfoEntity -// -// var body: some View { -// let hwModelString = node.user?.hwModel ?? "UNSET" -// -// Divider() -// if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { -// HStack { -// VStack(alignment: .center) { -// CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 150) -// } -// Divider() -// VStack { -// if node.user != nil { -// Image(hwModelString) -// .resizable() -// .aspectRatio(contentMode: .fit) -// .frame(width: 100, height: 100) -// .cornerRadius(5) -// -// Text(String(hwModelString)) -// .foregroundColor(.gray) -// .font(.title).fixedSize() -// } -// } -// Divider() -// if node.snr != 0 { -// VStack(alignment: .center) { -// let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate) -// LoRaSignalStrengthIndicator(signalStrength: signalStrength) -// Text("Signal \(signalStrength.description)").font(.title) -// Text("SNR \(String(format: "%.2f", node.snr))dB") -// .foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate)) -// .font(.title3) -// Text("RSSI \(node.rssi)dB") -// .foregroundColor(getRssiColor(rssi: node.rssi)) -// .font(.title3) -// } -// Divider() -// } -// -// if node.hasDeviceMetrics { -// let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) -// let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity -// VStack(alignment: .center) { -// BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) -// if mostRecent?.voltage ?? 0 > 0.0 { -// -// Text(String(format: "%.2f", mostRecent?.voltage ?? 0.0) + " V") -// .font(.title) -// .foregroundColor(.gray) -// .fixedSize() -// } -// } -// .padding() -// } -// } -// .padding() -// -// Divider() -// HStack(alignment: .center) { -// -// VStack { -// HStack { -// Image(systemName: "person") -// .font(.title) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("user").font(.title)+Text(":").font(.title) -// } -// Text("!\(String(format: "%02x", node.num))") -// .font(.title).foregroundColor(.gray) -// } -// Divider() -// VStack { -// HStack { -// Image(systemName: "number") -// .font(.title2) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("Node Number:").font(.title) -// } -// Text(String(node.num)).font(.title).foregroundColor(.gray) -// } -// Divider() -// VStack { -// HStack { -// Image(systemName: "clock.badge.checkmark.fill") -// .font(.title) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("heard.last").font(.title)+Text(":").font(.title) -// -// } -// DateTimeText(dateTime: node.lastHeard) -// .font(.title3) -// .foregroundColor(.gray) -// } -// } -// Divider() -// -// } else { -// -// HStack { -// -// VStack(alignment: .center) { -// CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65) -// } -// if node.user != nil { -// Divider() -// VStack { -// Image(node.user!.hwModel ?? "unset".localized) -// .resizable() -// .aspectRatio(contentMode: .fit) -// .frame(width: 75, height: 75) -// .cornerRadius(5) -// Text(String(node.user!.hwModel ?? "unset".localized)) -// .font(.caption2).fixedSize() -// } -// } -// if node.snr != 0 { -// Divider() -// VStack(alignment: .center) { -// let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate) -// LoRaSignalStrengthIndicator(signalStrength: signalStrength) -// Text("Signal \(signalStrength.description)").font(.footnote) -// Text("SNR \(String(format: "%.2f", node.snr))dB") -// .foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate)) -// .font(.caption2) -// Text("RSSI \(node.rssi)dB") -// .foregroundColor(getRssiColor(rssi: node.rssi)) -// .font(.caption2) -// } -// } -// let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) -// if deviceMetrics?.count ?? 0 >= 1 { -// Divider() -// let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity -// VStack(alignment: .center) { -// BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) -// if mostRecent?.voltage ?? 0 > 0 { -// -// Text(String(format: "%.2f", mostRecent?.voltage ?? 0) + " V") -// .font(.callout) -// .foregroundColor(.gray) -// .fixedSize() -// } -// } -// } -// } -// Divider() -// HStack(alignment: .center) { -// VStack { -// HStack { -// Image(systemName: "person") -// .font(.title2) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("User Id:").font(.title2) -// } -// Text(node.user?.userId ?? "?").font(.title3).foregroundColor(.gray) -// } -// Divider() -// VStack { -// HStack { -// Image(systemName: "number") -// .font(.title2) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("Node Number:").font(.title2) -// } -// Text(String(node.num)).font(.title3).foregroundColor(.gray) -// } -// } -// Divider() -// } -// -// VStack { -// -// if node.hasPositions{ -// -// NavigationLink { -// PositionLog(node: node) -// } label: { -// -// Image(systemName: "building.columns") -// .symbolRenderingMode(.hierarchical) -// .font(.title) -// -// Text("Position Log") -// .font(.title3) -// } -// .fixedSize(horizontal: false, vertical: true) -// Divider() -// } -// -// if node.hasDeviceMetrics { -// -// NavigationLink { -// DeviceMetricsLog(node: node) -// } label: { -// -// Image(systemName: "flipphone") -// .symbolRenderingMode(.hierarchical) -// .font(.title) -// -// Text("Device Metrics Log") -// .font(.title3) -// } -// Divider() -// } -// if node.hasEnvironmentMetrics { -// NavigationLink { -// EnvironmentMetricsLog(node: node) -// } label: { -// -// Image(systemName: "chart.xyaxis.line") -// .symbolRenderingMode(.hierarchical) -// .font(.title) -// -// Text("Environment Metrics Log") -// .font(.title3) -// } -// Divider() -// } -// NavigationLink { -// DetectionSensorLog(node: node) -// } label: { -// -// Image(systemName: "sensor") -// .symbolRenderingMode(.hierarchical) -// .font(.title) -// -// Text("Detection Sensor Log") -// .font(.title3) -// } -// .fixedSize(horizontal: false, vertical: true) -// Divider() -// } -// } -//} diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 08b6ff7c..9a971ac0 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -17,7 +17,7 @@ enum SelectedDetail { -struct NodeListSplit: View { +struct NodeList: View { @State private var columnVisibility = NavigationSplitViewVisibility.all @State private var selectedNode: NodeInfoEntity?